def __init__(self, parent_widget): QDialog.__init__(self, parent_widget) self.setupUi(self) settings_directory = os.path.join(os.environ['OPUS_HOME'], 'settings') self._config_filename = os.path.join(settings_directory, 'database_server_configurations.xml') try: root = ElementTree(file=self._config_filename).getroot() view = XmlView(self) model = XmlModel(root) delegate = XmlItemDelegate(view) view.setModel(model) # Turns out that Qt Garbage collects the model (and delegate) if we don't explicitly # bind it to a Python object in addition to using the PyQt .setModel() method. view._model = model view._delegate = delegate view.setItemDelegate(delegate) view.openDefaultItems() self.gridlayout.addWidget(view) self.tree_view = view self.xml_root = root return except IOError, ex: MessageBox.error(mainwindow = self, text = 'Could not initialize Database Settings', detailed_text = str(ex)) self.xml_root = None self._config_filename = '' self.configFile = None
def add_model_view_delegate(self): ''' Initialize and bind the Model, View and Delegate for this controller. This method is called before any initialization of the widgets. Subclasses that wish to use their own model, view and/or delegate should override this method and initialize their own widgets. ''' self.model = XmlModel(self.xml_root, self.project) self.view = XmlView(self.manager.base_widget) self.delegate = XmlItemDelegate(self.view)
class XmlController(object): ''' Controller class for XML Trees. ''' def __init__(self, manager): ''' @param manager (AbstractManager) The parent manager for this XmlController The XmlController will attach itself to manger.base_widget on initialization. ''' self.manager = manager self.project = self.manager.project self.xml_root = manager.xml_root self.model = None self.view = None self.delegate = None self.add_model_view_delegate() self.view.setItemDelegate(self.delegate) self.view.setModel(self.model) self.view.openDefaultItems() self.manager.base_widget.layout().addWidget(self.view) self.view.setContextMenuPolicy(Qt.CustomContextMenu) QObject.connect(self.view, SIGNAL("customContextMenuRequested(const QPoint &)"), self.process_custom_menu) QObject.connect(self.view, SIGNAL('activated(const QModelIndex&)'), self.on_activated) # Actions for common menu choices # Note that revert and delete are the same action, but we want to present them differently # to show that deleting an inherited node (reverting) will keep the node around self.act_remove_selected = self.create_action( 'delete', 'Delete', self.remove_selected_node) self.act_revert = self.create_action('revert', 'Revert to inherited value', self.remove_selected_node) self.act_make_editable = self.create_action( 'make_editable', 'Make node local', self.make_selected_editable) self.act_clone_node = self.create_action('clone', 'Duplicate', self.clone_selected_node) self.act_rename_node = self.create_action('rename', 'Rename', self.rename_selected_node) self.actExportXMLToFile = self.create_action( 'export', "Export XML Node With Inherited Values To File", self.exportXMLToFile) self.actImportXMLFromFile = self.create_action( 'import', "Import XML Node From File", self.importXMLFromFile) self.actExportXMLToFile_all = self.create_action( 'export_all', "Export all XML Nodes With Inherited Values To File", self.exportXMLToFile) self.actImportXMLFromFile_all = self.create_action( 'import_all', "Import all XML Nodes From File", self.importXMLFromFile) self.actExportXMLToFile_without_inherited = self.create_action( 'export_without_inherited', 'Export XML Node To File', self.export_without_inherited) self.actExportXMLToFile_all_without_inherited = self.create_action( 'export_all_without_inherited', 'Export all XML Nodes To File', self.export_without_inherited) self.act_edit = self.create_action('inspect', 'Edit as XML', self.edit) self.act_edit_all = self.create_action('inspect_all', 'Edit all as XML', self.edit) self.act_copy_to_parent = self.create_action( 'copy_to_parent', 'Copy to parent', self.copy_selected_to_parent) self.act_move_to_parent = self.create_action( 'move_to_parent', 'Move to parent', self.move_selected_to_parent) def add_model_view_delegate(self): ''' Initialize and bind the Model, View and Delegate for this controller. This method is called before any initialization of the widgets. Subclasses that wish to use their own model, view and/or delegate should override this method and initialize their own widgets. ''' self.model = XmlModel(self.xml_root, self.project) self.view = XmlView(self.manager.base_widget) self.delegate = XmlItemDelegate(self.view) def create_action(self, icon_name, text, callback): return create_qt_action(icon_name, text, callback, self.view) def rebuild_tree(self): ''' Rebuild the model tree ''' if self.model: self.model.rebuild_tree() # CK: TODO This is a method left from before the refactoring in december, don't know # if it's needed/used def close(self): ''' Closes the controller and removes it from the parent if it is not empty. @return: True. Previous behavior was to return False if self.model was dirty. This is no longer done as dirty checks have been moved. ''' self.view.hide() self.manager.base_widget.layout().removeWidget(self.view) return True def selected_item(self): ''' Get the currently selected item in the controller's view. @return: the selected item (XmlItem) or None if no item is selected. ''' index = self.selected_index() return index.internalPointer() if index else None def selected_index(self): ''' Get the index for the currently selected item in the controller's view. @return: the index (QModelIndex) for the selected item or None ''' index = self.view.currentIndex() if index.isValid(): return index return None def has_selected_item(self): ''' Tests if the controller's view has a selected item or not. @return: True if there is a selected item, otherwise False. ''' return self.selected_index() is not None def process_custom_menu(self, position): ''' Default method for creating a context sensitive popup menu. Calls self.add_custom_menu_items_for_node() to populate the menu with context-specific menu items. @param position (QPoint) point of request for the popupmenu. ''' item = self.select_item_at(position) assert item is not None node = item.node menu = self.create_custom_menu_for_node(node) if not menu.isEmpty(): menu.exec_(self.view.mapToGlobal(position)) def on_activated(self, index): if not self.execute_default_action(index): self.view.setExpanded(index, not self.view.isExpanded(index)) def execute_default_action(self, index): if self.model.hasChildren(index) and not self.view.isExpanded(index): return node = index.internalPointer().node if node is None: return menu = self.create_custom_menu_for_node(node) act = menu.defaultAction() if act is None: return act.activate(QAction.Trigger) return True def create_custom_menu_for_node(self, node): menu = QMenu(self.view) if node is self.model.root_node(): self.add_default_menu_items_for_widget(menu) elif not self.add_custom_menu_items_for_node(node, menu): if not menu.isEmpty(): menu.addSeparator() self.add_default_menu_items_for_node(node, menu) return menu def set_project_dirty(self): if self.project: self.project.dirty = True def remove_selected_node(self): ''' Removes the selected item from the model. ''' if not self.has_selected_item(): return index = self.selected_index() self.model.removeRow(index.row(), self.model.parent(index)) self.view.clearSelection() self.set_project_dirty() # CK: this is a helper function for the clone_node method, but maybe its general enough to be # promoted to a higher abstraction layer? def _get_unique_name_like_node(self, node): ''' Search sibling items with the same tag to find a name that is guaranteed to be unique. First it tries to insert the item with it's current name. If that fails it appends _copy to the name and tries again. Subsequent tries have _copyX appended to them where X is a number from 1 -> @param node (Element) the item holding the node to find a new name for @return (String) a unique name for the new node. ''' base_name = node.get('name') # Get all the names that currently exist in the same level if node.getparent() is None: # no parent = no siblings return base_name sibling_nodes = node.getparent().getchildren() taken_names = [n.get('name') for n in sibling_nodes] if base_name not in taken_names: return base_name try_name = 'Copy of %s' % base_name copy_number = 0 while try_name in taken_names: copy_number += 1 try_name = 'Copy %d of %s' % (copy_number, base_name) return try_name def clone_selected_node(self): ''' Clone the selected node and insert it as a sibling with a unique name ''' if not self.has_selected_item(): return index = self.selected_index() item = self.selected_item() cloned_node = deepcopy(item.node) cloned_node.set('name', self._get_unique_name_like_node(item.node)) # Insert the cloned node into the tree if self.model.insert_sibling(cloned_node, index) is not None: index_of_clone = self.model.last_inserted_index # Select the new clone if it was inserted if index_of_clone is not None: self.view.setCurrentIndex(index_of_clone) self.set_project_dirty() def rename_selected_node(self): ''' Opens a dialog box for changing the node name. ''' if not self.has_selected_item(): return item = self.selected_item() node = item.node taken_names = [ n.get('name') for n in node.getparent().getchildren() if not n is node ] dialog = RenameDialog(node.get('name'), taken_names, self.view) if dialog.exec_() == dialog.Accepted: node.set('name', dialog.accepted_name) self.set_project_dirty() def make_selected_editable(self): ''' Copies the selected node to this project and strips the inhertied flag from all it's immidiate parents and all it's child nodes. ''' if not self.has_selected_item(): return self.model.make_item_local(self.selected_item()) def rebuild(self): '''rebuild...''' self.model.rebuild_tree() def copy_selected_to_parent(self): ''' Copy the selected item to the first parent configuration ''' if not self.has_selected_item(): return if not get_mainwindow_instance().okToCloseProject( 'copying the node to the parent configuration (reload required)' ): return self.model.copy_to_parent(self.selected_index()) get_mainwindow_instance().reloadProject() def move_selected_to_parent(self): ''' Move the selected item to the first parent configuration ''' if not self.has_selected_item(): return if not get_mainwindow_instance().okToCloseProject( 'moving the node to the parent configuration (reload required)' ): return self.model.move_to_parent(self.selected_index()) self.project.save() get_mainwindow_instance().reloadProject() def select_item_at(self, point): ''' Select the item at "point" to visualize which item we are working on and making the item accessible through self.selected_item(). If the point is invalid, the currently selected item, if any, is deselected, and the root item is returned. @param point (QPoint): coordinates for where to get the item. @return: The selected item if the point was valid, None otherwise ''' index = self.view.indexAt(point) if not index.isValid or index.column( ) != 0: # only allow right-clicking on left side nodes index = self.view.rootIndex() item = self.model.root_item() else: item = index.internalPointer() self.view.setCurrentIndex(index) assert item is not None return item def get_selected_or_root_node(self): if not self.has_selected_item(): return self.model.root_node() else: return self.selected_item().node def get_selected_or_root_node_and_index(self): node = self.get_selected_or_root_node() if node is self.model.root_node(): index = self.model.index_for_item(self.model.root_item()) else: index = self.model.index_for_node(node) assert index is not None return node, index def get_clean_copy_of_selected_node(self, inherited=True): root_node = self.get_selected_or_root_node() root_node = deepcopy(root_node) if not inherited: self.project.xml_config._clean_tree(root_node) # Write out the file self.project.xml_config._indent(root_node) return root_node def export_without_inherited(self): self.exportXMLToFile(inherited=False) def exportXMLToFile(self, inherited=True): ''' NO DOCUMENTATION ''' # Ask the users where they want to save the file start_dir = paths.get_project_configs_path() configDialog = QFileDialog() filter_str = QString("*.xml") fd = configDialog.getSaveFileName(self.manager.base_widget, QString("Save As..."), QString(start_dir), filter_str) # Check for cancel if len(fd) == 0: return fileNameInfo = QFileInfo(QString(fd)) fileName = fileNameInfo.fileName().trimmed() fileNamePath = fileNameInfo.absolutePath().trimmed() saveName = os.path.join(str(fileNamePath), str(fileName)) root_node = self.get_clean_copy_of_selected_node(inherited) ElementTree(root_node).write(saveName) def check_import_node(self, clicked_node, xml_node): if (clicked_node.tag == xml_node.tag) and (clicked_node.get('name') == xml_node.get('name')): return root_dummy = clicked_node.tag if 'name' in clicked_node: root_dummy += ' name="%s"' % clicked_node['name'] root_dummy = '<%s/>' % root_dummy raise ValueError('Expected an element like %s as root element' % root_dummy) def importXMLFromFile(self): ''' NO DOCUMENTATION ''' # print "importXMLFromFile" # First, prompt the user for the filename to read in start_dir = paths.get_project_configs_path() configDialog = QFileDialog() filter_str = QString("*.xml") fd = configDialog.getOpenFileName( self.manager.base_widget, "Please select an XML file to import...", start_dir, filter_str) # Check for cancel if len(fd) == 0: return fileName = QString(fd) # Pass that in to create a new XMLConfiguration try: options = '' xml_tree = load_xml_file(str(fileName)) xml_node = xml_tree.getroot() options = ' or another node in the tree view' self.import_from_node(xml_node) except Exception, e: MessageBox.error(mainwindow=self.view, text='Cannot insert XML file.', detailed_text='XML insert failed. ' '%s. ' 'Please select another XML file%s.' % (e, options)) return
class XmlController(object): ''' Controller class for XML Trees. ''' def __init__(self, manager): ''' @param manager (AbstractManager) The parent manager for this XmlController The XmlController will attach itself to manger.base_widget on initialization. ''' self.manager = manager self.project = self.manager.project self.xml_root = manager.xml_root self.model = None self.view = None self.delegate = None self.add_model_view_delegate() self.view.setItemDelegate(self.delegate) self.view.setModel(self.model) self.view.openDefaultItems() self.manager.base_widget.layout().addWidget(self.view) self.view.setContextMenuPolicy(Qt.CustomContextMenu) QObject.connect(self.view, SIGNAL("customContextMenuRequested(const QPoint &)"), self.process_custom_menu) # Actions for common menu choices # Note that revert and delete are the same action, but we want to present them differently # to show that deleting an inherited node (reverting) will keep the node around self.act_remove_selected = self.create_action('delete', 'Delete', self.remove_selected_node) self.act_revert = self.create_action('revert', 'Revert to inherited value', self.remove_selected_node) self.act_make_editable = self.create_action('make_editable', 'Make node local', self.make_selected_editable) self.act_clone_node = self.create_action('clone', 'Duplicate', self.clone_selected_node) self.act_rename_node = self.create_action('rename', 'Rename', self.rename_selected_node) def add_model_view_delegate(self): ''' Initialize and bind the Model, View and Delegate for this controller. This method is called before any initialization of the widgets. Subclasses that wish to use their own model, view and/or delegate should override this method and initialize their own widgets. ''' self.model = XmlModel(self.xml_root, self.project) self.view = XmlView(self.manager.base_widget) self.delegate = XmlItemDelegate(self.view) def create_action(self, icon_name, text, callback): return create_qt_action(icon_name, text, callback, self.view) def rebuild_tree(self): ''' Rebuild the model tree ''' if self.model: self.model.rebuild_tree() # CK: TODO This is a method left from before the refactoring in december, don't know # if it's needed/used def close(self): ''' Closes the controller and removes it from the parent if it is not empty. @return: True. Previous behavior was to return False if self.model was dirty. This is no longer done as dirty checks have been moved. ''' self.view.hide() self.manager.base_widget.layout().removeWidget(self.view) return True def selected_item(self): ''' Get the currently selected item in the controller's view. @return: the selected item (XmlItem) or None if no item is selected. ''' index = self.selected_index() return index.internalPointer() if index else None def selected_index(self): ''' Get the index for the currently selected item in the controller's view. @return: the index (QModelIndex) for the selected item or None ''' index = self.view.currentIndex() if index.isValid(): return index return None def has_selected_item(self): ''' Tests if the controller's view has a selected item or not. @return: True if there is a selected item, otherwise False. ''' return self.selected_index() is not None def process_custom_menu(self, position): ''' Abstract method for creating a context sensitive popup menu. @param position (QPoint) point of request for the popupmenu. ''' item = self.select_item_at(position) if not item: return node = item.node menu = QMenu(self.view) self.add_default_menu_items_for_node(node, menu) if not menu.isEmpty(): menu.exec_(QCursor.pos()) def remove_selected_node(self): ''' Removes the selected item from the model. ''' if not self.has_selected_item(): return index = self.selected_index() self.model.removeRow(index.row(), self.model.parent(index)) self.view.clearSelection() # CK: this is a helper function for the clone_node method, but maybe its general enough to be # promoted to a higher abstraction layer? def _get_unique_name_like_node(self, node): ''' Search sibling items with the same tag to find a name that is guaranteed to be unique. First it tries to insert the item with it's current name. If that fails it appends _copy to the name and tries again. Subsequent tries have _copyX appended to them where X is a number from 1 -> @param node (Element) the item holding the node to find a new name for @return (String) a unique name for the new node. ''' base_name = node.get('name') # Get all the names that currently exist in the same level if node.getparent() is None: # no parent = no siblings return base_name sibling_nodes = node.getparent().getchildren() taken_names = [n.get('name') for n in sibling_nodes] if base_name not in taken_names: return base_name try_name = 'Copy of %s' % base_name copy_number = 0 while try_name in taken_names: copy_number += 1 try_name = 'Copy %d of %s' % (copy_number, base_name) return try_name def clone_selected_node(self): ''' Clone the selected node and insert it as a sibling with a unique name ''' if not self.has_selected_item(): return index = self.selected_index() item = self.selected_item() cloned_node = deepcopy(item.node) cloned_node.set('name', self._get_unique_name_like_node(item.node)) # Insert the cloned node into the tree if self.model.insert_sibling(cloned_node, index) is not None: index_of_clone = self.model.last_inserted_index # Select the new clone if it was inserted if index_of_clone is not None: self.view.setCurrentIndex(index_of_clone) def rename_selected_node(self): ''' Opens a dialog box for changing the node name. ''' if not self.has_selected_item(): return item = self.selected_item() node = item.node taken_names = [n.get('name') for n in node.getparent().getchildren() if not n is node] dialog = RenameDialog(node.get('name'), taken_names, self.view) if dialog.exec_() == dialog.Accepted: node.set('name', dialog.accepted_name) def make_selected_editable(self): ''' Copies the selected node to this project and strips the inhertied flag from all it's immidiate parents and all it's child nodes. ''' if not self.has_selected_item(): return self.model.make_item_local(self.selected_item()) def select_item_at(self, point): ''' Select the item at "point" to visualize which item we are working on and making the item accessible through self.selected_item(). @param point (QPoint): coordinates for where to get the item. @return: The selected item if the point was valid, None otherwise ''' index = self.view.indexAt(point) if not index.isValid or index.column() != 0: # only allow right-clicking on left side nodes return None self.view.setCurrentIndex(index) return index.internalPointer() def add_default_menu_items_for_node(self, node, menu): ''' Append a list of menu items that is common for all nodes regardless of which manager they are in. @param node (Element): node to inspect @param menu (QMenu): menu to append actions to ''' added_actions = [] # Inherited nodes can be made local if node.get('inherited'): added_actions.append(self.act_make_editable) # nodes that do not have a 'name' attribute are special nodes that should not be copyable # for example; <expression_library>, <model_manager> etc.. if node.get('name') is not None: # or node.get('copyable') == 'True': added_actions.append(self.act_clone_node) # named nodes that are not inherited can be renamed if node.get('name') is not None and not node.get('inherited'): added_actions.append(self.act_rename_node) if self.project.is_shadowing(node): added_actions.append(self.act_revert) elif node.tag in _REMOVABLE_NODE_TYPES and not node.get('inherited'): added_actions.append(self.act_remove_selected) # Separate from other items if added_actions and not menu.isEmpty(): menu.addSeparator() map(lambda x: menu.addAction(x), added_actions)