예제 #1
0
class SignaltoSignalTest(UsesQCoreApplication):

    def setUp(self):
        UsesQCoreApplication.setUp(self)
        self.receiver = ExtQObject()
        self.timeline = QTimeLine(100)

    def tearDown(self):
        del self.timeline
        del self.receiver
        UsesQCoreApplication.tearDown(self)

    def testSignaltoSignal(self):
        self.timeline.setUpdateInterval(10)

        self.timeline.finished.connect(self.app.quit)

        self.timeline.valueChanged.connect(self.receiver.signalbetween)
        self.receiver.signalbetween.connect(self.receiver.foo)

        self.timeline.start()

        self.app.exec_()

        self.assertTrue(self.receiver.counter > 1)
예제 #2
0
class SignaltoSignalTest(UsesQCoreApplication):

    def setUp(self):
        UsesQCoreApplication.setUp(self)
        self.receiver = ExtQObject()
        self.timeline = QTimeLine(100)

    def tearDown(self):
        del self.timeline
        del self.receiver
        UsesQCoreApplication.tearDown(self)

    def testSignaltoSignal(self):
        self.timeline.setUpdateInterval(10)

        self.timeline.finished.connect(self.app.quit)

        self.timeline.valueChanged.connect(self.receiver.signalbetween)
        self.receiver.signalbetween.connect(self.receiver.foo)

        self.timeline.start()

        self.app.exec_()

        self.assert_(self.receiver.counter > 1)
예제 #3
0
class QViewPortMover:
    def __init__(self,
                 disasm_graph: 'QDisassemblyGraph',
                 x: int,
                 y: int,
                 target_x: int,
                 target_y: int,
                 interval: int = 700,
                 max_frame: int = 100):
        self.disasm_graph = disasm_graph
        self.target_x = target_x
        self.target_y = target_y

        self.initial_x = x
        self.initial_y = y
        self.x_step = (self.target_x - self.initial_x) / max_frame
        self.y_step = (self.target_y - self.initial_y) / max_frame

        self._move_timeline = QTimeLine(interval)
        self._move_timeline.setFrameRange(0, max_frame)
        self._move_timeline.setUpdateInterval(10)

    def start(self):
        self._move_timeline.frameChanged.connect(self._set_pos)
        self._move_timeline.start()

    def _set_pos(self, step):
        self.disasm_graph.centerOn(self.initial_x + self.x_step * step,
                                   self.initial_y + self.y_step * step)
예제 #4
0
class NativeSignalsTest(UsesQCoreApplication):
    def setUp(self):
        UsesQCoreApplication.setUp(self)
        self.called = False
        self.timeline = QTimeLine(100)

    def tearDown(self):
        del self.called
        del self.timeline
        UsesQCoreApplication.tearDown(self)

    def testSignalWithIntArgument(self):
        def valueChangedSlot(value):
            self.called = True
            self.assertEqual(type(value), float)
            self.app.quit()

        self.timeline.valueChanged.connect(valueChangedSlot)
        self.timeline.start()

        self.app.exec_()
        self.assertTrue(self.called)

    def testSignalWithoutArguments(self):
        def finishedSlot():
            self.called = True
            self.app.quit()

        self.timeline.finished.connect(finishedSlot)
        self.timeline.start()

        self.app.exec_()
        self.assertTrue(self.called)
예제 #5
0
class FaderWidget(QWidget):
    def __init__(self, old_widget, new_widget):
        QWidget.__init__(self, new_widget)

        self.pixmap_opacity = 1.0
        self.old_pixmap = QPixmap(new_widget.size())
        old_widget.render(self.old_pixmap)

        self.timeline = QTimeLine()
        self.timeline.valueChanged.connect(self.animate)
        self.timeline.finished.connect(self.close)
        self.timeline.setDuration(230)
        self.timeline.start()

        self.resize(new_widget.size())
        self.show()

    def paintEvent(self, event):
        painter = QPainter()
        painter.begin(self)
        painter.setOpacity(self.pixmap_opacity)
        painter.drawPixmap(0, 0, self.old_pixmap)
        painter.end()

    def animate(self, value):
        self.pixmap_opacity = 1.0 - value
        self.repaint()
예제 #6
0
class CombinerIcon(ProjectItemIcon):
    _SHAKE_FACTOR = 0.05

    def __init__(self, toolbox, x, y, project_item, icon):
        """View icon for the Design View.

        Args:
            toolbox (ToolBoxUI): QMainWindow instance
            x (float): Icon x coordinate
            y (float): Icon y coordinate
            project_item (ProjectItem): Item
            icon (str): icon resource path
        """
        super().__init__(toolbox,
                         x,
                         y,
                         project_item,
                         icon,
                         icon_color=QColor("#990000"),
                         background_color=QColor("#ffcccc"))
        self.time_line = QTimeLine()
        self.time_line.setLoopCount(0)  # loop forever
        self.time_line.setFrameRange(0, 10)
        self.time_line.setDirection(QTimeLine.Backward)
        self.time_line.valueChanged.connect(
            self._handle_time_line_value_changed)
        self.time_line.stateChanged.connect(
            self._handle_time_line_state_changed)
        self._svg_item_pos = self.svg_item.pos()

    @Slot(float)
    def _handle_time_line_value_changed(self, value):
        rect = self.svg_item.sceneBoundingRect()
        width = rect.width()
        height = rect.height()
        x = random.uniform(-self._SHAKE_FACTOR, self._SHAKE_FACTOR) * width
        y = random.uniform(-self._SHAKE_FACTOR, self._SHAKE_FACTOR) * height
        self.svg_item.setPos(self._svg_item_pos + QPointF(x, y))

    @Slot("QTimeLine::State")
    def _handle_time_line_state_changed(self, new_state):
        if new_state == QTimeLine.NotRunning:
            self.svg_item.setPos(self._svg_item_pos)

    def start_animation(self):
        """Start the animation that plays when the Combiner associated to this GraphicsItem is running.
        """
        if self.time_line.state() == QTimeLine.Running:
            return
        self.time_line.start()

    def stop_animation(self):
        """Stop animation"""
        if self.time_line.state() != QTimeLine.Running:
            return
        self.time_line.stop()
예제 #7
0
class ToolIcon(ProjectItemIcon):
    def __init__(self, toolbox, x, y, project_item, icon):
        """Tool icon for the Design View.

        Args:
            toolbox (ToolBoxUI): QMainWindow instance
            x (float): Icon x coordinate
            y (float): Icon y coordinate
            project_item (ProjectItem): Item
            icon (str): icon resource path
        """
        super().__init__(
            toolbox, x, y, project_item, icon, icon_color=QColor("red"), background_color=QColor("#ffe6e6")
        )
        self.time_line = QTimeLine()
        self.time_line.setLoopCount(0)  # loop forever
        self.time_line.setFrameRange(0, 10)
        self.time_line.setDuration(1200)
        self.time_line.setDirection(QTimeLine.Backward)
        self.time_line.valueChanged.connect(self._handle_time_line_value_changed)
        self.time_line.stateChanged.connect(self._handle_time_line_state_changed)
        self._svg_item_pos = self.svg_item.pos()
        rect = self.svg_item.sceneBoundingRect()
        self._anim_transformation_origin_point_y = -0.75 * rect.height()
        self._anim_delta_x_factor = 0.5 * rect.width()

    @Slot(float)
    def _handle_time_line_value_changed(self, value):
        angle = value * 45.0
        self.svg_item.setRotation(angle)
        delta_y = 0.5 * self.svg_item.sceneBoundingRect().height()
        delta = QPointF(self._anim_delta_x_factor * value, delta_y)
        self.svg_item.setPos(self._svg_item_pos + delta)

    @Slot("QTimeLine::State")
    def _handle_time_line_state_changed(self, new_state):
        if new_state == QTimeLine.Running:
            self.svg_item.setTransformOriginPoint(0, self._anim_transformation_origin_point_y)
        elif new_state == QTimeLine.NotRunning:
            self.svg_item.setTransformOriginPoint(0, 0)
            self.svg_item.setPos(self._svg_item_pos)
            self.svg_item.setRotation(0)

    def start_animation(self):
        """Starts the item execution animation.
        """
        if self.time_line.state() == QTimeLine.Running:
            return
        self.time_line.start()

    def stop_animation(self):
        """Stop animation"""
        if self.time_line.state() != QTimeLine.Running:
            return
        self.time_line.stop()
예제 #8
0
class NativeSignalsTest(UsesQCoreApplication):

    def setUp(self):
        UsesQCoreApplication.setUp(self)
        self.called = False
        self.timeline = QTimeLine(100)

    def tearDown(self):
        del self.called
        del self.timeline
        UsesQCoreApplication.tearDown(self)

    def testSignalWithIntArgument(self):

        def valueChangedSlot(value):
            self.called = True
            self.assertEqual(type(value), float)
            self.app.quit()

        self.timeline.valueChanged.connect(valueChangedSlot)
        self.timeline.start()

        self.app.exec_()
        self.assert_(self.called)

    def testSignalWithoutArguments(self):

        def finishedSlot():
            self.called = True
            self.app.quit()

        self.timeline.finished.connect(finishedSlot)
        self.timeline.start()

        self.app.exec_()
        self.assert_(self.called)
