class WTreeEdit(QWidget): """TreeEdit widget is to show and edit all of the pyleecan objects data.""" # Signals dataChanged = Signal() def __init__(self, obj, *args, **kwargs): QWidget.__init__(self, *args, **kwargs) self.class_dict = ClassInfo().get_dict() self.treeDict = None # helper to track changes self.obj = obj # the object self.is_save_needed = False self.model = TreeEditModel(obj) self.setupUi() # === Signals === self.selectionModel.selectionChanged.connect(self.onSelectionChanged) self.treeView.collapsed.connect(self.onItemCollapse) self.treeView.expanded.connect(self.onItemExpand) self.treeView.customContextMenuRequested.connect(self.openContextMenu) self.model.dataChanged.connect(self.onDataChanged) self.dataChanged.connect(self.setSaveNeeded) # === Finalize === # set 'root' the selected item and resize columns self.treeView.setCurrentIndex(self.treeView.model().index(0, 0)) self.treeView.resizeColumnToContents(0) def setupUi(self): """Setup the UI""" # === Widgets === # TreeView self.treeView = QTreeView() # self.treeView.rootNode = model.invisibleRootItem() self.treeView.setModel(self.model) self.treeView.setAlternatingRowColors(False) # self.treeView.setColumnWidth(0, 150) self.treeView.setMinimumWidth(100) self.treeView.setContextMenuPolicy(Qt.CustomContextMenu) self.selectionModel = self.treeView.selectionModel() self.statusBar = QStatusBar() self.statusBar.setSizeGripEnabled(False) self.statusBar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) self.statusBar.setStyleSheet( "QStatusBar {border: 1px solid rgb(200, 200, 200)}") self.saveLabel = QLabel("unsaved") self.saveLabel.setVisible(False) self.statusBar.addPermanentWidget(self.saveLabel) # Splitters self.leftSplitter = QSplitter() self.leftSplitter.setStretchFactor(0, 0) self.leftSplitter.setStretchFactor(1, 1) # === Layout === # Horizontal Div. self.hLayout = QVBoxLayout() self.hLayout.setContentsMargins(0, 0, 0, 0) self.hLayout.setSpacing(0) # add widgets to layout self.hLayout.addWidget(self.leftSplitter) self.hLayout.addWidget(self.statusBar) # add widgets self.leftSplitter.addWidget(self.treeView) self.setLayout(self.hLayout) def update(self, obj): """Check if object has changed and update tree in case.""" if not obj is self.obj: self.obj = obj self.model = TreeEditModel(obj) self.treeView.setModel(self.model) self.model.dataChanged.connect(self.onDataChanged) self.selectionModel = self.treeView.selectionModel() self.selectionModel.selectionChanged.connect( self.onSelectionChanged) self.treeView.setCurrentIndex(self.treeView.model().index(0, 0)) self.setSaveNeeded(True) def setSaveNeeded(self, state=True): self.is_save_needed = state self.saveLabel.setVisible(state) def openContextMenu(self, point): """Generate and open context the menu at the given point position.""" index = self.treeView.indexAt(point) pos = QtGui.QCursor.pos() if not index.isValid(): return # get the data item = self.model.item(index) obj_info = self.model.get_obj_info(item) # init the menu menu = TreeEditContextMenu(obj_dict=obj_info, parent=self) menu.exec_(pos) self.onSelectionChanged(self.selectionModel.selection()) def onItemCollapse(self, index): """Slot for item collapsed""" # dynamic resize for ii in range(3): self.treeView.resizeColumnToContents(ii) def onItemExpand(self, index): """Slot for item expand""" # dynamic resize for ii in range(3): self.treeView.resizeColumnToContents(ii) def onDataChanged(self, first=None, last=None): """Slot for changed data""" self.dataChanged.emit() self.onSelectionChanged(self.selectionModel.selection()) def onSelectionChanged(self, itemSelection): """Slot for changed item selection""" # get the index if itemSelection.indexes(): index = itemSelection.indexes()[0] else: index = self.treeView.model().index(0, 0) self.treeView.setCurrentIndex(index) return # get the data item = self.model.item(index) obj = item.object() typ = type(obj).__name__ obj_info = self.model.get_obj_info(item) ref_typ = obj_info["ref_typ"] if obj_info else None # set statusbar information on class typ msg = f"{typ} (Ref: {ref_typ})" if ref_typ else f"{typ}" self.statusBar.showMessage(msg) # --- choose the respective widget by class type --- # numpy array -> table editor if typ == "ndarray": widget = WTableData(obj, editable=True) widget.dataChanged.connect(self.dataChanged.emit) elif typ == "MeshSolution": widget = WMeshSolution(obj) # only a view (not editable) # list (no pyleecan type, non empty) -> table editor # TODO add another widget for lists of non 'primitive' types (e.g. DataND) elif isinstance(obj, list) and not self.isListType(ref_typ) and obj: widget = WTableData(obj, editable=True) widget.dataChanged.connect(self.dataChanged.emit) # generic editor else: # widget = SimpleInputWidget().generate(obj) widget = WTableParameterEdit(obj) widget.dataChanged.connect(self.dataChanged.emit) # show the widget if self.leftSplitter.widget(1) is None: self.leftSplitter.addWidget(widget) else: self.leftSplitter.replaceWidget(1, widget) widget.setParent( self.leftSplitter) # workaround for PySide2 replace bug widget.show() pass def isListType(self, typ): if not typ: return False return typ[0] == "[" and typ[-1] == "]" and typ[1:-1] in self.class_dict def isDictType(self, typ): if not typ: return False return typ[0] == "{" and typ[-1] == "}" and typ[1:-1] in self.class_dict
class ManageRelationshipsDialog(AddOrManageRelationshipsDialog): """A dialog to query user's preferences for managing relationships. """ def __init__(self, parent, db_mngr, *db_maps, relationship_class_key=None): """ Args: parent (SpineDBEditor): data store widget db_mngr (SpineDBManager): the manager to do the removal *db_maps: DiffDatabaseMapping instances relationship_class_key (str, optional): relationships class name, object_class name list string. """ super().__init__(parent, db_mngr, *db_maps) self.setWindowTitle("Manage relationships") self.table_view.setSelectionBehavior(QAbstractItemView.SelectRows) self.remove_rows_button.setToolButtonStyle(Qt.ToolButtonIconOnly) self.remove_rows_button.setToolTip( "<p>Remove selected relationships.</p>") self.remove_rows_button.setIconSize(QSize(24, 24)) self.db_map = db_maps[0] self.relationship_ids = dict() layout = self.header_widget.layout() self.db_combo_box = QComboBox(self) layout.addSpacing(32) layout.addWidget(QLabel("Database")) layout.addWidget(self.db_combo_box) self.splitter = QSplitter(self) self.add_button = QToolButton(self) self.add_button.setToolTip( "<p>Add relationships by combining selected available objects.</p>" ) self.add_button.setIcon(QIcon(":/icons/menu_icons/cubes_plus.svg")) self.add_button.setIconSize(QSize(24, 24)) self.add_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.add_button.setText(">>") label_available = QLabel("Available objects") label_existing = QLabel("Existing relationships") self.layout().addWidget(self.header_widget, 0, 0, 1, 4, Qt.AlignHCenter) self.layout().addWidget(label_available, 1, 0) self.layout().addWidget(label_existing, 1, 2) self.layout().addWidget(self.splitter, 2, 0) self.layout().addWidget(self.add_button, 2, 1) self.layout().addWidget(self.table_view, 2, 2) self.layout().addWidget(self.remove_rows_button, 2, 3) self.layout().addWidget(self.button_box, 3, 0, -1, -1) self.hidable_widgets = [ self.add_button, label_available, label_existing, self.table_view, self.remove_rows_button, ] for widget in self.hidable_widgets: widget.hide() self.existing_items_model = MinimalTableModel(self, lazy=False) self.new_items_model = MinimalTableModel(self, lazy=False) self.model.sub_models = [ self.new_items_model, self.existing_items_model ] self.db_combo_box.addItems([db_map.codename for db_map in db_maps]) self.reset_relationship_class_combo_box(db_maps[0].codename, relationship_class_key) self.connect_signals() def make_model(self): return CompoundTableModel(self) def splitter_widgets(self): return [self.splitter.widget(i) for i in range(self.splitter.count())] def connect_signals(self): """Connect signals to slots.""" super().connect_signals() self.db_combo_box.currentTextChanged.connect( self.reset_relationship_class_combo_box) self.add_button.clicked.connect(self.add_relationships) @Slot(str) def reset_relationship_class_combo_box(self, database, relationship_class_key=None): self.db_map = self.keyed_db_maps[database] self.relationship_class_keys = list( self.db_map_rel_cls_lookup[self.db_map]) self.rel_cls_combo_box.addItems( [f"{name}" for name, _ in self.relationship_class_keys]) try: current_index = self.relationship_class_keys.index( relationship_class_key) self.reset_model(current_index) self._handle_model_reset() except ValueError: current_index = -1 self.rel_cls_combo_box.setCurrentIndex(current_index) @Slot(bool) def add_relationships(self, checked=True): object_names = [[item.text(0) for item in wg.selectedItems()] for wg in self.splitter_widgets()] candidate = list(product(*object_names)) existing = self.new_items_model._main_data + self.existing_items_model._main_data to_add = list(set(candidate) - set(existing)) count = len(to_add) self.new_items_model.insertRows(0, count) self.new_items_model._main_data[0:count] = to_add self.model.refresh() @Slot(int) def reset_model(self, index): """Setup model according to current relationship_class selected in combobox. """ self.class_name, self.object_class_name_list = self.relationship_class_keys[ index] object_class_name_list = self.object_class_name_list.split(",") self.model.set_horizontal_header_labels(object_class_name_list) self.existing_items_model.set_horizontal_header_labels( object_class_name_list) self.new_items_model.set_horizontal_header_labels( object_class_name_list) self.relationship_ids.clear() for db_map in self.db_maps: relationship_classes = self.db_map_rel_cls_lookup[db_map] rel_cls = relationship_classes.get( (self.class_name, self.object_class_name_list), None) if rel_cls is None: continue for relationship in self.db_mngr.get_items_by_field( db_map, "relationship", "class_id", rel_cls["id"]): key = tuple(relationship["object_name_list"].split(",")) self.relationship_ids[key] = relationship["id"] existing_items = list(self.relationship_ids) self.existing_items_model.reset_model(existing_items) self.model.refresh() self.model.modelReset.emit() for wg in self.splitter_widgets(): wg.deleteLater() for name in object_class_name_list: tree_widget = QTreeWidget(self) tree_widget.setSelectionMode(QAbstractItemView.ExtendedSelection) tree_widget.setColumnCount(1) tree_widget.setIndentation(0) header_item = QTreeWidgetItem([name]) header_item.setTextAlignment(0, Qt.AlignHCenter) tree_widget.setHeaderItem(header_item) objects = self.db_mngr.get_items_by_field(self.db_map, "object", "class_name", name) items = [QTreeWidgetItem([obj["name"]]) for obj in objects] tree_widget.addTopLevelItems(items) tree_widget.resizeColumnToContents(0) self.splitter.addWidget(tree_widget) sizes = [wg.columnWidth(0) for wg in self.splitter_widgets()] self.splitter.setSizes(sizes) for widget in self.hidable_widgets: widget.show() def resize_window_to_columns(self, height=None): table_view_width = (self.table_view.frameWidth() * 2 + self.table_view.verticalHeader().width() + self.table_view.horizontalHeader().length()) self.table_view.setMinimumWidth(table_view_width) self.table_view.setMinimumHeight( self.table_view.verticalHeader().defaultSectionSize() * 16) margins = self.layout().contentsMargins() if height is None: height = self.sizeHint().height() self.resize( margins.left() + margins.right() + table_view_width + self.add_button.width() + self.splitter.width(), height, ) @Slot() def accept(self): """Collect info from dialog and try to add items.""" keys_to_remove = set(self.relationship_ids) - set( self.existing_items_model._main_data) to_remove = [self.relationship_ids[key] for key in keys_to_remove] self.db_mngr.remove_items({self.db_map: {"relationship": to_remove}}) to_add = [[self.class_name, object_name_list] for object_name_list in self.new_items_model._main_data] self.db_mngr.import_data({self.db_map: { "relationships": to_add }}, command_text="Add relationships") super().accept()
class FE14ChapterSpawnsTab(QWidget): def __init__(self): super().__init__() self.chapter_data = None self.dispos_model = None self.dispos = None self.terrain = None self.tiles_model = None self.terrain_mode = False self.initialized_selection_signal = False self.selected_faction = None left_panel_container = QWidget() left_panel_layout = QVBoxLayout() self.toggle_editor_type_checkbox = QCheckBox() self.toggle_editor_type_checkbox.setText("Spawns/Terrain") self.toggle_editor_type_checkbox.setChecked(True) self.toggle_editor_type_checkbox.stateChanged.connect( self._on_mode_change_requested) self.toggle_coordinate_type_checkbox = QCheckBox() self.toggle_coordinate_type_checkbox.setText( "Coordinate (1)/Coordinate (2)") self.toggle_coordinate_type_checkbox.setChecked(True) self.toggle_coordinate_type_checkbox.stateChanged.connect( self._on_coordinate_change_requested) self.tree_view = QTreeView() left_panel_layout.addWidget(self.toggle_editor_type_checkbox) left_panel_layout.addWidget(self.toggle_coordinate_type_checkbox) left_panel_layout.addWidget(self.tree_view) left_panel_container.setLayout(left_panel_layout) self.grid = FE14MapGrid() self.dispos_scroll, self.dispos_form = PropertyForm.create_with_scroll( dispo.SPAWN_TEMPLATE) self.terrain_form, self.terrain_persistent_editors, self.tile_form = _create_terrain_form( ) self.organizer = QSplitter() self.organizer.addWidget(left_panel_container) self.organizer.addWidget(self.grid) self.organizer.addWidget(self.dispos_scroll) main_layout = QVBoxLayout(self) main_layout.addWidget(self.organizer) self.setLayout(main_layout) self.add_faction_shortcut = QShortcut(QKeySequence("Ctrl+F"), self) self.add_item_shortcut = QShortcut(QKeySequence("Ctrl+N"), self) self.grid.focused_spawn_changed.connect(self._on_focused_spawn_changed) self.add_faction_shortcut.activated.connect( self._on_add_faction_requested) self.add_item_shortcut.activated.connect(self._on_add_item_requested) self.dispos_form.editors["PID"].editingFinished.connect( self._on_pid_field_changed) self.dispos_form.editors["Team"].currentIndexChanged.connect( self._on_team_field_changed) self.dispos_form.editors["Coordinate (1)"].textChanged.connect( self._on_coordinate_1_field_changed) self.dispos_form.editors["Coordinate (2)"].textChanged.connect( self._on_coordinate_2_field_changed) def update_chapter_data(self, chapter_data): self.chapter_data = chapter_data if self.chapter_data and self.chapter_data.dispos and self.chapter_data.terrain: self.setEnabled(True) self.dispos = self.chapter_data.dispos self.dispos_model = DisposModel(self.dispos) self.terrain = self.chapter_data.terrain self.tiles_model = TilesModel(self.terrain.tiles) if self.terrain_mode: self.tree_view.setModel(self.tiles_model) else: self.tree_view.setModel(self.dispos_model) self.tree_view.selectionModel().currentChanged.connect( self._on_tree_selection_changed) self.grid.set_chapter_data(chapter_data) self._update_forms(self.terrain, None, None) else: self.setEnabled(False) self.grid.clear() def _update_forms(self, terrain_target, tile_target, dispos_target): self.dispos_form.update_target(dispos_target) self.tile_form.update_target(tile_target) for editor in self.terrain_persistent_editors: editor.update_target(terrain_target) def _on_focused_spawn_changed(self, spawn): self.dispos_form.update_target(spawn) def _on_mode_change_requested(self, state): self.organizer.widget(2).setParent(None) self.terrain_mode = state != QtGui.Qt.Checked if not self.terrain_mode: self.grid.transition_to_dispos_mode() self.organizer.addWidget(self.dispos_scroll) self.tree_view.setModel(self.dispos_model) else: self.grid.transition_to_terrain_mode() self.organizer.addWidget(self.terrain_form) self.tree_view.setModel(self.tiles_model) self.tree_view.selectionModel().currentChanged.connect( self._on_tree_selection_changed) def _on_coordinate_change_requested(self, _state): self.grid.toggle_coordinate_key() def _on_tree_selection_changed(self, index: QModelIndex, _previous): data = index.data(QtCore.Qt.UserRole) if self.terrain_mode: self.grid.selected_tile = data self.tile_form.update_target(data) else: if type(data) == PropertyContainer: self.grid.select_spawn(data) self.selected_faction = None else: self.selected_faction = data def _on_add_faction_requested(self): if self.dispos_model: (faction_name, ok) = QInputDialog.getText(self, "Enter a faction name.", "Name:") if ok: self.dispos_model.add_faction(faction_name) def _on_add_item_requested(self): if not self.chapter_data or (not self.terrain_mode and not self.selected_faction): return if self.terrain_mode: self.tiles_model.add_tile() else: self.dispos_model.add_spawn_to_faction(self.selected_faction) spawn = self.selected_faction.spawns[-1] self.grid.add_spawn_to_map(spawn) def _on_coordinate_1_field_changed(self, _text): new_position = self._parse_coordinate_field( self.dispos_form.editors["Coordinate (1)"]) self.grid.update_focused_spawn_position(new_position, "Coordinate (1)") def _on_coordinate_2_field_changed(self, _text): new_position = self._parse_coordinate_field( self.dispos_form.editors["Coordinate (2)"]) self.grid.update_focused_spawn_position(new_position, "Coordinate (2)") @staticmethod def _parse_coordinate_field(field): split_text = field.displayText().split() result = [] for entry in split_text: result.append(int(entry, 16)) return result def _on_team_field_changed(self): if self.chapter_data and self.chapter_data.dispos: self.grid.update_team_for_focused_spawn() def _on_pid_field_changed(self): self.dispos_model.refresh_spawn(self.grid.selected_spawns[-1])