Exemplo n.º 1
0
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()
Exemplo n.º 3
0
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])