class EntityQGraphicsView(CustomQGraphicsView):
    """QGraphicsView for the Entity Graph View."""

    graph_selection_changed = Signal(object)

    def __init__(self, parent):
        """

        Args:
            parent (QWidget): Graph View Form's (QMainWindow) central widget (self.centralwidget)
        """
        super().__init__(
            parent=parent)  # Parent is passed to QWidget's constructor
        self._spine_db_editor = None
        self._menu = QMenu(self)
        self.pos_x_parameter = "x"
        self.pos_y_parameter = "y"
        self.selected_items = list()
        self.removed_items = list()
        self.hidden_items = list()
        self.prunned_entity_ids = dict()
        self.heat_map_items = list()
        self._point_value_tuples_per_parameter_name = dict(
        )  # Used in the heat map menu
        self._hovered_obj_item = None
        self.relationship_class = None
        self.cross_hairs_items = []
        self.auto_expand_objects = None
        self._auto_expand_objects_action = None
        self._add_objects_action = None
        self._save_pos_action = None
        self._clear_pos_action = None
        self._hide_action = None
        self._show_hidden_action = None
        self._prune_entities_action = None
        self._prune_classes_action = None
        self._restore_all_pruned_action = None
        self._rebuild_action = None
        self._export_as_pdf_action = None
        self._zoom_action = None
        self._rotate_action = None
        self._arc_length_action = None
        self._restore_pruned_menu = None
        self._parameter_heat_map_menu = None
        self._previous_mouse_pos = None
        self._context_menu_pos = None

    @property
    def entity_items(self):
        return [
            x for x in self.scene().items()
            if isinstance(x, EntityItem) and x not in self.removed_items
        ]

    def setScene(self, scene):
        super().setScene(scene)
        scene.selectionChanged.connect(self._handle_scene_selection_changed)

    @Slot()
    def _handle_scene_selection_changed(self):
        """Filters parameters by selected objects in the graph."""
        if self.scene() is None:
            return
        selected_items = self.scene().selectedItems()
        selected_objs = [
            x for x in selected_items if isinstance(x, ObjectItem)
        ]
        selected_rels = [
            x for x in selected_items if isinstance(x, RelationshipItem)
        ]
        self.selected_items = selected_objs + selected_rels
        self.graph_selection_changed.emit({
            "object": selected_objs,
            "relationship": selected_rels
        })

    def connect_spine_db_editor(self, spine_db_editor):
        self._spine_db_editor = spine_db_editor
        self.populate_context_menu()

    def populate_context_menu(self):
        self._auto_expand_objects_action = self._menu.addAction(
            "Auto-expand objects")
        self._auto_expand_objects_action.setCheckable(True)
        self.auto_expand_objects = (self._spine_db_editor.qsettings.value(
            "appSettings/autoExpandObjects", defaultValue="false") == "true")
        self._auto_expand_objects_action.setChecked(self.auto_expand_objects)
        self._auto_expand_objects_action.toggled.connect(
            self.set_auto_expand_objects)
        self._menu.addSeparator()
        self._add_objects_action = self._menu.addAction(
            "Add objects", self.add_objects_at_position)
        self._menu.addSeparator()
        self._save_pos_action = self._menu.addAction("Save positions",
                                                     self.save_positions)
        self._clear_pos_action = self._menu.addAction(
            "Clear saved positions", self.clear_saved_positions)
        self._menu.addSeparator()
        self._hide_action = self._menu.addAction("Hide",
                                                 self.hide_selected_items)
        self._show_hidden_action = self._menu.addAction(
            "Show hidden", self.show_hidden_items)
        self._menu.addSeparator()
        self._prune_entities_action = self._menu.addAction(
            "Prune entities", self.prune_selected_entities)
        self._prune_classes_action = self._menu.addAction(
            "Prune classes", self.prune_selected_classes)
        self._restore_pruned_menu = self._menu.addMenu("Restore")
        self._restore_pruned_menu.triggered.connect(self.restore_pruned_items)
        self._restore_all_pruned_action = self._menu.addAction(
            "Restore all", self.restore_all_pruned_items)
        self._menu.addSeparator()
        # FIXME: The heap map doesn't seem to be working nicely
        # self._parameter_heat_map_menu = self._menu.addMenu("Add heat map")
        # self._parameter_heat_map_menu.triggered.connect(self.add_heat_map)
        self._menu.addSeparator()
        self._rebuild_action = self._menu.addAction(
            "Rebuild", self._spine_db_editor.build_graph)
        self._export_as_pdf_action = self._menu.addAction(
            "Export as PDF", self.export_as_pdf)
        self._menu.addSeparator()
        self._zoom_action = ToolBarWidgetAction("Zoom",
                                                self._menu,
                                                compact=True)
        self._zoom_action.tool_bar.addAction(
            "-", self.zoom_out).setToolTip("Zoom out")
        self._zoom_action.tool_bar.addAction(
            "Reset", self.reset_zoom).setToolTip("Reset zoom")
        self._zoom_action.tool_bar.addAction(
            "+", self.zoom_in).setToolTip("Zoom in")
        self._rotate_action = ToolBarWidgetAction("Rotate",
                                                  self._menu,
                                                  compact=True)
        self._rotate_action.tool_bar.addAction(
            "\u2b6f",
            self.rotate_anticlockwise).setToolTip("Rotate counter-clockwise")
        self._rotate_action.tool_bar.addAction(
            "\u2b6e", self.rotate_clockwise).setToolTip("Rotate clockwise")
        self._arc_length_action = ToolBarWidgetAction("Arc length",
                                                      self._menu,
                                                      compact=True)
        self._arc_length_action.tool_bar.addAction(
            QIcon(CharIconEngine("\uf422")), "",
            self.decrease_arc_length).setToolTip("Decrease arc length")
        self._arc_length_action.tool_bar.addAction(
            QIcon(CharIconEngine("\uf424")), "",
            self.increase_arc_length).setToolTip("Increase arc length")
        self._menu.addSeparator()
        self._menu.addAction(self._zoom_action)
        self._menu.addAction(self._arc_length_action)
        self._menu.addAction(self._rotate_action)
        self._menu.aboutToShow.connect(self._update_actions_visibility)

    def increase_arc_length(self):
        for item in self.entity_items:
            item.setPos(1.1 * item.pos())
            item.update_arcs_line()

    def decrease_arc_length(self):
        for item in self.entity_items:
            item.setPos(item.pos() / 1.1)
            item.update_arcs_line()

    @Slot()
    def _update_actions_visibility(self):
        """Enables or disables actions according to current selection in the graph."""
        self._save_pos_action.setEnabled(bool(self.selected_items))
        self._clear_pos_action.setEnabled(bool(self.selected_items))
        self._hide_action.setEnabled(bool(self.selected_items))
        self._show_hidden_action.setEnabled(bool(self.hidden_items))
        self._prune_entities_action.setEnabled(bool(self.selected_items))
        self._prune_classes_action.setEnabled(bool(self.selected_items))
        self._restore_pruned_menu.setEnabled(
            any(self.prunned_entity_ids.values()))
        self._restore_all_pruned_action.setEnabled(
            any(self.prunned_entity_ids.values()))
        self._prune_entities_action.setText(
            f"Prune {self._get_selected_entity_names()}")
        self._prune_classes_action.setText(
            f"Prune {self._get_selected_class_names()}")
        has_graph = bool(self.items())
        self._rebuild_action.setEnabled(has_graph)
        self._zoom_action.setEnabled(has_graph)
        self._rotate_action.setEnabled(has_graph)
        self._export_as_pdf_action.setEnabled(has_graph)
        # FIXME: The heap map doesn't seem to be working nicely
        # self._parameter_heat_map_menu.setEnabled(has_graph)
        # if has_graph:
        #    self._populate_add_heat_map_menu()

    def make_items_menu(self):
        menu = QMenu(self)
        menu.addAction(self._save_pos_action)
        menu.addAction(self._clear_pos_action)
        menu.addSeparator()
        menu.addAction(self._hide_action)
        menu.addAction(self._prune_entities_action)
        menu.addAction(self._prune_classes_action)
        menu.addSeparator()
        menu.addAction("Edit", self.edit_selected)
        menu.addAction("Remove", self.remove_selected)
        menu.aboutToShow.connect(self._update_actions_visibility)
        return menu

    @Slot(bool)
    def set_auto_expand_objects(self, checked=False):
        self.auto_expand_objects = checked
        self._auto_expand_objects_action.setChecked(checked)
        self._spine_db_editor.build_graph()

    @Slot(bool)
    def add_objects_at_position(self, checked=False):
        self._spine_db_editor.add_objects_at_position(self._context_menu_pos)

    @Slot(bool)
    def edit_selected(self, _=False):
        """Edits selected items."""
        obj_items = [
            item for item in self.selected_items
            if isinstance(item, ObjectItem)
        ]
        rel_items = [
            item for item in self.selected_items
            if isinstance(item, RelationshipItem)
        ]
        self._spine_db_editor.show_edit_objects_form(obj_items)
        self._spine_db_editor.show_edit_relationships_form(rel_items)

    @Slot(bool)
    def remove_selected(self, _=False):
        """Removes selected items."""
        if not self.selected_items:
            return
        db_map_typed_data = {}
        for item in self.selected_items:
            db_map, entity_id = item.db_map_entity_id
            db_map_typed_data.setdefault(db_map, {}).setdefault(
                item.entity_type, set()).add(entity_id)
        self._spine_db_editor.db_mngr.remove_items(db_map_typed_data)

    @Slot(bool)
    def hide_selected_items(self, checked=False):
        """Hides selected items."""
        self.hidden_items.extend(self.selected_items)
        for item in self.selected_items:
            item.set_all_visible(False)

    @Slot(bool)
    def show_hidden_items(self, checked=False):
        """Shows hidden items."""
        if not self.scene():
            return
        for item in self.hidden_items:
            item.set_all_visible(True)
        self.hidden_items.clear()

    def _get_selected_entity_names(self):
        if not self.selected_items:
            return ""
        names = "'" + self.selected_items[0].entity_name + "'"
        if len(self.selected_items) > 1:
            names += f" and {len(self.selected_items) - 1} other entities"
        return names

    def _get_selected_class_names(self):
        if not self.selected_items:
            return ""
        entity_class_names = list(
            set(item.entity_class_name for item in self.selected_items))
        names = "'" + entity_class_names[0] + "'"
        if len(entity_class_names) > 1:
            names += f" and {len(entity_class_names) - 1} other classes"
        return names

    @Slot(bool)
    def prune_selected_entities(self, checked=False):
        """Prunes selected items."""
        entity_ids = {x.db_map_entity_id for x in self.selected_items}
        key = self._get_selected_entity_names()
        self.prunned_entity_ids[key] = entity_ids
        self._restore_pruned_menu.addAction(key)
        self._spine_db_editor.build_graph()

    @Slot(bool)
    def prune_selected_classes(self, checked=False):
        """Prunes selected items."""
        db_map_class_ids = {}
        for x in self.selected_items:
            db_map_class_ids.setdefault(x.db_map, set()).add(x.entity_class_id)
        entity_ids = {
            (db_map, x["id"])
            for db_map, class_ids in db_map_class_ids.items()
            for x in self._spine_db_editor.db_mngr.get_items(db_map, "object")
            if x["class_id"] in class_ids
        }
        entity_ids |= {(db_map, x["id"])
                       for db_map, class_ids in db_map_class_ids.items()
                       for x in self._spine_db_editor.db_mngr.get_items(
                           db_map, "relationship")
                       if x["class_id"] in class_ids}
        key = self._get_selected_class_names()
        self.prunned_entity_ids[key] = entity_ids
        self._restore_pruned_menu.addAction(key)
        self._spine_db_editor.build_graph()

    @Slot(bool)
    def restore_all_pruned_items(self, checked=False):
        """Reinstates all pruned items."""
        self.prunned_entity_ids.clear()
        self._spine_db_editor.build_graph()

    @Slot("QAction")
    def restore_pruned_items(self, action):
        """Reinstates last pruned items."""
        key = action.text()
        if self.prunned_entity_ids.pop(key, None) is not None:
            action = next(
                iter(a for a in self._restore_pruned_menu.actions()
                     if a.text() == key))
            self._restore_pruned_menu.removeAction(action)
            self._spine_db_editor.build_graph()

    @Slot(bool)
    def select_position_parameters(self, checked=False):
        dialog = SelectPositionParametersDialog(self._spine_db_editor)
        dialog.show()
        dialog.selection_made.connect(self._set_position_parameters)

    @Slot(str, str)
    def _set_position_parameters(self, parameter_pos_x, parameter_pos_y):
        self.pos_x_parameter = parameter_pos_x
        self.pos_y_parameter = parameter_pos_y

    @Slot(bool)
    def save_positions(self, checked=False):
        if not self.pos_x_parameter or not self.pos_y_parameter:
            msg = "You haven't selected the position parameters. Please go to Graph -> Select position parameters"
            self._spine_db_editor.msg.emit(msg)
            return
        obj_items = [
            item for item in self.selected_items
            if isinstance(item, ObjectItem)
        ]
        rel_items = [
            item for item in self.selected_items
            if isinstance(item, RelationshipItem)
        ]
        db_map_class_obj_items = {}
        db_map_class_rel_items = {}
        for item in obj_items:
            db_map_class_obj_items.setdefault(item.db_map, {}).setdefault(
                item.entity_class_name, []).append(item)
        for item in rel_items:
            db_map_class_rel_items.setdefault(item.db_map, {}).setdefault(
                item.entity_class_name, []).append(item)
        db_map_data = {}
        for db_map, class_obj_items in db_map_class_obj_items.items():
            data = db_map_data.setdefault(db_map, {})
            for class_name, obj_items in class_obj_items.items():
                data["object_parameters"] = [
                    (class_name, self.pos_x_parameter),
                    (class_name, self.pos_y_parameter)
                ]
                data["object_parameter_values"] = [
                    (class_name, item.entity_name, self.pos_x_parameter,
                     item.pos().x()) for item in obj_items
                ] + [(class_name, item.entity_name, self.pos_y_parameter,
                      item.pos().y()) for item in obj_items]
        for db_map, class_rel_items in db_map_class_rel_items.items():
            data = db_map_data.setdefault(db_map, {})
            for class_name, rel_items in class_rel_items.items():
                data["relationship_parameters"] = [
                    (class_name, self.pos_x_parameter),
                    (class_name, self.pos_y_parameter),
                ]
                data["relationship_parameter_values"] = [
                    (class_name, item.object_name_list.split(","),
                     self.pos_x_parameter, item.pos().x())
                    for item in rel_items
                ] + [(class_name, item.object_name_list.split(","),
                      self.pos_y_parameter, item.pos().y())
                     for item in rel_items]
        self._spine_db_editor.db_mngr.import_data(db_map_data)

    @Slot(bool)
    def clear_saved_positions(self, checked=False):
        if not self.selected_items:
            return
        db_map_ids = {}
        for item in self.selected_items:
            db_map_ids.setdefault(item.db_map, set()).add(item.entity_id)
        db_map_typed_data = {}
        for db_map, ids in db_map_ids.items():
            db_map_typed_data[db_map] = {
                "parameter_value":
                set(pv["id"] for parameter_name in (self.pos_x_parameter,
                                                    self.pos_y_parameter)
                    for pv in self._spine_db_editor.db_mngr.get_items_by_field(
                        db_map, "parameter_value", "parameter_name",
                        parameter_name) if pv["entity_id"] in ids)
            }
        self._spine_db_editor.db_mngr.remove_items(db_map_typed_data)
        self._spine_db_editor.build_graph()

    @Slot(bool)
    def export_as_pdf(self, checked=False):
        file_path = self._spine_db_editor.get_pdf_file_path()
        if not file_path:
            return
        source = self._get_viewport_scene_rect()
        current_zoom_factor = self.zoom_factor
        self._zoom(1.0 / current_zoom_factor)
        self.scene().clearSelection()
        printer = QPrinter()
        printer.setPaperSize(source.size(), QPrinter.Point)
        printer.setOutputFileName(file_path)
        painter = QPainter(printer)
        self.scene().render(painter, QRectF(), source)
        painter.end()
        self._zoom(current_zoom_factor)
        self._spine_db_editor.file_exported.emit(file_path)

    def _populate_add_heat_map_menu(self):
        """Populates the menu 'Add heat map' with parameters for currently shown items in the graph."""
        db_map_class_ids = {}
        for item in self.entity_items:
            db_map_class_ids.setdefault(item.db_map,
                                        set()).add(item.entity_class_id)
        db_map_parameters = self._spine_db_editor.db_mngr.find_cascading_parameter_data(
            db_map_class_ids, "parameter_definition")
        db_map_class_parameters = {}
        parameter_value_ids = {}
        for db_map, parameters in db_map_parameters.items():
            for p in parameters:
                db_map_class_parameters.setdefault(
                    (db_map, p["entity_class_id"]), []).append(p)
            parameter_value_ids = {
                (db_map, pv["parameter_id"], pv["entity_id"]): pv["id"]
                for pv in self._spine_db_editor.db_mngr.
                find_cascading_parameter_values_by_definition(
                    {db_map: {x["id"]
                              for x in parameters}})[db_map]
            }
        self._point_value_tuples_per_parameter_name.clear()
        for item in self.entity_items:
            for parameter in db_map_class_parameters.get(
                (item.db_map, item.entity_class_id), ()):
                pv_id = parameter_value_ids.get(
                    (item.db_map, parameter["id"], item.entity_id))
                try:
                    value = float(
                        self._spine_db_editor.db_mngr.get_value(
                            item.db_map, "parameter_value", pv_id))
                    pos = item.pos()
                    self._point_value_tuples_per_parameter_name.setdefault(
                        parameter["parameter_name"], []).append(
                            (pos.x(), -pos.y(), value))
                except (TypeError, ValueError):
                    pass
        self._parameter_heat_map_menu.clear()
        for name, point_value_tuples in self._point_value_tuples_per_parameter_name.items(
        ):
            if len(point_value_tuples) > 1:
                self._parameter_heat_map_menu.addAction(name)
        self._parameter_heat_map_menu.setDisabled(
            self._parameter_heat_map_menu.isEmpty())

    @Slot("QAction")
    def add_heat_map(self, action):
        """Adds heat map for the parameter in the action text.
        """
        self._clean_up_heat_map_items()
        point_value_tuples = self._point_value_tuples_per_parameter_name[
            action.text()]
        x, y, values = zip(*point_value_tuples)
        heat_map, xv, yv, min_x, min_y, max_x, max_y = make_heat_map(
            x, y, values)
        heat_map_item, hm_figure = make_figure_graphics_item(self.scene(),
                                                             z=-3,
                                                             static=True)
        colorbar_item, cb_figure = make_figure_graphics_item(self.scene(),
                                                             z=3,
                                                             static=False)
        colormesh = hm_figure.gca().pcolormesh(xv, yv, heat_map)
        cb_figure.colorbar(colormesh, fraction=1)
        cb_figure.gca().set_visible(False)
        width = max_x - min_x
        height = max_y - min_y
        heat_map_item.widget().setGeometry(min_x, min_y, width, height)
        extent = self._spine_db_editor.VERTEX_EXTENT
        colorbar_item.widget().setGeometry(max_x + extent, min_y, 2 * extent,
                                           height)
        self.heat_map_items += [heat_map_item, colorbar_item]

    def _clean_up_heat_map_items(self):
        for item in self.heat_map_items:
            item.hide()
            self.scene().removeItem(item)
        self.heat_map_items.clear()

    def set_cross_hairs_items(self, relationship_class, cross_hairs_items):
        """Sets 'cross_hairs' items for relationship creation.

        Args:
            relationship_class (dict)
            cross_hairs_items (list(QGraphicsItems))
        """
        self.relationship_class = relationship_class
        self.cross_hairs_items = cross_hairs_items
        for item in cross_hairs_items:
            self.scene().addItem(item)
            item.apply_zoom(self.zoom_factor)
        cursor_pos = self.mapFromGlobal(QCursor.pos())
        self._update_cross_hairs_pos(cursor_pos)
        self.viewport().setCursor(Qt.BlankCursor)

    def clear_cross_hairs_items(self):
        self.relationship_class = None
        for item in self.cross_hairs_items:
            item.hide()
            item.scene().removeItem(item)
        self.cross_hairs_items.clear()
        self.viewport().unsetCursor()

    def _cross_hairs_has_valid_taget(self):
        return (self._hovered_obj_item.db_map
                == self.cross_hairs_items[0].db_map
                and self._hovered_obj_item.entity_class_id
                in self.relationship_class["object_class_ids_to_go"])

    def mousePressEvent(self, event):
        """Handles relationship creation if one it's in process."""
        if not self.cross_hairs_items:
            super().mousePressEvent(event)
            return
        if event.buttons() & Qt.RightButton or not self._hovered_obj_item:
            self.clear_cross_hairs_items()
            return
        if self._cross_hairs_has_valid_taget():
            self.relationship_class["object_class_ids_to_go"].remove(
                self._hovered_obj_item.entity_class_id)
            if self.relationship_class["object_class_ids_to_go"]:
                # Add hovered as member and keep going, we're not done yet
                ch_rel_item = self.cross_hairs_items[1]
                ch_arc_item = CrossHairsArcItem(
                    ch_rel_item, self._hovered_obj_item,
                    self._spine_db_editor._ARC_WIDTH)
                ch_rel_item.refresh_icon()
                self.scene().addItem(ch_arc_item)
                ch_arc_item.apply_zoom(self.zoom_factor)
                self.cross_hairs_items.append(ch_arc_item)
                return
            # Here we're done, add the relationships between the hovered and the members
            ch_item, _, *ch_arc_items = self.cross_hairs_items
            obj_items = [arc_item.obj_item for arc_item in ch_arc_items]
            obj_items.remove(ch_item)
            self._spine_db_editor.finalize_relationship(
                self.relationship_class, self._hovered_obj_item, *obj_items)
            self.clear_cross_hairs_items()

    def mouseMoveEvent(self, event):
        """Updates the hovered object item if we're in relationship creation mode."""
        if self.cross_hairs_items:
            self._update_cross_hairs_pos(event.pos())
            return
        super().mouseMoveEvent(event)
        if not self.itemAt(
                event.pos()) and (event.buttons() & Qt.LeftButton != 0):
            if self._previous_mouse_pos is not None:
                delta = event.pos() - self._previous_mouse_pos
                self._scroll_scene_by(delta.x(), delta.y())
            self._previous_mouse_pos = event.pos()

    def _update_cross_hairs_pos(self, pos):
        """Updates the hovered object item and sets the 'cross_hairs' icon accordingly.

        Args:
            pos (QPoint): the desired position in view coordinates
        """
        cross_hairs_item = self.cross_hairs_items[0]
        scene_pos = self.mapToScene(pos)
        delta = scene_pos - cross_hairs_item.scenePos()
        cross_hairs_item.block_move_by(delta.x(), delta.y())
        self._hovered_obj_item = None
        obj_items = [
            item for item in self.items(pos) if isinstance(item, ObjectItem)
        ]
        self._hovered_obj_item = next(iter(obj_items), None)
        if self._hovered_obj_item is not None:
            if self._cross_hairs_has_valid_taget():
                if len(self.relationship_class["object_class_ids_to_go"]) == 1:
                    self.cross_hairs_items[0].set_check_icon()
                else:
                    self.cross_hairs_items[0].set_plus_icon()
                return
            self.cross_hairs_items[0].set_ban_icon()
            return
        self.cross_hairs_items[0].set_normal_icon()

    def mouseReleaseEvent(self, event):
        if not self.cross_hairs_items:
            super().mouseReleaseEvent(event)

    def _scroll_scene_by(self, dx, dy):
        if dx == dy == 0:
            return
        scene_rect = self.sceneRect()
        view_scene_rect = self.mapFromScene(scene_rect).boundingRect()
        view_rect = self.viewport().rect()
        scene_dx = abs((self.mapToScene(0, 0) - self.mapToScene(dx, 0)).x())
        scene_dy = abs((self.mapToScene(0, 0) - self.mapToScene(0, dy)).y())
        if dx < 0 and view_rect.right() - dx >= view_scene_rect.right():
            scene_rect.adjust(0, 0, scene_dx, 0)
        elif dx > 0 and view_rect.left() - dx <= view_scene_rect.left():
            scene_rect.adjust(-scene_dx, 0, 0, 0)
        if dy < 0 and view_rect.bottom() - dy >= view_scene_rect.bottom():
            scene_rect.adjust(0, 0, 0, scene_dy)
        elif dy > 0 and view_rect.top() - dy <= view_scene_rect.top():
            scene_rect.adjust(0, -scene_dy, 0, 0)
        self.scene().setSceneRect(scene_rect)

    def keyPressEvent(self, event):
        """Aborts relationship creation if user presses ESC."""
        super().keyPressEvent(event)
        if event.key() == Qt.Key_Escape and self.cross_hairs_items:
            self._spine_db_editor.msg.emit("Relationship creation aborted.")
            self.clear_cross_hairs_items()

    def contextMenuEvent(self, e):
        """Shows context menu.

        Args:
            e (QContextMenuEvent): Context menu event
        """
        super().contextMenuEvent(e)
        if e.isAccepted():
            return
        e.accept()
        self._context_menu_pos = self.mapToScene(e.pos())
        self._menu.exec_(e.globalPos())

    def _compute_max_zoom(self):
        return sys.maxsize

    def _use_smooth_zoom(self):
        return self._qsettings.value("appSettings/smoothEntityGraphZoom",
                                     defaultValue="false") == "true"

    def _zoom(self, factor):
        self.scale(factor, factor)
        self.apply_zoom()

    def apply_zoom(self):
        for item in self.items():
            if hasattr(item, "apply_zoom"):
                item.apply_zoom(self.zoom_factor)

    def wheelEvent(self, event):
        """Zooms in/out. If user has pressed the shift key, rotates instead.

        Args:
            event (QWheelEvent): Mouse wheel event
        """
        if event.modifiers() != Qt.ShiftModifier:
            super().wheelEvent(event)
            return
        if event.orientation() != Qt.Vertical:
            event.ignore()
            return
        event.accept()
        smooth_rotation = self._qsettings.value(
            "appSettings/smoothEntityGraphRotation", defaultValue="false")
        if smooth_rotation == "true":
            num_degrees = event.delta() / 8
            num_steps = num_degrees / 15
            self._scheduled_transformations += num_steps
            if self._scheduled_transformations * num_steps < 0:
                self._scheduled_transformations = num_steps
            if self.time_line:
                self.time_line.deleteLater()
            self.time_line = QTimeLine(200, self)
            self.time_line.setUpdateInterval(20)
            self.time_line.valueChanged.connect(
                self._handle_rotation_time_line_advanced)
            self.time_line.finished.connect(
                self._handle_transformation_time_line_finished)
            self.time_line.start()
        else:
            angle = event.angleDelta().y() / 8
            self._rotate(angle)
            self._set_preferred_scene_rect()

    def _handle_rotation_time_line_advanced(self, pos):
        """Performs rotation whenever the smooth rotation time line advances."""
        angle = self._scheduled_transformations / 2.0
        self._rotate(angle)

    def _rotate(self, angle):
        center = self._get_viewport_scene_rect().center()
        for item in self.items():
            if hasattr(item, "apply_rotation"):
                item.apply_rotation(angle, center)

    def rotate_clockwise(self):
        """Performs a rotate clockwise with fixed angle."""
        self._rotate(-self._angle / 8)
        self._set_preferred_scene_rect()

    def rotate_anticlockwise(self):
        """Performs a rotate anticlockwise with fixed angle."""
        self._rotate(self._angle / 8)
        self._set_preferred_scene_rect()
