def load_model(self, roots): ''' :param in model: the root object of a model, which is loaded into the tree-viewer and proxy models ''' # Set up the hidden Root model, with the 'model' object as its # only child # TODO: Figure out how to allow different roots and multiple # child objects. root = MetaModel() for r in roots: root.children.set_cardinality({r.__class__: '1..*'}) root.add_child(r) # Set up the proxy model for sorting/filtering self.proxy_model = SortFilterProxyModel(self) self.proxy_model.setDynamicSortFilter(True) self.proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) self.proxy_model.setSortRole(ItemModel.sort_role) self.proxy_model.setFilterRole(self.filter_role) self.proxy_model.setFilterKeyColumn(0) # the model stores the reference to the model that is # currently being edited/viewed; this can be a regular model, # a view model, or even a meta-model. All these models # inherit from the meta-metamodel so have the same interfaces # and can be interacted with in the same way self.model = ItemModel(root) self.model.setMeta(self.META) self.model.rowsAboutToBeRemoved.connect(self.modelRowsAboutToBeRemoved) # Link the actual model and the proxy model self.proxy_model.setSourceModel(self.model) self.filter_edit.textChanged.connect(self.proxy_model.setFilterRegExp) self.tree_view.setModel(self.proxy_model) self.tree_view.expandAll()
class Editor(QtGui.QMainWindow): '''The main editor class, which enables creating, loading, editing, and saving of models, meta-models, and view-models. Models are saved as \*.model files and contain the metamodel they were created with. Meta-models are saved as \*.meta files and can be used to create new models. View-models are saved as \*.view files and are automatically loaded as <Model Type>.view for each model that is opened in the visualizer. ''' ''' Models the editor is designed to load/edit/save All inherit at some point from the original base model classes Defined in meta.py ''' editor_modes = ['Model', 'Meta Model', 'View Model'] # Ways the tree view can filter the model (based on Meta-Type or Name) filter_roles = OrderedDict({ 'Meta': ItemModel.filter_meta_role, 'Name': ItemModel.filter_data_role }) def __init__(self): super(Editor, self).__init__() with open('MetaMetaModel.meta', 'w') as f: f.write(json.dumps(get_meta_meta_model(), indent=4)) # Set up the editor mode self.editor_mode = self.editor_modes[1] self.filter_role = self.filter_roles['Meta'] self.init_ui() self.clearModels() self.setWindowIcon(QtGui.QIcon('icons/editor.png')) def init_ui(self): QtGui.QApplication.setStyle(QtGui.QStyleFactory.create('Cleanlooks')) self.setStyleSheet('''QToolTip { background-color: black; color: white; border: black solid 1px }''') self.setGeometry(300, 300, 800, 600) self.setWindowTitle('Editor') # Create the actions for the program exitAction = Action('icons/toolbar/stop.png', 'Exit', self) exitAction.setShortcut('Ctrl+Q') exitAction.setStatusTip('Exit application') # note that this will call closeEvent exitAction.triggered.connect(self.close) newAction = Action('icons/toolbar/new.png', 'New', self) newAction.setStatusTip('New.') newAction.setShortcut('Ctrl+N') newAction.triggered.connect(self.newModel) openAction = Action('icons/toolbar/open.png', 'Open', self) openAction.setStatusTip('Open.') openAction.setShortcut('Ctrl+O') openAction.triggered.connect(self.openModel) saveAction = Action('icons/toolbar/save.png', 'Save', self) saveAction.setStatusTip('Save.') saveAction.setShortcut('Ctrl+S') saveAction.triggered.connect(self.saveModel) # Create the widgets for the program (embeddable in the # toolbar or elsewhere) self.mode_selector = QtGui.QComboBox(self) self.mode_selector.addItems(self.editor_modes) self.mode_selector.setCurrentIndex( self.editor_modes.index(self.editor_mode) ) self.mode_selector.currentIndexChanged.connect(self.changeMode) # Set up the Menus for the program self.menubar_init() self.menubar_add_menu('&File') self.menu_add_action('&File', exitAction) self.menu_add_action('&File', newAction) self.menu_add_action('&File', openAction) self.menu_add_action('&File', saveAction) # Set up the toolbars for the program self.toolbar_init() self.toolbar_create('toolbar1') self.toolbar_add_action('toolbar1', exitAction) self.toolbar_add_action('toolbar1', newAction) self.toolbar_add_action('toolbar1', openAction) self.toolbar_add_action('toolbar1', saveAction) self.toolbar_add_widget('toolbar1', self.mode_selector) # Set up the Tree View Widget self.tree_view = TreeView() self.tree_view.setUniformRowHeights(True) self.tree_view.setSortingEnabled(False) self.tree_view.activated.connect(self.openModelView) # don't want the tree collapsing when we open views self.tree_view.setExpandsOnDoubleClick(False) # Set up filtering on the tree_view self.filter_widget = QtGui.QWidget() self.filter_hbox = QtGui.QHBoxLayout() self.filter_label = QtGui.QLabel('Filter:') self.filter_type = QtGui.QComboBox(self) self.filter_type.addItems(self.filter_roles.keys()) self.filter_type.setCurrentIndex(0) self.filter_type.currentIndexChanged.connect(self.changeFilter) self.filter_hbox.addWidget(self.filter_label) self.filter_hbox.addWidget(self.filter_type) self.filter_widget.setLayout(self.filter_hbox) self.filter_edit = QtGui.QLineEdit() # Set up the navigator (tree viewer + filter) self.navigator = QtGui.QWidget() self.navigator_vbox = QtGui.QVBoxLayout() self.navigator_vbox.addWidget(self.filter_widget) self.navigator_vbox.addWidget(self.filter_edit) self.navigator_vbox.addWidget(self.tree_view) self.navigator.setLayout(self.navigator_vbox) # Create the Visualizer self.tabbedEditorWidget = TabbedEditor(self) self.openEditorTabs = {} # Split the main part into visualizer and tree_view self.splitter1 = QtGui.QSplitter(QtCore.Qt.Horizontal) self.splitter1.addWidget(self.navigator) self.splitter1.addWidget(self.tabbedEditorWidget) self.splitter1.setSizes([self.geometry().width()/4.0, 3.0 * self.geometry().width()/4.0]) # Set up the tabbed output viewer self.tabbedOutput = TabbedOutputWidget(self) # Split the Editor to show output self.splitter2 = QtGui.QSplitter(QtCore.Qt.Vertical) self.splitter2.addWidget(self.splitter1) self.splitter2.addWidget(self.tabbedOutput) self.splitter2.setSizes([4.0 * self.geometry().height()/5.0, self.geometry().height()/5.0]) # Set the central widget of the application self.setCentralWidget(self.splitter2) self.center() self.show() def center(self): qr = self.frameGeometry() cp = QtGui.QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) def clearModels(self): '''Clears all model data from the editor.''' self.META = None self.model = None self.proxy_model = None self.tree_view.reset() self.tree_view.setModel(None) def clearViewer(self): '''Close all visualizer views.''' self.openEditorTabs = {} self.tabbedEditorWidget.clear() def changeFilter(self, index): '''Event callback for when the user changes the filter type for the navigator. ''' text = str(self.filter_type.currentText()) self.filter_role = self.filter_roles[text] if self.model and self.proxy_model: self.proxy_model.setFilterRole(self.filter_role) # self.proxy_model.invalidate() def changeMode(self, index): '''Event callback for when the user changes the editor mode.''' text = str(self.mode_selector.currentText()) if text != self.editor_mode: self.editor_mode = text self.clearModels() self.clearViewer() def openModelView(self, modelIndex): '''Event callback for when the user double-clicks on a model item in the tree-viewer. Sends the model item to the visualizer and creates a new EditorView for it if one does not exist. :param in modelIndex: index into the AbstractItemModel which has been selected for viewing. ''' mi = self.proxy_model.mapToSource(modelIndex) item = self.model.getModel(mi) key = str(item.uuid) name = item.get_attribute('Name').scoped() if key not in self.openEditorTabs: ev = EditorView(self.tabbedEditorWidget) ev.setModel(self.model) self.openEditorTabs[key] = ev else: ev = self.openEditorTabs[key] self.tabbedEditorWidget.addTab(ev, name) ev.init_ui(index=mi, fname=item.kind() + '.view') self.tabbedEditorWidget.setCurrentIndex( self.tabbedEditorWidget.indexOf(ev) ) def newModel(self, event): '''Callback for creating a new (meta-, view-) model.''' self.clearModels() self.clearViewer() root = None if self.editor_mode == 'Model': fname = QtGui.QFileDialog.getOpenFileName( self, 'Select Meta Model', '', 'Meta Model Files (*.meta)', options=QtGui.QFileDialog.Options() ) if fname: self.open_meta(fname) uuid_dict = OrderedDict() base = MetaModel.fromMeta(self.META['__ROOT__'][0], uuid_dict) roots = [ MetaModel.fromMeta(r, uuid_dict)() for r in self.META['__ROOT__'] ] elif self.editor_mode == 'Meta Model': self.open_meta('MetaMetaModel.meta') roots = [MetaModel()] elif self.editor_mode == 'View Model': # TODO: Replace this with view_model code self.open_meta('MetaViewModel.meta') roots = [ViewModel()] if roots: self.load_model(roots) def openModel(self, event): '''Callback to allow the user to select a model file based on the current mode of the editor. ''' ftype = '{}'.format(self.editor_mode.lower().split()[0]) fname = QtGui.QFileDialog.getOpenFileName( self, 'Open {}'.format(self.editor_mode), '', '{} Files (*.{})'.format(self.editor_mode, ftype), options=QtGui.QFileDialog.Options() ) if fname: self.clearModels() self.clearViewer() roots = self.open_model(fname) if roots: self.load_model(roots) def open_meta(self, fname): '''Decodes a saved meta-model file and loads it into the editor.''' with open(fname, 'r') as f: meta_dict = json.loads(f.read()) print 'Loaded meta-model {}'.format(fname) # uuid_dict = {} # base = MetaModel.fromMeta(meta_dict['__ROOT__'], uuid_dict) # TODO: create meta_dict from loaded meta-model # TODO: Probably will need to resolve UUID referencce problems here uuid_dict = {} unresolved_keys = {} for r in meta_dict['__ROOT__']: a = MetaModel.fromDict(r, uuid_dict, unresolved_keys) buildMeta(meta_dict, r, uuid_dict) self.META = meta_dict def open_model(self, fname): '''Decodes a saved model {*.meta, *.model} file and loads it into the editor. ''' roots = [] with open(fname, 'r') as f: model_dict = json.loads(f.read()) print 'Loaded model {}'.format(fname) model_meta = model_dict['__META__'] meta_fname = model_meta['Name'] + '.meta' try: self.open_meta(meta_fname) except: print 'ERROR: Cannot find {}, please select location.'.format( meta_fname ) if self.META['MD5'] != model_meta['MD5']: # TODO: Do something more if the models aren't in sync print 'ERROR: Model and meta are out of sync!' else: if checkModelToMeta(model_dict['__ROOT__'], self.META): print 'Model conforms to Meta-Model.' # TODO: instantiate objects for model from model_dict # based on meta_dict roots = convertDictToModel( model_dict['__ROOT__'], self.META ) else: print 'ERROR: Model does not conform to Meta-Model!' return roots def load_model(self, roots): ''' :param in model: the root object of a model, which is loaded into the tree-viewer and proxy models ''' # Set up the hidden Root model, with the 'model' object as its # only child # TODO: Figure out how to allow different roots and multiple # child objects. root = MetaModel() for r in roots: root.children.set_cardinality({r.__class__: '1..*'}) root.add_child(r) # Set up the proxy model for sorting/filtering self.proxy_model = SortFilterProxyModel(self) self.proxy_model.setDynamicSortFilter(True) self.proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) self.proxy_model.setSortRole(ItemModel.sort_role) self.proxy_model.setFilterRole(self.filter_role) self.proxy_model.setFilterKeyColumn(0) # the model stores the reference to the model that is # currently being edited/viewed; this can be a regular model, # a view model, or even a meta-model. All these models # inherit from the meta-metamodel so have the same interfaces # and can be interacted with in the same way self.model = ItemModel(root) self.model.setMeta(self.META) self.model.rowsAboutToBeRemoved.connect(self.modelRowsAboutToBeRemoved) # Link the actual model and the proxy model self.proxy_model.setSourceModel(self.model) self.filter_edit.textChanged.connect(self.proxy_model.setFilterRegExp) self.tree_view.setModel(self.proxy_model) self.tree_view.expandAll() def saveModel(self, event): '''Saves a model according to the current mode of the editor.''' ftype = '{}'.format(self.editor_mode.lower().split()[0]) fname = QtGui.QFileDialog.getSaveFileName( self, 'Save {}'.format(self.editor_mode), '', '{} Files (*.{})'.format(self.editor_mode, ftype), options=QtGui.QFileDialog.Options() ) fname = str(fname) if fname: if fname[-len(ftype):] != ftype: fname += '.{}'.format(ftype) root = self.model.getModel(QtCore.QModelIndex()) # the actual root is not displayed and is always a Model() rootDict = [MetaModel.toDict(c) for c in root.children] modelDict = OrderedDict() modelDict['Name'] = fname.split('/')[-1].split('.')[-2] modelDict['MD5'] = hashlib.md5(str(rootDict)).hexdigest() modelDict['__META__'] = { 'Name': self.META['Name'], 'MD5': self.META['MD5'] } modelDict['__ROOT__'] = rootDict dictStr = json.dumps(modelDict, indent=4) with open(fname, 'w') as f: f.write(dictStr) return 0 @QtCore.pyqtSlot(QtCore.QModelIndex, int, int) def modelRowsAboutToBeRemoved(self, parent, start, end): ''' Slot for handling object deletion. Removes any open tabs/editor views for the removed object and all its children. ''' mi = parent.child(start, 0) item = self.model.getModel(mi) # print 'about to remove item: {}'.format(item) self.removeItem(item) def removeItem(self, item): '''Recursively removes the tabs for item and all its children.''' for c in item.children: self.removeItem(c) key = str(item.uuid) self.removeTab(key) def removeTab(self, key): '''Simple function to clear tab data for an object whose uuid = key.''' if key in self.openEditorTabs: # print 'deleting tab for {}'.format(key) ev = self.openEditorTabs[key] index = self.tabbedEditorWidget.indexOf(ev) if index >= 0: self.tabbedEditorWidget.removeTab(index) self.openEditorTabs.pop(key, None) del ev def closeEvent(self, event): event.accept() return reply = QtGui.QMessageBox.question( self, 'Quit', 'Sure you want to quit?', QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) if reply == QtGui.QMessageBox.Yes: event.accept() else: event.ignore() from menubar import \ menubar_init, \ menubar_add_menu, \ menu_add_action from toolbar import \ toolbar_init, \ toolbar_create, \ toolbar_add_action, \ toolbar_add_widget, \ toolbar_remove from action import \ action_init, \ action_create from context_menu import \ context_menu_init, \ context_menu_create, \ context_menu_add_action