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)
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)
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)
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)
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()
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()
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()
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()
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())
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()
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()
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)
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)
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()