예제 #10
0
class CustomQGraphicsView(QGraphicsView):
    """Super class for Design and Entity QGraphicsViews.

    Attributes:
        parent (QWidget): Parent widget
    """
    def __init__(self, parent):
        """Init CustomQGraphicsView."""
        super().__init__(parent=parent)
        self._zoom_factor_base = 1.0015
        self._angle = 120
        self._scheduled_transformations = 0
        self.time_line = None
        self._items_fitting_zoom = 1.0
        self._max_zoom = 10.0
        self._min_zoom = 0.1
        self._qsettings = QSettings("SpineProject", "Spine Toolbox")

    @property
    def zoom_factor(self):
        return self.transform().m11(
        )  # The [1, 1] element contains the x scaling factor

    def reset_zoom(self):
        """Resets zoom to the default factor."""
        self.scene().center_items()
        self._update_zoom_limits()
        self._zoom(self._items_fitting_zoom)

    def keyPressEvent(self, event):
        """Overridden method. Enable zooming with plus and minus keys (comma resets zoom).
        Send event downstream to QGraphicsItems if pressed key is not handled here.

        Args:
            event (QKeyEvent): Pressed key
        """
        if event.key() == Qt.Key_Plus:
            self.zoom_in()
        elif event.key() == Qt.Key_Minus:
            self.zoom_out()
        elif event.key() == Qt.Key_Comma:
            self.reset_zoom()
        else:
            super().keyPressEvent(event)

    def mousePressEvent(self, event):
        """Set rubber band selection mode if Control pressed.
        Enable resetting the zoom factor from the middle mouse button.
        """
        item = self.itemAt(event.pos())
        if not item or not item.acceptedMouseButtons() & event.buttons():
            if event.modifiers() & Qt.ControlModifier:
                self.setDragMode(QGraphicsView.RubberBandDrag)
                self.viewport().setCursor(Qt.CrossCursor)
            if event.button() == Qt.MidButton:
                self.reset_zoom()
        super().mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        """Reestablish scroll hand drag mode."""
        super().mouseReleaseEvent(event)
        item = next(
            iter([x for x in self.items(event.pos()) if x.hasCursor()]), None)
        was_not_rubber_band_drag = self.dragMode(
        ) != QGraphicsView.RubberBandDrag
        self.setDragMode(QGraphicsView.ScrollHandDrag)
        if item and was_not_rubber_band_drag:
            self.viewport().setCursor(item.cursor())
        else:
            self.viewport().setCursor(Qt.ArrowCursor)

    def _use_smooth_zoom(self):
        return self._qsettings.value("appSettings/smoothZoom",
                                     defaultValue="false") == "true"

    def wheelEvent(self, event):
        """Zooms in/out.

        Args:
            event (QWheelEvent): Mouse wheel event
        """
        if event.orientation() != Qt.Vertical:
            event.ignore()
            return
        event.accept()
        if self._use_smooth_zoom():
            angle = event.delta() / 8
            steps = angle / 15
            self._scheduled_transformations += steps
            if self._scheduled_transformations * steps < 0:
                self._scheduled_transformations = steps
            if self.time_line:
                self.time_line.deleteLater()
            self.time_line = QTimeLine(200, self)
            self.time_line.setUpdateInterval(20)
            self.time_line.valueChanged.connect(lambda x, pos=event.pos(
            ): self._handle_zoom_time_line_advanced(pos))
            self.time_line.finished.connect(
                self._handle_transformation_time_line_finished)
            self.time_line.start()
        else:
            angle = event.angleDelta().y()
            factor = self._zoom_factor_base**angle
            self.gentle_zoom(factor, event.pos())
            self._set_preferred_scene_rect()

    def resizeEvent(self, event):
        """
        Updates zoom if needed when the view is resized.

        Args:
            event (QResizeEvent): a resize event
        """
        new_size = self.size()
        old_size = event.oldSize()
        if new_size != old_size:
            scene = self.scene()
            if scene is not None:
                self._update_zoom_limits()
                if self.time_line:
                    self.time_line.deleteLater()
                self.time_line = QTimeLine(200, self)
                self.time_line.finished.connect(
                    self._handle_resize_time_line_finished)
                self.time_line.start()
        super().resizeEvent(event)

    def setScene(self, scene):
        """
        Sets a new scene to this view.

        Args:
            scene (ShrinkingScene): a new scene
        """
        super().setScene(scene)
        scene.item_move_finished.connect(self._handle_item_move_finished)
        scene.item_removed.connect(
            lambda _item: self._set_preferred_scene_rect())
        self.viewport().setCursor(Qt.ArrowCursor)

    @Slot("QGraphicsItem")
    def _handle_item_move_finished(self, item):
        self._ensure_item_visible(item)
        self._update_zoom_limits()

    def _update_zoom_limits(self):
        """
        Updates the minimum zoom limit and the zoom level with which the view fits all the items in the scene.
        """
        rect = self.scene().itemsBoundingRect()
        if rect.isEmpty():
            return
        viewport_scene_rect = self._get_viewport_scene_rect()
        x_factor = viewport_scene_rect.width() / rect.width()
        y_factor = viewport_scene_rect.height() / rect.height()
        self._items_fitting_zoom = 0.9 * min(x_factor, y_factor)
        self._min_zoom = self._compute_min_zoom()
        self._max_zoom = 10 * self._min_zoom

    def _compute_min_zoom(self):
        return min(0.5, self.zoom_factor * self._items_fitting_zoom)

    def _handle_zoom_time_line_advanced(self, pos):
        """Performs zoom whenever the smooth zoom time line advances."""
        factor = 1.0 + self._scheduled_transformations / 100.0
        self.gentle_zoom(factor, pos)

    @Slot()
    def _handle_transformation_time_line_finished(self):
        """Cleans up after the smooth transformation time line finishes."""
        if self._scheduled_transformations > 0:
            self._scheduled_transformations -= 1
        else:
            self._scheduled_transformations += 1
        if self.sender():
            self.sender().deleteLater()
        self.time_line = None
        self._set_preferred_scene_rect()

    @Slot()
    def _handle_resize_time_line_finished(self):
        """Cleans up after resizing time line finishes."""
        if self.sender():
            self.sender().deleteLater()
        self.time_line = None
        self._set_preferred_scene_rect()

    def zoom_in(self):
        """Perform a zoom in with a fixed scaling."""
        self.gentle_zoom(self._zoom_factor_base**self._angle)
        self._set_preferred_scene_rect()

    def zoom_out(self):
        """Perform a zoom out with a fixed scaling."""
        self.gentle_zoom(self._zoom_factor_base**-self._angle)
        self._set_preferred_scene_rect()

    def gentle_zoom(self, factor, zoom_focus=None):
        """
        Perform a zoom by a given factor.

        Args:
            factor (float): a scaling factor relative to the current scene scaling
            zoom_focus (QPoint): focus of the zoom, e.g. mouse pointer position
        """
        if zoom_focus is None:
            zoom_focus = self.viewport().rect().center()
        initial_focus_on_scene = self.mapToScene(zoom_focus)
        current_zoom = self.zoom_factor
        proposed_zoom = current_zoom * factor
        if proposed_zoom < self._min_zoom:
            factor = self._min_zoom / current_zoom
        elif proposed_zoom > self._max_zoom:
            factor = self._max_zoom / current_zoom
        if math.isclose(factor, 1.0):
            return
        self._zoom(factor)
        post_scaling_focus_on_scene = self.mapToScene(zoom_focus)
        center_on_scene = self.mapToScene(self.viewport().rect().center())
        focus_diff = post_scaling_focus_on_scene - initial_focus_on_scene
        self.centerOn(center_on_scene - focus_diff)

    def _zoom(self, factor):
        self.scale(factor, factor)

    def _get_viewport_scene_rect(self):
        """Returns the viewport rect mapped to the scene.

        Returns:
            QRectF
        """
        rect = self.viewport().rect()
        top_left = self.mapToScene(rect.topLeft())
        bottom_right = self.mapToScene(rect.bottomRight())
        return QRectF(top_left, bottom_right)

    def _ensure_item_visible(self, item):
        """Resets zoom if item is not visible."""
        # Because of zooming, we need to find the item scene's rect as below
        item_scene_rect = item.boundingRegion(
            item.sceneTransform()).boundingRect()
        viewport_scene_rect = self._get_viewport_scene_rect()
        if not viewport_scene_rect.contains(item_scene_rect.topLeft()):
            scene_rect = viewport_scene_rect.united(item_scene_rect)
            self.fitInView(scene_rect, Qt.KeepAspectRatio)
            self._set_preferred_scene_rect()

    @Slot()
    def _set_preferred_scene_rect(self):
        """Sets the scene rect to the result of uniting the scene viewport rect and the items bounding rect.
        """
        viewport_scene_rect = self._get_viewport_scene_rect()
        items_scene_rect = self.scene().itemsBoundingRect()
        self.scene().setSceneRect(viewport_scene_rect.united(items_scene_rect))
class GraphViewGraphicsView(QGraphicsView):
    """A QGraphicsView to use with the GraphViewForm."""

    item_dropped = Signal("QPoint", "QString", name="item_dropped")

    def __init__(self, parent):
        """Init class."""
        super().__init__(parent)
        self._graph_view_form = None
        self._zoom_factor_base = 1.0015
        self._angle = 120
        self.target_viewport_pos = None
        self.target_scene_pos = QPointF(0, 0)
        self._num_scheduled_scalings = 0
        self.anim = None
        self.rel_zoom_factor = 1.0
        self.default_zoom_factor = None
        self.max_rel_zoom_factor = 10.0
        self.min_rel_zoom_factor = 0.1

    def mouseMoveEvent(self, event):
        """Register mouse position to recenter the scene after zoom."""
        super().mouseMoveEvent(event)
        if self.target_viewport_pos is not None:
            delta = self.target_viewport_pos - event.pos()
            if delta.manhattanLength() <= 3:
                return
        self.target_viewport_pos = event.pos()
        self.target_scene_pos = self.mapToScene(self.target_viewport_pos)

    def wheelEvent(self, event):
        """Zoom in/out."""
        if event.orientation() != Qt.Vertical:
            event.ignore()
            return
        event.accept()
        try:
            config = self._graph_view_form._data_store._toolbox._config
            use_smooth_zoom = config.getboolean("settings", "use_smooth_zoom")
        except AttributeError:
            use_smooth_zoom = False
        if use_smooth_zoom:
            num_degrees = event.delta() / 8
            num_steps = num_degrees / 15
            self._num_scheduled_scalings += num_steps
            if self._num_scheduled_scalings * num_steps < 0:
                self._num_scheduled_scalings = num_steps
            if self.anim:
                self.anim.deleteLater()
            self.anim = QTimeLine(200, self)
            self.anim.setUpdateInterval(20)
            self.anim.valueChanged.connect(self.scaling_time)
            self.anim.finished.connect(self.anim_finished)
            self.anim.start()
        else:
            angle = event.angleDelta().y()
            factor = self._zoom_factor_base**angle
            self.gentle_zoom(factor)

    def scaling_time(self, x):
        """Called when animation value for smooth zoom changes. Perform zoom."""
        factor = 1.0 + self._num_scheduled_scalings / 100.0
        self.gentle_zoom(factor)

    def anim_finished(self):
        """Called when animation for smooth zoom finishes. Clean up."""
        if self._num_scheduled_scalings > 0:
            self._num_scheduled_scalings -= 1
        else:
            self._num_scheduled_scalings += 1
        self.sender().deleteLater()
        self.anim = None

    def zoom_in(self):
        """Perform a zoom in with a fixed scaling."""
        self.target_viewport_pos = self.viewport().rect().center()
        self.target_scene_pos = self.mapToScene(self.target_viewport_pos)
        self.gentle_zoom(self._zoom_factor_base**self._angle)

    def zoom_out(self):
        """Perform a zoom out with a fixed scaling."""
        self.gentle_zoom(self._zoom_factor_base**-self._angle)

    def reset_zoom(self):
        """Reset zoom to the default factor."""
        if not self.default_zoom_factor:
            return
        self.resetTransform()
        self.scale(self.default_zoom_factor, self.default_zoom_factor)
        self.rel_zoom_factor = 1.0

    def gentle_zoom(self, factor):
        """Perform a zoom by a given factor."""
        new_rel_zoom_factor = self.rel_zoom_factor * factor
        if new_rel_zoom_factor > self.max_rel_zoom_factor or new_rel_zoom_factor < self.min_rel_zoom_factor:
            return
        self.rel_zoom_factor = new_rel_zoom_factor
        self.scale(factor, factor)
        self.centerOn(self.target_scene_pos)
        delta_viewport_pos = self.target_viewport_pos - self.viewport(
        ).geometry().center()
        viewport_center = self.mapFromScene(
            self.target_scene_pos) - delta_viewport_pos
        self.centerOn(self.mapToScene(viewport_center))

    def scale_to_fit_scene(self):
        """Scale view so the scene fits best in it."""
        if not self.isVisible():
            return
        scene_rect = self.sceneRect()
        scene_extent = max(scene_rect.width(), scene_rect.height())
        if not scene_extent:
            return
        size = self.size()
        extent = min(size.height(), size.width())
        self.default_zoom_factor = extent / scene_extent
        self.reset_zoom()

    def mousePressEvent(self, event):
        """Set rubber band drag mode if control pressed."""
        if event.modifiers() & Qt.ControlModifier:
            self.setDragMode(QGraphicsView.RubberBandDrag)
        if event.button() == Qt.MidButton:
            self.reset_zoom()
        super().mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        """Restablish scroll hand drag mode."""
        super().mouseReleaseEvent(event)
        self.setDragMode(QGraphicsView.ScrollHandDrag)

    def dragLeaveEvent(self, event):
        """Accept event. Then call the super class method
        only if drag source is not DragListView."""
        event.accept()

    def dragEnterEvent(self, event):
        """Accept event. Then call the super class method
        only if drag source is not DragListView."""
        event.accept()
        source = event.source()
        if not isinstance(source, DragListView):
            super().dragEnterEvent(event)

    def dragMoveEvent(self, event):
        """Accept event. Then call the super class method
        only if drag source is not DragListView."""
        event.accept()
        source = event.source()
        if not isinstance(source, DragListView):
            super().dragMoveEvent(event)

    def dropEvent(self, event):
        """Only accept drops when the source is an instance of DragListView.
        Capture text from event's mimedata and emit signal.
        """
        source = event.source()
        if not isinstance(source, DragListView):
            super().dropEvent(event)
            return
        event.acceptProposedAction()
        text = event.mimeData().text()
        pos = event.pos()
        self.item_dropped.emit(pos, text)

    def contextMenuEvent(self, e):
        """Show context menu.

        Args:
            e (QContextMenuEvent): Context menu event
        """
        super().contextMenuEvent(e)
        if e.isAccepted():
            return
        if not self._graph_view_form:
            e.ignore()
            return
        e.accept()
        self._graph_view_form.show_graph_view_context_menu(e.globalPos())
예제 #12
0
class CustomQGraphicsView(QGraphicsView):
    """Super class for Design and Graph QGraphicsViews.

    Attributes:
        parent (QWidget): Parent widget
    """

    def __init__(self, parent):
        """Init CustomQGraphicsView."""
        super().__init__(parent=parent)
        self._zoom_factor_base = 1.0015
        self._angle = 120
        self._num_scheduled_scalings = 0
        self.anim = None
        self._scene_fitting_zoom = 1.0
        self._max_zoom = 10.0
        self._min_zoom = 0.1
        self._qsettings = QSettings("SpineProject", "Spine Toolbox")

    def keyPressEvent(self, event):
        """Overridden method. Enable zooming with plus and minus keys (comma resets zoom).
        Send event downstream to QGraphicsItems if pressed key is not handled here.

        Args:
            event (QKeyEvent): Pressed key
        """
        if event.key() == Qt.Key_Plus:
            self.zoom_in()
        elif event.key() == Qt.Key_Minus:
            self.zoom_out()
        elif event.key() == Qt.Key_Comma:
            self.reset_zoom()
        else:
            super().keyPressEvent(event)

    def enterEvent(self, event):
        """Overridden method. Do not show the stupid open hand mouse cursor.

        Args:
            event (QEvent): event
        """
        super().enterEvent(event)
        self.viewport().setCursor(Qt.ArrowCursor)

    def mousePressEvent(self, event):
        """Set rubber band selection mode if Control pressed.
        Enable resetting the zoom factor from the middle mouse button.
        """
        item = self.itemAt(event.pos())
        # print(not item, not int(item.acceptedMouseButtons() & event.buttons()))
        if not item or not item.acceptedMouseButtons() & event.buttons():
            if event.modifiers() & Qt.ControlModifier:
                self.setDragMode(QGraphicsView.RubberBandDrag)
                self.viewport().setCursor(Qt.CrossCursor)
            if event.button() == Qt.MidButton:
                self.reset_zoom()
        super().mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        """Reestablish scroll hand drag mode."""
        super().mouseReleaseEvent(event)
        item = self.itemAt(event.pos())
        if not item or not item.acceptedMouseButtons():
            self.setDragMode(QGraphicsView.ScrollHandDrag)
            self.viewport().setCursor(Qt.ArrowCursor)

    def wheelEvent(self, event):
        """Zoom in/out.

        Args:
            event (QWheelEvent): Mouse wheel event
        """
        if event.orientation() != Qt.Vertical:
            event.ignore()
            return
        event.accept()
        smooth_zoom = self._qsettings.value("appSettings/smoothZoom", defaultValue="false")
        if smooth_zoom == "true":
            num_degrees = event.delta() / 8
            num_steps = num_degrees / 15
            self._num_scheduled_scalings += num_steps
            if self._num_scheduled_scalings * num_steps < 0:
                self._num_scheduled_scalings = num_steps
            if self.anim:
                self.anim.deleteLater()
            self.anim = QTimeLine(200, self)
            self.anim.setUpdateInterval(20)
            self.anim.valueChanged.connect(lambda x, pos=event.pos(): self.scaling_time(pos))
            self.anim.finished.connect(self.anim_finished)
            self.anim.start()
        else:
            angle = event.angleDelta().y()
            factor = self._zoom_factor_base ** angle
            self.gentle_zoom(factor, event.pos())

    def resizeEvent(self, event):
        """
        Updates zoom if needed when the view is resized.

        Args:
            event (QResizeEvent): a resize event
        """
        new_size = self.size()
        old_size = event.oldSize()
        if new_size != old_size:
            scene = self.scene()
            if scene is not None:
                self._update_zoom_limits(scene.sceneRect())
                if new_size.width() > old_size.width() or new_size.height() > old_size.height():
                    transform = self.transform()
                    zoom = transform.m11()
                    if zoom < self._min_zoom:
                        # Reset the zoom if the view has grown and the current zoom is too small
                        self.reset_zoom()
        super().resizeEvent(event)

    def setScene(self, scene):
        """
        Sets a new scene to this view.

        Args:
            scene (ShrinkingScene): a new scene
        """
        super().setScene(scene)
        scene.sceneRectChanged.connect(self._update_zoom_limits)
        scene.item_move_finished.connect(self._ensure_item_visible)

    @Slot("QRectF")
    def _update_zoom_limits(self, rect):
        """
        Updates the minimum zoom limit and the zoom level with which the entire scene fits the view.

        Args:
            rect (QRectF): the scene's rect
        """
        scene_extent = max(rect.width(), rect.height())
        if not scene_extent:
            return
        size = self.size()
        extent = min(size.height(), size.width())
        self._scene_fitting_zoom = extent / scene_extent
        self._min_zoom = min(self._scene_fitting_zoom, 0.1)

    def scaling_time(self, pos):
        """Called when animation value for smooth zoom changes. Perform zoom."""
        factor = 1.0 + self._num_scheduled_scalings / 100.0
        self.gentle_zoom(factor, pos)

    def anim_finished(self):
        """Called when animation for smooth zoom finishes. Clean up."""
        if self._num_scheduled_scalings > 0:
            self._num_scheduled_scalings -= 1
        else:
            self._num_scheduled_scalings += 1
        self.sender().deleteLater()
        self.anim = None

    def zoom_in(self):
        """Perform a zoom in with a fixed scaling."""
        self.gentle_zoom(self._zoom_factor_base ** self._angle, self.viewport().rect().center())

    def zoom_out(self):
        """Perform a zoom out with a fixed scaling."""
        self.gentle_zoom(self._zoom_factor_base ** -self._angle, self.viewport().rect().center())

    def reset_zoom(self):
        """Reset zoom to the default factor."""
        self.resetTransform()
        if self._scene_fitting_zoom < 1.0:
            self.scale(self._scene_fitting_zoom, self._scene_fitting_zoom)

    def gentle_zoom(self, factor, zoom_focus):
        """
        Perform a zoom by a given factor.

        Args:
            factor (float): a scaling factor relative to the current scene scaling
            zoom_focus (QPoint): focus of the zoom, e.g. mouse pointer position
        """
        initial_focus_on_scene = self.mapToScene(zoom_focus)
        transform = self.transform()
        current_zoom = transform.m11()  # The [1, 1] element contains the x scaling factor
        proposed_zoom = current_zoom * factor
        if proposed_zoom < self._min_zoom:
            factor = self._min_zoom / current_zoom
        elif proposed_zoom > self._max_zoom:
            factor = self._max_zoom / current_zoom
        if math.isclose(factor, 1.0):
            return False
        self.scale(factor, factor)
        post_scaling_focus_on_scene = self.mapToScene(zoom_focus)
        center_on_scene = self.mapToScene(self.viewport().rect().center())
        focus_diff = post_scaling_focus_on_scene - initial_focus_on_scene
        self.centerOn(center_on_scene - focus_diff)
        return True

    @Slot("QGraphicsItem")
    def _ensure_item_visible(self, item):
        """Resets zoom if item is not visible."""
        if not self.viewport().geometry().contains(self.mapFromScene(item.pos())):
            self.reset_zoom()
예제 #13
0
class ImporterExporterAnimation:
    def __init__(self,
                 item,
                 duration=2000,
                 count=5,
                 percentage_size=0.24,
                 x_shift=0):
        """Initializes animation stuff.

        Args:
            item (QGraphicsItem): The item on top of which the animation should play.
        """
        self._item = item
        self.cubes = [QGraphicsTextItem("\uf1b2", item) for i in range(count)]
        self.opacity_at_value_path = QPainterPath(QPointF(0.0, 0.0))
        self.opacity_at_value_path.lineTo(QPointF(0.01, 1.0))
        self.opacity_at_value_path.lineTo(QPointF(0.5, 1.0))
        self.opacity_at_value_path.lineTo(QPointF(1.0, 0.0))
        self.time_line = QTimeLine()
        self.time_line.setLoopCount(0)  # loop forever
        self.time_line.setFrameRange(0, 10)
        self.time_line.setDuration(duration)
        self.time_line.setCurveShape(QTimeLine.LinearCurve)
        self.time_line.valueChanged.connect(
            self._handle_time_line_value_changed)
        self.time_line.stateChanged.connect(
            self._handle_time_line_state_changed)
        font = QFont('Font Awesome 5 Free Solid')
        item_rect = item.rect()
        cube_size = percentage_size * 0.875 * item_rect.height()
        font.setPixelSize(cube_size)
        rect = item_rect.translated(-0.5 * cube_size + x_shift, -cube_size)
        end = rect.center()
        ctrl = end - QPointF(0, 0.6 * rect.height())
        lower, upper = 0.2, 0.8
        starts = [lower + i * (upper - lower) / count for i in range(count)]
        starts = [
            rect.topLeft() + QPointF(start * rect.width(), 0)
            for start in starts
        ]
        self.paths = [QPainterPath(start) for start in starts]
        for path in self.paths:
            path.quadTo(ctrl, end)
        self.offsets = [i / count for i in range(count)]
        for cube in self.cubes:
            cube.setFont(font)
            cube.setDefaultTextColor("#003333")
            cube.setTransformOriginPoint(cube.boundingRect().center())
            cube.hide()
            cube.setOpacity(0)

    @Slot(float)
    def _handle_time_line_value_changed(self, value):
        for cube, offset, path in zip(self.cubes, self.offsets, self.paths):
            value = (offset + value) % 1.0
            opacity = self.opacity_at_value_path.pointAtPercent(value).y()
            cube.setOpacity(opacity)
            percent = self.percent(value)
            point = path.pointAtPercent(percent)
            angle = percent * 360.0
            cube.setPos(point)
            cube.setRotation(angle)

    @Slot("QTimeLine::State")
    def _handle_time_line_state_changed(self, new_state):
        if new_state == QTimeLine.Running:
            random.shuffle(self.offsets)
            for cube in self.cubes:
                cube.show()
        elif new_state == QTimeLine.NotRunning:
            for cube in self.cubes:
                cube.hide()

    def start(self):
        """Starts the animation."""
        if self.time_line.state() == QTimeLine.Running:
            return
        self.time_line.start()

    @staticmethod
    def percent(value):
        raise NotImplementedError()

    def stop(self):
        """Stops the animation"""
        self.time_line.stop()
예제 #14
0
class ToolIcon(ProjectItemIcon):
    def __init__(self, toolbox, x, y, w, h, name):
        """Tool icon for the Design View.

        Args:
            toolbox (ToolBoxUI): QMainWindow instance
            x (float): Icon x coordinate
            y (float): Icon y coordinate
            w (float): Width of master icon
            h (float): Height of master icon
            name (str): Item name
        """
        super().__init__(
            toolbox,
            x,
            y,
            w,
            h,
            name,
            ":/icons/project_item_icons/hammer.svg",
            icon_color=QColor("red"),
            background_color=QColor("#ffe6e6"),
        )
        # animation stuff
        self.timer = QTimeLine()
        self.timer.setLoopCount(0)  # loop forever
        self.timer.setFrameRange(0, 10)
        # self.timer.setCurveShape(QTimeLine.CosineCurve)
        self.timer.valueForTime = self._value_for_time
        self.tool_animation = QGraphicsItemAnimation()
        self.tool_animation.setItem(self.svg_item)
        self.tool_animation.setTimeLine(self.timer)
        self.delta = 0.25 * self.svg_item.sceneBoundingRect().height()

    @staticmethod
    def _value_for_time(msecs):
        rem = (msecs % 1000) / 1000
        return 1.0 - rem

    def start_animation(self):
        """Start the animation that plays when the Tool associated to this GraphicsItem is running.
        """
        if self.timer.state() == QTimeLine.Running:
            return
        self.svg_item.moveBy(0, -self.delta)
        offset = 0.75 * self.svg_item.sceneBoundingRect().height()
        for angle in range(1, 45):
            step = angle / 45.0
            self.tool_animation.setTranslationAt(step, 0, offset)
            self.tool_animation.setRotationAt(step, angle)
            self.tool_animation.setTranslationAt(step, 0, -offset)
            self.tool_animation.setPosAt(
                step,
                QPointF(self.svg_item.pos().x(),
                        self.svg_item.pos().y() + offset))
        self.timer.start()

    def stop_animation(self):
        """Stop animation"""
        if self.timer.state() != QTimeLine.Running:
            return
        self.timer.stop()
        self.svg_item.moveBy(0, self.delta)
        self.timer.setCurrentTime(999)
class ImportExportAnimation:
    def __init__(self, parent_item, src_item, dst_item, duration=2000):
        """Initializes animation stuff.

        Args:
            parent_item (QGraphicsItem): The item on top of which the animation should play.
            src_item (QGraphicsItem): The source item.
            dst_item (QGraphicsItem): The destination item.
            duration (int. optional): The desired duration of each loop in milliseconds, defaults to 1000.
        """
        self._parent_item = parent_item
        self.src_item = src_item
        self.dst_item = dst_item
        font = QFont('Font Awesome 5 Free Solid')
        size = 0.875 * round(parent_item.rect().height() / 2)
        font.setPixelSize(size)
        self.src_item.setFont(font)
        self.dst_item.setFont(font)
        self.src_opacity_effect = QGraphicsOpacityEffect()
        self.src_item.setGraphicsEffect(self.src_opacity_effect)
        self.dst_opacity_effect = QGraphicsOpacityEffect()
        self.dst_item.setGraphicsEffect(self.dst_opacity_effect)
        self.timer = QTimeLine()
        self.timer.setLoopCount(0)  # loop forever
        self.timer.setFrameRange(0, 10)
        self.timer.valueChanged.connect(self._handle_timer_value_changed)
        self.timer.setDuration(duration)
        self.src_animation = QGraphicsItemAnimation()
        self.src_animation.setItem(self.src_item)
        self.src_animation.setTimeLine(self.timer)
        self.dst_animation = QGraphicsItemAnimation()
        self.dst_animation.setItem(self.dst_item)
        self.dst_animation.setTimeLine(self.timer)

    @Slot(float)
    def _handle_timer_value_changed(self, value):
        self.src_opacity_effect.setOpacity(1.0 - 2 * value)
        self.dst_opacity_effect.setOpacity(2 * value - 1.0)

    def start(self):
        """Starts the animation."""
        rect = self._parent_item.rect()
        dx = self.src_item.boundingRect().width()
        dy = self.dst_item.boundingRect().height()
        rect.adjust(0, 0, -dx, -dy)
        src, dst = rect.topLeft(), rect.bottomRight()
        vec = dst - src
        self.src_item.setParentItem(self._parent_item)
        self.dst_item.setParentItem(self._parent_item)
        self.src_item.setPos(src)
        self.dst_item.setPos(src)
        self.src_opacity_effect.setOpacity(0.0)
        self.dst_opacity_effect.setOpacity(0.0)
        for i in range(100):
            step = i / 100.0
            self.src_animation.setPosAt(step, src + vec * step)
            self.dst_animation.setPosAt(step, src + vec * step)
        self.timer.start()

    def stop(self):
        """Stops the animation"""
        self.timer.stop()
        self.src_item.setParentItem(None)
        self.dst_item.setParentItem(None)
        self.src_item.scene().removeItem(self.src_item)
        self.dst_item.scene().removeItem(self.dst_item)
        self.timer.setCurrentTime(999)
예제 #16
0
class MainWindow(QMainWindow):
    def __init__(self):
        global pixmapDict, specialDescriptionDict
        super(MainWindow, self).__init__()
        self.ui = Ui_pipboy()
        self.ui.setupUi(self)

        self.ui.chartContainer.setContentsMargins(0, 0, 0, 0)

        self.anim = QTimeLine(20000, self)
        self.anim.setFrameRange(0, 500)
        self.anim.setLoopCount(0)
        self.anim.setUpdateInterval(16)
        self.anim.frameChanged[int].connect(
            self.ui.perks_description.verticalScrollBar().setValue)
        self.anim.frameChanged[int].connect(
            self.ui.aid_effect_label.verticalScrollBar().setValue)
        self.anim.frameChanged[int].connect(
            self.ui.data_description.verticalScrollBar().setValue)

        #self.anim2 = QPropertyAnimation(self.ui.main_tab, b"pos")
        #self.anim2.setEasingCurve(QEasingCurve.OutBounce)
        #self.anim2.setDuration(2000)
        #self.anim2.setStartValue(QPoint(10, -400))
        #self.anim2.setEndValue(QPoint(10, 0))
        #self.anim2.start()

        self.random = QRandomGenerator.global_()

        self.ui.stat_tab.setFocus()
        self.ui.stat_tab.currentChanged.connect(self.shift)
        self.ui.stat_tab.installEventFilter(self)
        self.ui.inv_tab.installEventFilter(self)

        self.ui.special_list.installEventFilter(self)
        self.ui.perks_list.installEventFilter(self)
        self.ui.test_list.installEventFilter(self)
        self.ui.apparel_list.installEventFilter(self)
        self.ui.aid_list.installEventFilter(self)
        self.ui.ammo_list.installEventFilter(self)
        self.ui.data_list.installEventFilter(self)
        self.ui.radio_list.installEventFilter(self)

        self.ui.main_img.setPixmap(description.main_img_pixmap)

        self.ui.special_image.setPixmap(description.pixmapDict.get(0))
        self.ui.perks_image.setPixmap(description.pixmatPerksDict.get(0))
        self.ui.weapon_image.setPixmap(description.pixmapWeaponDict.get(0))
        self.ui.apparel_image.setPixmap(description.pixmapWeaponDict.get(0))
        self.ui.aid_image.setPixmap(description.pixmapAidDict.get(0))
        self.ui.ammo_image.setPixmap(description.pixmapAmmoDict.get(0))

        lay = QVBoxLayout(self.ui.chartContainer)
        lay.setContentsMargins(0, 0, 0, 0)

        self.chartview = QtCharts.QChartView()
        self.chartview.setContentsMargins(0, 0, 0, 0)
        lay.addWidget(self.chartview)

        self.chart = QtCharts.QChart()
        self.chart.legend().hide()
        self.chart.setAnimationOptions(QtCharts.QChart.SeriesAnimations)

        self.series = QtCharts.QLineSeries()
        self.pen = QPen(QColor(119, 251, 81, 255))
        self.pen.setWidth(3)
        self.pen.setJoinStyle(Qt.RoundJoin)
        self.series.setPen(self.pen)

        backgroundGradient = QLinearGradient(QPointF(100, 100),
                                             QPointF(200, 200))

        backgroundGradient.setColorAt(0, QColor(0, 0, 0, 255))
        backgroundGradient.setColorAt(1, QColor(0, 0, 0, 255))
        self.chart.setBackgroundBrush(backgroundGradient)
        self.chart.setPlotAreaBackgroundBrush(backgroundGradient)

        self.chart.addSeries(self.series)

        self.chart.createDefaultAxes()

        self.chart.axisX(self.series).setVisible(False)
        self.chart.axisY(self.series).setVisible(False)
        self.chart.axisY(self.series).setRange(0, 100)
        self.chartview.setChart(self.chart)

        self.play = False
        self.player = QMediaPlayer()

        self.playlistFalloutNewVegas = QMediaPlaylist(self.player)
        self.playlistFalloutNewVegas.addMedia(
            QMediaContent(description.falooutNewVegas))
        self.playlistFalloutNewVegas.setCurrentIndex(1)

        self.playListMohaveMusic = QMediaPlaylist(self.player)

        for url in description.mohaveMusic:
            self.playListMohaveMusic.addMedia(QMediaContent(url))

        self.playListMohaveMusic.setCurrentIndex(1)

        self.playlisNewVegas = QMediaPlaylist(self.player)

        for url in description.newVegas:
            self.playlisNewVegas.addMedia(QMediaContent(url))

        self.playlisNewVegas.setCurrentIndex(1)

        self.playlistDict = {
            0: self.playlistFalloutNewVegas,
            1: self.playListMohaveMusic,
            2: self.playlisNewVegas
        }

    def append_data_and_plot(self, d):
        """Append and update the plot"""
        num, m = d

        ax1 = self.chart.axisX(self.series)

        xmin = xmax = num

        step = 100

        for p in self.series.pointsVector()[-step:]:
            xmin = min(p.x(), xmin)
            xmax = max(p.x(), xmax)

        xmin = max(0, xmax - step)

        ax1.setMin(xmin)
        ax1.setMax(xmax)

        self.series.append(QPointF(num, m))

    def eventFilter(self, obj, event):
        if event.type() == QEvent.KeyPress:
            #print(obj)
            if event.key() == 49:
                self.ui.main_tab.setCurrentIndex(1)
                self.ui.stat_tab.setFocus()
                #self.ui.hp_head.setValue(self.random.bounded(50, 100))
                #self.ui.hp_left_arm.setValue(self.random.bounded(50, 100))
                #self.ui.hp_right_arm.setValue(self.random.bounded(50, 100))
                #self.ui.hp_left_leg.setValue(self.random.bounded(50, 100))
                #self.ui.hp_right_leg.setValue(self.random.bounded(50, 100))
                #self.ui.hp_body.setValue(self.random.bounded(50, 100))
                return True
            elif event.key() == 50:
                self.ui.main_tab.setCurrentIndex(2)
                self.ui.inv_tab.setFocus()
                return True
            elif event.key() == 51:
                self.ui.main_tab.setCurrentIndex(3)
                self.ui.data_list.setFocus()
                self.ui.data_list.setCurrentRow(0)
                return True
            elif event.key() == 52:
                self.ui.main_tab.setCurrentIndex(4)
                return True
            elif event.key() == 53:
                self.ui.main_tab.setCurrentIndex(5)
                self.ui.radio_list.setFocus()
                self.ui.radio_list.setCurrentRow(0)
                return True
            elif event.key() == 54:
                print(QApplication.focusWidget())
                return True
            #focus from stat_tab to special_list
            elif (obj
                  == self.ui.stat_tab) and (self.ui.stat_tab.currentIndex()
                                            == 1) and (event.key() == 47):
                self.ui.special_list.setFocus()
                self.ui.special_list.setCurrentRow(0)
                self.ui.special_image.setPixmap(description.pixmapDict.get(0))
                self.ui.special_description.setText(
                    description.specialDescriptionDict.get(0))
                return True
            #
            elif (obj == self.ui.special_list) and (event.key() == 16777236):
                self.ui.special_list.setCurrentRow(
                    self.ui.special_list.currentRow() + 1)

                if (self.ui.special_list.currentRow() == -1):
                    self.ui.special_list.setCurrentRow(0)

                self.ui.special_list.scrollToItem(
                    self.ui.special_list.currentItem())

                self.ui.special_image.setPixmap(
                    description.pixmapDict.get(
                        self.ui.special_list.currentRow()))
                self.ui.special_description.setText(
                    description.specialDescriptionDict.get(
                        self.ui.special_list.currentRow()))

            elif (obj == self.ui.special_list) and (event.key() == 16777234):
                self.ui.special_list.setCurrentRow(
                    self.ui.special_list.currentRow() - 1)

                if (self.ui.special_list.currentRow() == -1):
                    self.ui.special_list.setCurrentRow(0)

                self.ui.special_list.scrollToItem(
                    self.ui.special_list.currentItem())

                self.ui.special_image.setPixmap(
                    description.pixmapDict.get(
                        self.ui.special_list.currentRow()))
                self.ui.special_description.setText(
                    description.specialDescriptionDict.get(
                        self.ui.special_list.currentRow()))
            #focus from special_list to stat_tab
            elif (obj == self.ui.special_list) and (event.key() == 47):
                self.ui.special_list.setCurrentRow(-1)
                self.ui.stat_tab.setFocus()
                return True
            # focus from stat_tab to perks_list
            elif (obj
                  == self.ui.stat_tab) and (self.ui.stat_tab.currentIndex()
                                            == 2) and (event.key() == 47):
                self.ui.perks_list.setFocus()
                self.ui.perks_list.setCurrentRow(0)
                self.anim.setCurrentTime(0)
                self.anim.start()
            elif (obj == self.ui.perks_list) and (event.key() == 16777236):
                self.ui.perks_list.setCurrentRow(
                    self.ui.perks_list.currentRow() + 1)

                if (self.ui.perks_list.currentRow() == -1):
                    self.ui.perks_list.setCurrentRow(0)

                self.ui.perks_list.scrollToItem(
                    self.ui.perks_list.currentItem())

                self.ui.perks_image.setPixmap(
                    description.pixmatPerksDict.get(
                        self.ui.perks_list.currentRow()))
                self.ui.perks_description.setText(
                    description.perksDescriptionDict.get(
                        self.ui.perks_list.currentRow()))

                self.anim.stop()
                self.anim.setCurrentTime(0)
                self.anim.start()
            elif (obj == self.ui.perks_list) and (event.key() == 16777234):
                self.ui.perks_list.setCurrentRow(
                    self.ui.perks_list.currentRow() - 1)

                if (self.ui.perks_list.currentRow() == -1):
                    self.ui.perks_list.setCurrentRow(0)

                self.ui.perks_list.scrollToItem(
                    self.ui.perks_list.currentItem())

                self.ui.perks_image.setPixmap(
                    description.pixmatPerksDict.get(
                        self.ui.perks_list.currentRow()))
                self.ui.perks_description.setText(
                    description.perksDescriptionDict.get(
                        self.ui.perks_list.currentRow()))

                self.anim.stop()
                self.anim.setCurrentTime(0)
                self.anim.start()
            #focus from perks_list to stat_tab
            elif (obj == self.ui.perks_list) and (event.key() == 47):
                self.ui.perks_list.setCurrentRow(-1)
                self.ui.stat_tab.setFocus()
                self.anim.stop()
                return True

            #/------------------------------------------------INV-WEAPON--------------------------------------------------/
            elif (obj == self.ui.inv_tab) and (self.ui.inv_tab.currentIndex()
                                               == 0) and (event.key() == 47):
                self.ui.test_list.setFocus()
                self.ui.test_list.setCurrentRow(0)
                return True
            elif (obj == self.ui.test_list) and (event.key() == 47):
                self.ui.test_list.setCurrentRow(-1)
                self.ui.inv_tab.setFocus()
                return True

            elif (obj == self.ui.test_list) and (event.key() == 16777236):
                self.ui.test_list.setCurrentRow(
                    self.ui.test_list.currentRow() + 1)
                if (self.ui.test_list.currentRow() == -1):
                    self.ui.test_list.setCurrentRow(0)
                self.ui.test_list.scrollToItem(self.ui.test_list.currentItem())

                self.ui.weapon_image.setPixmap(
                    description.pixmapWeaponDict.get(
                        self.ui.test_list.currentRow()))

                self.ui.w_damage_label.setText(
                    description.weaponDescriptionDict.get(
                        self.ui.test_list.currentRow())[0])
                self.ui.w_weight_label.setText(
                    description.weaponDescriptionDict.get(
                        self.ui.test_list.currentRow())[1])
                self.ui.w_cost_label.setText(
                    description.weaponDescriptionDict.get(
                        self.ui.test_list.currentRow())[2])
                self.ui.w_durability_PB.setValue(self.random.bounded(20, 100))
                self.ui.w_ammo_label.setText(
                    description.weaponDescriptionDict.get(
                        self.ui.test_list.currentRow())[3])
                self.ui.w_effect_label.setText(
                    description.weaponDescriptionDict.get(
                        self.ui.test_list.currentRow())[4])

            elif (obj == self.ui.test_list) and (event.key() == 16777234):
                self.ui.test_list.setCurrentRow(
                    self.ui.test_list.currentRow() - 1)

                if (self.ui.test_list.currentRow() == -1):
                    self.ui.test_list.setCurrentRow(0)

                self.ui.test_list.scrollToItem(self.ui.test_list.currentItem())

                self.ui.weapon_image.setPixmap(
                    description.pixmapWeaponDict.get(
                        self.ui.test_list.currentRow()))

                self.ui.w_damage_label.setText(
                    description.weaponDescriptionDict.get(
                        self.ui.test_list.currentRow())[0])
                self.ui.w_weight_label.setText(
                    description.weaponDescriptionDict.get(
                        self.ui.test_list.currentRow())[1])
                self.ui.w_cost_label.setText(
                    description.weaponDescriptionDict.get(
                        self.ui.test_list.currentRow())[2])
                self.ui.w_durability_PB.setValue(self.random.bounded(20, 100))
                self.ui.w_ammo_label.setText(
                    description.weaponDescriptionDict.get(
                        self.ui.test_list.currentRow())[3])
                self.ui.w_effect_label.setText(
                    description.weaponDescriptionDict.get(
                        self.ui.test_list.currentRow())[4])

            #/------------------------------------------------INV-APPAREL--------------------------------------------------/
            elif (obj == self.ui.inv_tab) and (self.ui.inv_tab.currentIndex()
                                               == 1) and (event.key() == 47):
                self.ui.apparel_list.setFocus()
                self.ui.apparel_list.setCurrentRow(0)
                return True
            elif (obj == self.ui.apparel_list) and (event.key() == 47):
                self.ui.apparel_list.setCurrentRow(-1)
                self.ui.inv_tab.setFocus()
                return True

            elif (obj == self.ui.apparel_list) and (event.key() == 16777236):
                self.ui.apparel_list.setCurrentRow(
                    self.ui.apparel_list.currentRow() + 1)
                if (self.ui.apparel_list.currentRow() == -1):
                    self.ui.apparel_list.setCurrentRow(0)
                self.ui.apparel_list.scrollToItem(
                    self.ui.apparel_list.currentItem())

                self.ui.apparel_image.setPixmap(
                    description.pixmapClothesDict.get(
                        self.ui.apparel_list.currentRow()))

                self.ui.a_damage_label.setText(
                    description.clothesDescriptionDict.get(
                        self.ui.apparel_list.currentRow())[0])
                self.ui.a_weight_label.setText(
                    description.clothesDescriptionDict.get(
                        self.ui.apparel_list.currentRow())[1])
                self.ui.a_cost_label.setText(
                    description.clothesDescriptionDict.get(
                        self.ui.apparel_list.currentRow())[2])
                self.ui.a_durability_PB.setValue(self.random.bounded(0, 100))
                self.ui.a_type_label.setText(
                    description.clothesDescriptionDict.get(
                        self.ui.apparel_list.currentRow())[3])
                self.ui.a_effect_label.setText(
                    description.clothesDescriptionDict.get(
                        self.ui.apparel_list.currentRow())[4])

            elif (obj == self.ui.apparel_list) and (event.key() == 16777234):
                self.ui.apparel_list.setCurrentRow(
                    self.ui.apparel_list.currentRow() - 1)

                if (self.ui.apparel_list.currentRow() == -1):
                    self.ui.apparel_list.setCurrentRow(0)

                self.ui.apparel_list.scrollToItem(
                    self.ui.apparel_list.currentItem())

                self.ui.apparel_image.setPixmap(
                    description.pixmapClothesDict.get(
                        self.ui.apparel_list.currentRow()))

                self.ui.a_damage_label.setText(
                    description.clothesDescriptionDict.get(
                        self.ui.apparel_list.currentRow())[0])
                self.ui.a_weight_label.setText(
                    description.clothesDescriptionDict.get(
                        self.ui.apparel_list.currentRow())[1])
                self.ui.a_cost_label.setText(
                    description.clothesDescriptionDict.get(
                        self.ui.apparel_list.currentRow())[2])
                self.ui.a_durability_PB.setValue(self.random.bounded(0, 100))
                self.ui.a_type_label.setText(
                    description.clothesDescriptionDict.get(
                        self.ui.apparel_list.currentRow())[3])
                self.ui.a_effect_label.setText(
                    description.clothesDescriptionDict.get(
                        self.ui.apparel_list.currentRow())[4])

            #/------------------------------------------------INV-AID--------------------------------------------------/
            elif (obj == self.ui.inv_tab) and (self.ui.inv_tab.currentIndex()
                                               == 2) and (event.key() == 47):
                self.ui.aid_list.setFocus()
                self.ui.aid_list.setCurrentRow(0)
                return True
            elif (obj == self.ui.aid_list) and (event.key() == 47):
                self.ui.aid_list.setCurrentRow(-1)
                self.ui.inv_tab.setFocus()
                return True

            elif (obj == self.ui.aid_list) and (event.key() == 16777236):
                self.ui.aid_list.setCurrentRow(self.ui.aid_list.currentRow() +
                                               1)
                if (self.ui.aid_list.currentRow() == -1):
                    self.ui.aid_list.setCurrentRow(0)
                self.ui.aid_list.scrollToItem(self.ui.aid_list.currentItem())

                self.ui.aid_image.setPixmap(
                    description.pixmapAidDict.get(
                        self.ui.aid_list.currentRow()))

                self.ui.aid_weight_label.setText(
                    description.aidDescriptionDict.get(
                        self.ui.aid_list.currentRow())[0])
                self.ui.aid_cost_label.setText(
                    description.aidDescriptionDict.get(
                        self.ui.aid_list.currentRow())[1])
                self.ui.aid_effect_label.setText(
                    description.aidDescriptionDict.get(
                        self.ui.aid_list.currentRow())[2])

                self.anim.stop()
                self.anim.setCurrentTime(0)
                self.anim.start()
            elif (obj == self.ui.aid_list) and (event.key() == 16777234):
                self.ui.aid_list.setCurrentRow(self.ui.aid_list.currentRow() -
                                               1)

                if (self.ui.aid_list.currentRow() == -1):
                    self.ui.aid_list.setCurrentRow(0)

                self.ui.aid_list.scrollToItem(self.ui.aid_list.currentItem())

                self.ui.aid_image.setPixmap(
                    description.pixmapAidDict.get(
                        self.ui.aid_list.currentRow()))

                self.ui.aid_weight_label.setText(
                    description.aidDescriptionDict.get(
                        self.ui.aid_list.currentRow())[0])
                self.ui.aid_cost_label.setText(
                    description.aidDescriptionDict.get(
                        self.ui.aid_list.currentRow())[1])
                self.ui.aid_effect_label.setText(
                    description.aidDescriptionDict.get(
                        self.ui.aid_list.currentRow())[2])

                self.anim.stop()
                self.anim.setCurrentTime(0)
                self.anim.start()
            #/------------------------------------------------INV-AMMO--------------------------------------------------/
            elif (obj == self.ui.inv_tab) and (self.ui.inv_tab.currentIndex()
                                               == 3) and (event.key() == 47):
                self.ui.ammo_list.setFocus()
                self.ui.ammo_list.setCurrentRow(0)
                return True
            elif (obj == self.ui.ammo_list) and (event.key() == 47):
                self.ui.ammo_list.setCurrentRow(-1)
                self.ui.inv_tab.setFocus()
                return True

            elif (obj == self.ui.ammo_list) and (event.key() == 16777236):
                self.ui.ammo_list.setCurrentRow(
                    self.ui.ammo_list.currentRow() + 1)
                if (self.ui.ammo_list.currentRow() == -1):
                    self.ui.ammo_list.setCurrentRow(0)
                self.ui.ammo_list.scrollToItem(self.ui.ammo_list.currentItem())

                self.ui.ammo_image.setPixmap(
                    description.pixmapAmmoDict.get(
                        self.ui.ammo_list.currentRow()))

                self.ui.ammo_weight_label.setText(
                    description.ammoDescriptionDict.get(
                        self.ui.ammo_list.currentRow())[0])
                self.ui.ammo_cost_label.setText(
                    description.ammoDescriptionDict.get(
                        self.ui.ammo_list.currentRow())[1])

            elif (obj == self.ui.ammo_list) and (event.key() == 16777234):
                self.ui.ammo_list.setCurrentRow(
                    self.ui.ammo_list.currentRow() - 1)

                if (self.ui.ammo_list.currentRow() == -1):
                    self.ui.ammo_list.setCurrentRow(0)

                self.ui.ammo_list.scrollToItem(self.ui.ammo_list.currentItem())

                self.ui.ammo_image.setPixmap(
                    description.pixmapAmmoDict.get(
                        self.ui.ammo_list.currentRow()))

                self.ui.ammo_weight_label.setText(
                    description.ammoDescriptionDict.get(
                        self.ui.ammo_list.currentRow())[0])
                self.ui.ammo_cost_label.setText(
                    description.ammoDescriptionDict.get(
                        self.ui.ammo_list.currentRow())[1])

            #/------------------------------------------------DATA--------------------------------------------------/
            elif (obj == self.ui.data_list) and (event.key() == 16777236):
                self.ui.data_list.setCurrentRow(
                    self.ui.data_list.currentRow() + 1)

                if (self.ui.data_list.currentRow() == -1):
                    self.ui.data_list.setCurrentRow(0)

                self.ui.data_list.scrollToItem(self.ui.data_list.currentItem())

                self.ui.data_description.setText(
                    description.questDescriptionDict.get(
                        self.ui.data_list.currentRow()))

                self.anim.stop()
                self.anim.setCurrentTime(0)
                self.anim.start()
            elif (obj == self.ui.data_list) and (event.key() == 16777234):
                self.ui.data_list.setCurrentRow(
                    self.ui.data_list.currentRow() - 1)

                if (self.ui.data_list.currentRow() == -1):
                    self.ui.data_list.setCurrentRow(0)

                self.ui.data_list.scrollToItem(self.ui.data_list.currentItem())

                self.ui.data_description.setText(
                    description.questDescriptionDict.get(
                        self.ui.data_list.currentRow()))

                self.anim.stop()
                self.anim.setCurrentTime(0)
                self.anim.start()

            #/------------------------------------------------RADIO--------------------------------------------------/
            elif (obj == self.ui.radio_list) and (event.key() == 16777236):
                self.ui.radio_list.setCurrentRow(
                    self.ui.radio_list.currentRow() + 1)

                if (self.ui.radio_list.currentRow() == -1):
                    self.ui.radio_list.setCurrentRow(0)

            elif (obj == self.ui.radio_list) and (event.key() == 16777234):
                self.ui.radio_list.setCurrentRow(
                    self.ui.radio_list.currentRow() - 1)

                if (self.ui.radio_list.currentRow() == -1):
                    self.ui.radio_list.setCurrentRow(0)

                #self.ui.data_description.setText(description.questDescriptionDict.get(self.ui.radio_list.currentRow()))
            elif (obj == self.ui.radio_list) and (event.key() == 47):
                if self.play:
                    self.player.stop()
                    self.play = False
                else:
                    self.player.setPlaylist(
                        self.playlistDict.get(self.ui.radio_list.currentRow()))
                    self.playListMohaveMusic.setCurrentIndex(
                        self.random.bounded(0, 9) + 1)
                    self.playlisNewVegas.setCurrentIndex(
                        self.random.bounded(0, 10) + 1)

                    self.player.play()
                    self.play = True
                return True
            else:
                #print(event.key())
                return True

        return super(MainWindow, self).eventFilter(obj, event)

    @Slot()
    def shift(self):
        print(self.ui.stat_tab.currentIndex())

        index = self.ui.stat_tab.currentIndex()
        if index == 0:
            self.ui.stat_tab.setGeometry(0, 0, 904, 380)
        elif index == 1:
            self.ui.stat_tab.setGeometry(-108, 0, 904, 380)
        elif index == 2:
            self.ui.stat_tab.setGeometry(-214, 0, 904, 380)
예제 #17
0
class EntityQGraphicsView(CustomQGraphicsView):
    """QGraphicsView for the Entity Graph View."""
    def __init__(self, parent):
        """

        Args:
            parent (QWidget): Graph View Form's (QMainWindow) central widget (self.centralwidget)
        """
        super().__init__(
            parent=parent)  # Parent is passed to QWidget's constructor
        self._spine_db_editor = None
        self._menu = QMenu(self)
        self._hovered_obj_item = None
        self.relationship_class = None
        self.cross_hairs_items = []

    def set_cross_hairs_items(self, relationship_class, cross_hairs_items):
        """Sets 'cross_hairs' items for relationship creation.

        Args:
            relationship_class (dict)
            cross_hairs_items (list(QGraphicsItems))
        """
        self.relationship_class = relationship_class
        self.cross_hairs_items = cross_hairs_items
        for item in cross_hairs_items:
            self.scene().addItem(item)
            item.apply_zoom(self.zoom_factor)
        cursor_pos = self.mapFromGlobal(QCursor.pos())
        self._update_cross_hairs_pos(cursor_pos)
        self.viewport().setCursor(Qt.BlankCursor)

    def clear_cross_hairs_items(self):
        self.relationship_class = None
        for item in self.cross_hairs_items:
            item.hide()
            item.scene().removeItem(item)
        self.cross_hairs_items.clear()
        self.viewport().unsetCursor()

    def connect_spine_db_editor(self, spine_db_editor):
        self._spine_db_editor = spine_db_editor
        self.create_context_menu()

    def create_context_menu(self):
        self._menu.addAction(
            self._spine_db_editor.ui.actionExport_graph_as_pdf)
        self._menu.addSeparator()
        for action in self._spine_db_editor.ui.menuGraph.actions():
            self._menu.addAction(action)

    def edit_selected(self):
        """Edits selected items using the connected Spine db editor."""
        self._spine_db_editor.edit_entity_graph_items()

    def remove_selected(self):
        """Removes selected items using the connected Spine db editor."""
        self._spine_db_editor.remove_entity_graph_items()

    def _cross_hairs_has_valid_taget(self):
        return (self._hovered_obj_item.db_map
                == self.cross_hairs_items[0].db_map
                and self._hovered_obj_item.entity_class_id
                in self.relationship_class["object_class_ids_to_go"])

    def mousePressEvent(self, event):
        """Handles relationship creation if one it's in process."""
        if not self.cross_hairs_items:
            super().mousePressEvent(event)
            return
        if event.buttons() & Qt.RightButton or not self._hovered_obj_item:
            self.clear_cross_hairs_items()
            return
        if self._cross_hairs_has_valid_taget():
            self.relationship_class["object_class_ids_to_go"].remove(
                self._hovered_obj_item.entity_class_id)
            if self.relationship_class["object_class_ids_to_go"]:
                # Add hovered as member and keep going, we're not done yet
                ch_rel_item = self.cross_hairs_items[1]
                ch_arc_item = CrossHairsArcItem(
                    ch_rel_item, self._hovered_obj_item,
                    self._spine_db_editor._ARC_WIDTH)
                ch_rel_item.refresh_icon()
                self.scene().addItem(ch_arc_item)
                ch_arc_item.apply_zoom(self.zoom_factor)
                self.cross_hairs_items.append(ch_arc_item)
                return
            # Here we're done, add the relationships between the hovered and the members
            ch_item, _, *ch_arc_items = self.cross_hairs_items
            obj_items = [arc_item.obj_item for arc_item in ch_arc_items]
            obj_items.remove(ch_item)
            self._spine_db_editor.finalize_relationship(
                self.relationship_class, self._hovered_obj_item, *obj_items)
            self.clear_cross_hairs_items()

    def mouseMoveEvent(self, event):
        """Updates the hovered object item if we're in relationship creation mode."""
        if not self.cross_hairs_items:
            super().mouseMoveEvent(event)
            return
        self._update_cross_hairs_pos(event.pos())

    def _update_cross_hairs_pos(self, pos):
        """Updates the hovered object item and sets the 'cross_hairs' icon accordingly.

        Args:
            pos (QPoint): the desired position in view coordinates
        """
        cross_hairs_item = self.cross_hairs_items[0]
        scene_pos = self.mapToScene(pos)
        delta = scene_pos - cross_hairs_item.scenePos()
        cross_hairs_item.block_move_by(delta.x(), delta.y())
        self._hovered_obj_item = None
        obj_items = [
            item for item in self.items(pos) if isinstance(item, ObjectItem)
        ]
        self._hovered_obj_item = next(iter(obj_items), None)
        if self._hovered_obj_item is not None:
            if self._cross_hairs_has_valid_taget():
                if len(self.relationship_class["object_class_ids_to_go"]) == 1:
                    self.cross_hairs_items[0].set_check_icon()
                else:
                    self.cross_hairs_items[0].set_plus_icon()
                return
            self.cross_hairs_items[0].set_ban_icon()
            return
        self.cross_hairs_items[0].set_normal_icon()

    def mouseReleaseEvent(self, event):
        if not self.cross_hairs_items:
            super().mouseReleaseEvent(event)

    def keyPressEvent(self, event):
        """Aborts relationship creation if user presses ESC."""
        super().keyPressEvent(event)
        if event.key() == Qt.Key_Escape and self.cross_hairs_items:
            self._spine_db_editor.msg.emit("Relationship creation aborted.")
            self.clear_cross_hairs_items()

    def contextMenuEvent(self, e):
        """Shows context menu.

        Args:
            e (QContextMenuEvent): Context menu event
        """
        super().contextMenuEvent(e)
        if e.isAccepted():
            return
        e.accept()
        self._spine_db_editor._handle_menu_graph_about_to_show()
        self._menu.exec_(e.globalPos())

    def _compute_min_zoom(self):
        return 0.5 * self.zoom_factor * self._items_fitting_zoom

    def _use_smooth_zoom(self):
        return self._qsettings.value("appSettings/smoothEntityGraphZoom",
                                     defaultValue="false") == "true"

    def _zoom(self, factor):
        self.scale(factor, factor)
        self.apply_zoom()

    def apply_zoom(self):
        for item in self.items():
            if hasattr(item, "apply_zoom"):
                item.apply_zoom(self.zoom_factor)

    def wheelEvent(self, event):
        """Zooms in/out. If user has pressed the shift key, rotates instead.

        Args:
            event (QWheelEvent): Mouse wheel event
        """
        if event.modifiers() != Qt.ShiftModifier:
            super().wheelEvent(event)
            return
        if event.orientation() != Qt.Vertical:
            event.ignore()
            return
        event.accept()
        smooth_rotation = self._qsettings.value(
            "appSettings/smoothEntityGraphRotation", defaultValue="false")
        if smooth_rotation == "true":
            num_degrees = event.delta() / 8
            num_steps = num_degrees / 15
            self._scheduled_transformations += num_steps
            if self._scheduled_transformations * num_steps < 0:
                self._scheduled_transformations = num_steps
            if self.time_line:
                self.time_line.deleteLater()
            self.time_line = QTimeLine(200, self)
            self.time_line.setUpdateInterval(20)
            self.time_line.valueChanged.connect(
                self._handle_rotation_time_line_advanced)
            self.time_line.finished.connect(
                self._handle_transformation_time_line_finished)
            self.time_line.start()
        else:
            angle = event.angleDelta().y() / 8
            self._rotate(angle)
            self._set_preferred_scene_rect()

    def _handle_rotation_time_line_advanced(self, pos):
        """Performs rotation whenever the smooth rotation time line advances."""
        angle = self._scheduled_transformations / 2.0
        self._rotate(angle)

    def _rotate(self, angle):
        center = self._get_viewport_scene_rect().center()
        for item in self.items():
            if hasattr(item, "apply_rotation"):
                item.apply_rotation(angle, center)

    def rotate_clockwise(self):
        """Performs a rotate clockwise with fixed angle."""
        self._rotate(-self._angle / 8)
        self._set_preferred_scene_rect()

    def rotate_anticlockwise(self):
        """Performs a rotate anticlockwise with fixed angle."""
        self._rotate(self._angle / 8)
        self._set_preferred_scene_rect()