def start(self): # Layout for single images self.single_image = QWidget() self.single_image_layout = QGridLayout() self.single_image_view = QGraphicsView() self.single_image_scene = QGraphicsScene() self.single_image_view.setScene(self.single_image_scene) self.single_image_layout.addWidget(self.single_image_view) self.single_image.setLayout(self.single_image_layout) # Layout for multiple images self.multiple_image = QWidget() self.multiple_image_view_layout = QVBoxLayout() self.multiple_image_layout = QGraphicsGridLayout() self.multiple_image_view = QGraphicsView() self.multiple_image_scene = QGraphicsScene() self.multiple_image_view.setScene(self.multiple_image_scene) self.panel = QGraphicsWidget() self.multiple_image_scene.addItem(self.panel) self.multiple_image_view_layout.addWidget(self.multiple_image_view) self.panel.setLayout(self.multiple_image_layout) self.multiple_image.setLayout(self.multiple_image_view_layout) self.addWidget(self.single_image) self.addWidget(self.multiple_image)
def testReimplementationTypes(self): w = QGraphicsWidget() # PYSIDE-86: # This case failed because GObjA was reimplementing # the method itemChange() from QGraphicsItem, # and then the QVariant was not associated with # a QGraphicsItem but a QObjectItem because the base # class was a QObject. gobjA = GObjA() gobjA.setParentItem(w) self.assertIs(type(w), type(gobjA.parentItem())) gobjB = GObjB() gobjB.setParentItem(w) self.assertIs(type(w), type(gobjB.parentItem()))
def __init__(self, params): super(NodeInstance, self).__init__() self.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemSendsScenePositionChanges) self.setAcceptHoverEvents(True) # GENERAL ATTRIBUTES # the constructor parameters are stored in a tuple to make the source code of custom NIs cleaner parent_node, flow, config = params self.parent_node = parent_node self.flow = flow self.movement_state = None self.movement_pos_from = None self.painted_once = False self.inputs = [] self.outputs = [] self.color = self.parent_node.color # manipulated by self.animator # self.node_instance_painter = NodeInstancePainter(self) self.default_actions = { 'remove': { 'method': self.action_remove }, 'update shape': { 'method': self.update_shape } } # for context menus self.special_actions = { } # only gets written in custom NodeInstance-subclasses self.personal_logs = [] # 'initializing' will be set to False below. It's needed for the ports setup, to prevent shape updating stuff self.initializing = True self.temp_state_data = None self.init_config = config # UI self.shadow_effect = None self.width = -1 self.height = -1 self.title_label = TitleLabel(self) self.animator = NodeInstanceAnimator(self) # needs self.title_label self.main_widget = None self.main_widget_proxy: FlowProxyWidget = None if self.parent_node.has_main_widget: self.main_widget = self.parent_node.main_widget_class(self) self.main_widget_proxy = FlowProxyWidget(self.flow) self.main_widget_proxy.setWidget(self.main_widget) # LOADING UI self.body_layout: QGraphicsLinearLayout = None self.inputs_layout: QGraphicsLinearLayout = None self.outputs_layout: QGraphicsLinearLayout = None self.layout: QGraphicsLinearLayout = self.setup_ui() self.widget = QGraphicsWidget(self) self.widget.setLayout(self.layout) # TOOLTIP if self.parent_node.description != '': self.setToolTip('<html><head/><body><p>' + self.parent_node.description + '</p></body></html>') self.setCursor(Qt.SizeAllCursor) # DESIGN THEME Design.flow_theme_changed.connect(self.theme_changed)
class NodeInstance(QGraphicsItem): def __init__(self, params): super(NodeInstance, self).__init__() self.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemSendsScenePositionChanges) self.setAcceptHoverEvents(True) # GENERAL ATTRIBUTES # the constructor parameters are stored in a tuple to make the source code of custom NIs cleaner parent_node, flow, config = params self.parent_node = parent_node self.flow = flow self.movement_state = None self.movement_pos_from = None self.painted_once = False self.inputs = [] self.outputs = [] self.color = self.parent_node.color # manipulated by self.animator # self.node_instance_painter = NodeInstancePainter(self) self.default_actions = { 'remove': { 'method': self.action_remove }, 'update shape': { 'method': self.update_shape } } # for context menus self.special_actions = { } # only gets written in custom NodeInstance-subclasses self.personal_logs = [] # 'initializing' will be set to False below. It's needed for the ports setup, to prevent shape updating stuff self.initializing = True self.temp_state_data = None self.init_config = config # UI self.shadow_effect = None self.width = -1 self.height = -1 self.title_label = TitleLabel(self) self.animator = NodeInstanceAnimator(self) # needs self.title_label self.main_widget = None self.main_widget_proxy: FlowProxyWidget = None if self.parent_node.has_main_widget: self.main_widget = self.parent_node.main_widget_class(self) self.main_widget_proxy = FlowProxyWidget(self.flow) self.main_widget_proxy.setWidget(self.main_widget) # LOADING UI self.body_layout: QGraphicsLinearLayout = None self.inputs_layout: QGraphicsLinearLayout = None self.outputs_layout: QGraphicsLinearLayout = None self.layout: QGraphicsLinearLayout = self.setup_ui() self.widget = QGraphicsWidget(self) self.widget.setLayout(self.layout) # TOOLTIP if self.parent_node.description != '': self.setToolTip('<html><head/><body><p>' + self.parent_node.description + '</p></body></html>') self.setCursor(Qt.SizeAllCursor) # DESIGN THEME Design.flow_theme_changed.connect(self.theme_changed) def initialized(self): """All ports and the main widget get finally created here.""" # LOADING CONFIG if self.init_config is not None: # self.setPos(config['position x'], config['position y']) self.setup_ports(self.init_config['inputs'], self.init_config['outputs']) if self.main_widget: try: self.main_widget.set_data( self.init_config['main widget data']) except Exception as e: print('Exception while setting data in', self.parent_node.title, 'NodeInstance\'s main widget:', e, ' (was this intended?)') self.special_actions = self.set_special_actions_data( self.init_config['special actions']) self.temp_state_data = self.init_config['state data'] else: self.setup_ports() # LOADING DATA if self.temp_state_data is not None: try: self.set_data(self.temp_state_data) except Exception as e: print('Exception while setting data in', self.parent_node.title, 'NodeInstance:', e, ' (was this intended?)') self.initializing = False # No self.update_shape() here because for some reason, the bounding rect hasn't been initialized yet, so # self.update_shape() gets called when the item is being drawn the first time (see paint event in NI painter) # TODO: change that ^ once there is a solution for this: https://forum.qt.io/topic/117179/force-qgraphicsitem-to-update-immediately-wait-for-update-event self.update_design() # load current design, update QGraphicsItem self.update() # and finally update the NodeInstance once def setup_ui(self): """Creates the empty layouts for the NI's widget.""" # main layout layout = QGraphicsLinearLayout(Qt.Vertical) layout.setSpacing(10) if self.parent_node.design_style == 'extended': layout.addItem(self.title_label) layout.setAlignment(self.title_label, Qt.AlignTop) # inputs self.inputs_layout = QGraphicsLinearLayout(Qt.Vertical) self.inputs_layout.setSpacing(2) # outputs self.outputs_layout = QGraphicsLinearLayout(Qt.Vertical) self.outputs_layout.setSpacing(2) # body self.body_layout = QGraphicsLinearLayout(Qt.Horizontal) self.body_layout.setSpacing(4) self.body_layout.addItem(self.inputs_layout) self.body_layout.setAlignment(self.inputs_layout, Qt.AlignVCenter | Qt.AlignLeft) self.body_layout.addStretch() self.body_layout.addItem(self.outputs_layout) self.body_layout.setAlignment(self.outputs_layout, Qt.AlignVCenter | Qt.AlignRight) if self.main_widget is not None: if self.parent_node.main_widget_pos == 'between ports': self.body_layout.insertItem(1, self.main_widget_proxy) self.body_layout.insertStretch(2) layout.addItem(self.body_layout) elif self.parent_node.main_widget_pos == 'under ports': layout.addItem(self.body_layout) layout.addItem(self.main_widget_proxy) layout.setAlignment(self.main_widget_proxy, Qt.AlignHCenter) else: layout.addItem(self.body_layout) return layout def rebuild_ui(self): """Due to some really strange and annoying behaviour of these QGraphicsWidgets, they don't want to shrink automatically when content is removed, they just stay large, even with a Minimum SizePolicy. I didn't find a way around that yet, so for now I have to recreate the whole layout and make sure the widget uses the smallest size possible.""" # if I don't manually remove the ports from the layouts, # they get deleted when setting the widget's layout to None below for inp in self.inputs: self.inputs_layout.removeAt(0) for out in self.outputs: self.outputs_layout.removeAt(0) self.layout = self.setup_ui() # recreate layout # forcefully making the widget shrink self.widget.setLayout(None) self.widget.resize(self.widget.minimumSize()) self.widget.setLayout(self.layout) # add inputs to new layout for inp in self.inputs: self.add_input_to_layout(inp) for out in self.outputs: self.add_output_to_layout(out) # __ _ __ __ # ____ _ / / ____ _ ____ _____ (_) / /_ / /_ ____ ___ # / __ `/ / / / __ `/ / __ \ / ___/ / / / __/ / __ \ / __ `__ \ # / /_/ / / / / /_/ / / /_/ / / / / / / /_ / / / / / / / / / / # \__,_/ /_/ \__, / \____/ /_/ /_/ \__/ /_/ /_/ /_/ /_/ /_/ # /____/ def update(self, input_called=-1, output_called=-1): """This is the method used to activate a NodeInstance. Note that this signature shadows the update() method from QGraphicsItem used to graphically update a QGraphicsItem which can be accessed via QGraphicsItem.update(self).""" if Design.animations_enabled: self.animator.start() Debugger.debug('update in', self.parent_node.title, 'on input', input_called) try: self.update_event(input_called) except Exception as e: Debugger.debug('EXCEPTION IN', self.parent_node.title, 'NI:', e) def update_event(self, input_called=-1): """Gets called when an input received a signal. This is where the magic begins in subclasses.""" pass def input(self, index): """Returns the value of a data input. If the input is connected, the value of the connected output is used: If not, the value of the widget is used.""" Debugger.debug('input called in', self.parent_node.title, 'NI:', index) return self.inputs[index].get_val() def exec_output(self, index): """Executes an execution output, sending a signal to all connected execution inputs causing the connected NIs to update.""" self.outputs[index].exec() def set_output_val(self, index, val): """Sets the value of a data output. self.data_outputs_updated() has to be called manually after all values are set.""" if not self.flow.viewport_update_mode.sync: # asynchronous viewport updates vp = self.flow.viewport() vp.repaint(self.flow.mapFromScene(self.sceneBoundingRect())) self.outputs[index].set_val(val) def data_outputs_updated(self): """(outdated!) Sends update signals to all data outputs causing connected NIs to update.""" Debugger.debug('updating data outputs in', self.parent_node.title) for o in self.outputs: if o.type_ == 'data': o.updated_val() Debugger.debug('data outputs in', self.parent_node.title, 'updated') def remove_event(self): """Method to stop all threads in hold of the NI itself.""" pass # _ # ____ _ ____ (_) # / __ `/ / __ \ / / # / /_/ / / /_/ / / / # \__,_/ / .___/ /_/ # /_/ # # all algorithm-unrelated api methods: # LOGGING def new_log(self, title): """Requesting a new personal Log. Handy method for subclasses.""" new_log = self.flow.parent_script.logger.new_log(self, title) self.personal_logs.append(new_log) return new_log def disable_personal_logs(self): """Disables personal Logs. They remain visible unless the user closes them via the appearing button.""" for log in self.personal_logs: log.disable() def enable_personal_logs(self): """Resets personal Logs to normal state (hiding close button, changing style sheet).""" for log in self.personal_logs: log.enable() def log_message(self, message: str, target='global'): """Access to global_tools Script Logs ('global' or 'error').""" self.flow.parent_script.logger.log_message(message, target) # SHAPE def update_shape(self): """Causes recompilation of the whole shape.""" # if not self.initializing: # just to make sure # self.rebuild_ui() # (hopefully) temporary fix -> see rebuild_ui() docstring if self.main_widget is not None: # maybe the main_widget got resized self.main_widget_proxy.setMaximumSize(self.main_widget.size()) self.widget.adjustSize() self.widget.adjustSize() self.body_layout.invalidate() self.layout.invalidate() self.layout.activate() # very essential; repositions everything in case content has changed (inputs/outputs/widget) if self.parent_node.design_style == 'minimalistic': # making it recompute its true minimumWidth here self.widget.adjustSize() if self.layout.minimumWidth() < self.title_label.width + 15: self.layout.setMinimumWidth(self.title_label.width + 15) self.layout.activate() self.width = self.boundingRect().width() self.height = self.boundingRect().height() rect = QRectF(QPointF(-self.width / 2, -self.height / 2), QPointF(self.width / 2, self.height / 2)) self.widget.setPos(rect.left(), rect.top()) if not self.parent_node.design_style == 'extended': self.title_label.setPos( QPointF(-self.title_label.boundingRect().width() / 2, -self.title_label.boundingRect().height() / 2)) self.flow.viewport().update() # PORTS def create_new_input(self, type_, label, widget_name=None, widget_pos='under', pos=-1, config=None): """Creates and adds a new input. Handy for subclasses.""" Debugger.debug('create_new_input called') pi = InputPortInstance(self, type_, label, config_data=config, widget_name=widget_name, widget_pos=widget_pos) if pos < -1: pos += len(self.inputs) if pos == -1: self.inputs.append(pi) self.add_input_to_layout(pi) else: self.inputs.insert(pos, pi) self.insert_input_into_layout(pos, pi) if not self.initializing: self.update_shape() self.update() def add_input_to_layout(self, i): if self.inputs_layout.count() > 0: self.inputs_layout.addStretch() self.inputs_layout.addItem(i) self.inputs_layout.setAlignment(i, Qt.AlignLeft) def insert_input_into_layout(self, index, i): self.inputs_layout.insertItem(index * 2, i) # *2 because of the stretches self.inputs_layout.setAlignment(i, Qt.AlignLeft) if len(self.inputs) > 1: self.inputs_layout.insertStretch( index * 2 + 1) # *2+1 because of the stretches, too def delete_input(self, i): """Disconnects and removes input. Handy for subclasses.""" inp: InputPortInstance = None if type(i) == int: inp = self.inputs[i] elif type(i) == InputPortInstance: inp = i for cpi in inp.connected_port_instances: self.flow.connect_gates(inp.gate, cpi.gate) # for some reason, I have to remove all widget items manually from the scene too. setting the items to # ownedByLayout(True) does not work, I don't know why. self.scene().removeItem(inp.gate) self.scene().removeItem(inp.label) if inp.proxy is not None: self.scene().removeItem(inp.proxy) self.inputs_layout.removeItem(inp) self.inputs.remove(inp) # just a temporary workaround for the issues discussed here: # https://forum.qt.io/topic/116268/qgraphicslayout-not-properly-resizing-to-change-of-content self.rebuild_ui() if not self.initializing: self.update_shape() self.update() def create_new_output(self, type_, label, pos=-1): """Creates and adds a new output. Handy for subclasses.""" pi = OutputPortInstance(self, type_, label) if pos < -1: pos += len(self.outputs) if pos == -1: self.outputs.append(pi) self.add_output_to_layout(pi) else: self.outputs.insert(pos, pi) self.insert_output_into_layout(pos, pi) if not self.initializing: self.update_shape() self.update() def add_output_to_layout(self, o): if self.outputs_layout.count() > 0: self.outputs_layout.addStretch() self.outputs_layout.addItem(o) self.outputs_layout.setAlignment(o, Qt.AlignRight) def insert_output_into_layout(self, index, o): self.outputs_layout.insertItem(index * 2, o) # *2 because of the stretches self.outputs_layout.setAlignment(o, Qt.AlignRight) if len(self.outputs) > 1: self.outputs_layout.insertStretch( index * 2 + 1) # *2+1 because of the stretches, too def delete_output(self, o): """Disconnects and removes output. Handy for subclasses.""" out: OutputPortInstance = None if type(o) == int: out = self.outputs[o] elif type(o) == OutputPortInstance: out = o for cpi in out.connected_port_instances: self.flow.connect_gates(out.gate, cpi.gate) # see delete_input() for info! self.scene().removeItem(out.gate) self.scene().removeItem(out.label) self.outputs_layout.removeItem(out) self.outputs.remove(out) # just a temporary workaround for the issues discussed here: # https://forum.qt.io/topic/116268/qgraphicslayout-not-properly-resizing-to-change-of-content self.rebuild_ui() if not self.initializing: self.update_shape() self.update() # GET, SET DATA def get_data(self): """ This method gets subclassed and specified. If the NI has states (so, the behavior depends on certain values), all these values must be stored in JSON-able format in a dict here. This dictionary will be used to reload the node's state when loading a project or pasting copied/cut nodes in the Flow (the states get copied too), see self.set_data(self, data) below. Unfortunately, I can't use pickle or something like that due to PySide2 which runs on C++, not Python. :return: Dictionary representing all values necessary to determine the NI's current state """ return {} def set_data(self, data): """ If the NI has states, it's state should get reloaded here according to what was previously provided by the same class in get_data(), see above. :param data: Dictionary representing all values necessary to determine the NI's current state """ pass @staticmethod def get_default_stylesheet(): """Handy method for subclasses to access the application window's stylesheet for UI content.""" return Design.ryven_stylesheet # VARIABLES def get_vars_handler(self): return self.flow.parent_script.variables_handler def get_var_val(self, name): return self.get_vars_handler().get_var_val(name) def set_var_val(self, name, val): return self.get_vars_handler().set_var(name, val) def register_var_receiver(self, name, method): self.get_vars_handler().register_receiver(self, name, method) def unregister_var_receiver(self, name): self.get_vars_handler().unregister_receiver(self, name) # -------------------------------------------------------------------------------------- # UI STUFF ---------------------------------------- def theme_changed(self, new_theme): self.title_label.theme_changed(new_theme) self.update_design() def update_design(self): """Loads the shadow effect option and causes redraw with active theme.""" if Design.node_instance_shadows_shown: self.shadow_effect = QGraphicsDropShadowEffect() self.shadow_effect.setXOffset(12) self.shadow_effect.setYOffset(12) self.shadow_effect.setBlurRadius(20) self.shadow_effect.setColor(QColor('#2b2b2b')) self.setGraphicsEffect(self.shadow_effect) else: self.setGraphicsEffect(None) self.title_label.update_design() # print(self.title_label.color) self.animator.reload_values() QGraphicsItem.update(self) def boundingRect(self): # remember: (0, 0) shall be the NI's center! rect = QRectF() w = self.layout.geometry().width() h = self.layout.geometry().height() rect.setLeft(-w / 2) rect.setTop(-h / 2) rect.setWidth(w) rect.setHeight(h) return rect # PAINTING def paint(self, painter, option, widget=None): """All painting is done by NodeInstancePainter""" # in order to access a meaningful geometry of GraphicsWidget contents in update_shape(), the paint event # has to be called once. See here: # https://forum.qt.io/topic/117179/force-qgraphicsitem-to-update-immediately-wait-for-update-event/4 if not self.painted_once: self.title_label.update_design() # also necessary self.update_shape() # self.node_instance_painter.paint(painter, option, self.color, self.width, self.height, self.boundingRect(), # Design.flow_theme, widget) Design.flow_theme.node_inst_painter.paint_NI( design_style=self.parent_node.design_style, painter=painter, option=option, c=self.color, w=self.width, h=self.height, bounding_rect=self.boundingRect(), title_rect=self.title_label.boundingRect()) self.painted_once = True def get_context_menu(self): menu = QMenu(self.flow) for a in self.get_actions(self.get_extended_default_actions(), menu): # menu needed for 'parent' if type(a) == NodeInstanceAction: menu.addAction(a) elif type(a) == QMenu: menu.addMenu(a) menu.addSeparator() actions = self.get_actions(self.special_actions, menu) for a in actions: # menu needed for 'parent' if type(a) == NodeInstanceAction: menu.addAction(a) elif type(a) == QMenu: menu.addMenu(a) return menu def itemChange(self, change, value): """This method ensures that all connections, selection borders etc. that get drawn in the Flow are constantly redrawn during a NI drag. Should get disabled when running in performance mode - not implemented yet.""" if change == QGraphicsItem.ItemPositionChange: if Design.performance_mode == 'pretty': self.flow.viewport().update() if self.movement_state == MovementEnum.mouse_clicked: self.movement_state = MovementEnum.position_changed return QGraphicsItem.itemChange(self, change, value) def hoverEnterEvent(self, event): self.title_label.set_NI_hover_state(hovering=True) QGraphicsItem.hoverEnterEvent(self, event) def hoverLeaveEvent(self, event): self.title_label.set_NI_hover_state(hovering=False) QGraphicsItem.hoverLeaveEvent(self, event) def mousePressEvent(self, event): """Used for Moving-Commands in Flow - may be replaced later with a nicer determination of a moving action.""" self.movement_state = MovementEnum.mouse_clicked self.movement_pos_from = self.pos() return QGraphicsItem.mousePressEvent(self, event) def mouseReleaseEvent(self, event): """Used for Moving-Commands in Flow - may be replaced later with a nicer determination of a moving action.""" if self.movement_state == MovementEnum.position_changed: self.flow.selected_components_moved(self.pos() - self.movement_pos_from) self.movement_state = None return QGraphicsItem.mouseReleaseEvent(self, event) # ACTIONS def get_extended_default_actions(self): actions_dict = self.default_actions.copy() for index in range(len(self.inputs)): inp = self.inputs[index] if inp.type_ == 'exec': actions_dict['exec input ' + str(index)] = { 'method': self.action_exec_input, 'data': { 'input index': index } } return actions_dict def action_exec_input(self, data): self.update(data['input index']) def get_actions(self, actions_dict, menu): actions = [] for k in actions_dict: v_dict = actions_dict[k] try: method = v_dict['method'] data = None try: data = v_dict['data'] except KeyError: pass action = NodeInstanceAction(k, menu, data) action.triggered_with_data.connect( method) # see NodeInstanceAction for explanation action.triggered_without_data.connect( method) # see NodeInstanceAction for explanation actions.append(action) except KeyError: action_menu = QMenu(k, menu) sub_actions = self.get_actions(v_dict, action_menu) for a in sub_actions: action_menu.addAction(a) actions.append(action_menu) return actions def action_remove(self): self.flow.remove_node_instance_triggered(self) def get_special_actions_data(self, actions): cleaned_actions = actions.copy() for key in cleaned_actions: v = cleaned_actions[key] if type(v) == M: # callable(v): cleaned_actions[key] = v.method_name elif callable(v): cleaned_actions[key] = v.__name__ elif type(v) == dict: cleaned_actions[key] = self.get_special_actions_data(v) else: cleaned_actions[key] = v return cleaned_actions def set_special_actions_data(self, actions_data): actions = {} for key in actions_data: if type(actions_data[key]) != dict: if key == 'method': try: actions['method'] = M(getattr(self, actions_data[key])) except AttributeError: # outdated method referenced pass elif key == 'data': actions['data'] = actions_data[key] else: actions[key] = self.set_special_actions_data(actions_data[key]) return actions # PORTS def setup_ports(self, inputs_config=None, outputs_config=None): if not inputs_config and not outputs_config: for i in range(len(self.parent_node.inputs)): inp = self.parent_node.inputs[i] self.create_new_input( inp.type_, inp.label, widget_name=self.parent_node.inputs[i].widget_name, widget_pos=self.parent_node.inputs[i].widget_pos) for o in range(len(self.parent_node.outputs)): out = self.parent_node.outputs[o] self.create_new_output(out.type_, out.label) else: # when loading saved NIs, the port instances might not be synchronised to the parent's ports anymore for inp in inputs_config: has_widget = inp['has widget'] self.create_new_input( inp['type'], inp['label'], widget_name=inp['widget name'] if has_widget else None, widget_pos=inp['widget position'] if has_widget else None, config=inp['widget data'] if has_widget else None) for out in outputs_config: self.create_new_output(out['type'], out['label']) def get_input_widget_class(self, widget_name): """Returns a reference to the widget class of a given name for instantiation.""" custom_node_input_widget_classes = self.flow.parent_script.main_window.custom_node_input_widget_classes widget_class = custom_node_input_widget_classes[ self.parent_node][widget_name] return widget_class def add_input_to_scene(self, i): self.flow.scene().addItem(i.gate) self.flow.scene().addItem(i.label) if i.widget: self.flow.scene().addItem(i.proxy) def del_and_remove_input_from_scene(self, i_index): i = self.inputs[i_index] for p in self.inputs[i_index].connected_port_instances: self.flow.connect_gates(i.gate, p.gate) self.flow.scene().removeItem(i.gate) self.flow.scene().removeItem(i.label) if i.widget: self.flow.scene().removeItem(i.proxy) i.widget.remove_event() self.inputs.remove(i) def add_output_to_scene(self, o): self.flow.scene().addItem(o.gate) self.flow.scene().addItem(o.label) def del_and_remove_output_from_scene(self, o_index): o = self.outputs[o_index] for p in self.outputs[o_index].connected_port_instances: self.flow.connect_gates(o.gate, p.gate) self.flow.scene().removeItem(o.gate) self.flow.scene().removeItem(o.label) self.outputs.remove(o) # GENERAL def about_to_remove_from_scene(self): """Called from Flow when the NI gets removed from the scene to stop all running threads and disable personal logs.""" if self.main_widget: self.main_widget.remove_event() self.remove_event() self.disable_personal_logs() def is_active(self): for i in self.inputs: if i.type_ == 'exec': return True for o in self.outputs: if o.type_ == 'exec': return True return False def has_main_widget(self): """Might be used later in CodePreview_Widget to enable not only showing the NI's class but also it's main_widget's class.""" return self.main_widget is not None def get_input_widgets(self): """Might be used later in CodePreview_Widget to enable not only showing the NI's class but its input widgets' classes.""" input_widgets = [] for i in range(len(self.inputs)): inp = self.inputs[i] if inp.widget is not None: input_widgets.append({i: inp.widget}) return input_widgets def get_json_data(self): """Returns all metadata of the NI including position, package etc. in a JSON-able dict format. Used to rebuild the Flow when loading a project.""" # general attributes node_instance_dict = { 'parent node title': self.parent_node.title, 'parent node type': self.parent_node.type_, 'parent node package': self.parent_node.package, 'parent node description': self.parent_node.description, 'position x': self.pos().x(), 'position y': self.pos().y() } if self.main_widget: node_instance_dict['main widget data'] = self.main_widget.get_data( ) node_instance_dict['state data'] = self.get_data() node_instance_dict['special actions'] = self.get_special_actions_data( self.special_actions) # inputs node_instance_inputs_list = [] for i in self.inputs: input_dict = i.get_json_data() node_instance_inputs_list.append(input_dict) node_instance_dict['inputs'] = node_instance_inputs_list # outputs node_instance_outputs_list = [] for o in self.outputs: output_dict = o.get_json_data() node_instance_outputs_list.append(output_dict) node_instance_dict['outputs'] = node_instance_outputs_list return node_instance_dict
class DisplayWidget(QStackedWidget): imageClicked = Signal(int, QEvent) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.max_columns = None # Set when presenter starts # Max cols for multimages self.cached_images = {} # Cache read of image from disk self.start() def emitter(self, index, event): self.imageClicked.emit(index, event) def start(self): # Layout for single images self.single_image = QWidget() self.single_image_layout = QGridLayout() self.single_image_view = QGraphicsView() self.single_image_scene = QGraphicsScene() self.single_image_view.setScene(self.single_image_scene) self.single_image_layout.addWidget(self.single_image_view) self.single_image.setLayout(self.single_image_layout) # Layout for multiple images self.multiple_image = QWidget() self.multiple_image_view_layout = QVBoxLayout() self.multiple_image_layout = QGraphicsGridLayout() self.multiple_image_view = QGraphicsView() self.multiple_image_scene = QGraphicsScene() self.multiple_image_view.setScene(self.multiple_image_scene) self.panel = QGraphicsWidget() self.multiple_image_scene.addItem(self.panel) self.multiple_image_view_layout.addWidget(self.multiple_image_view) self.panel.setLayout(self.multiple_image_layout) self.multiple_image.setLayout(self.multiple_image_view_layout) self.addWidget(self.single_image) self.addWidget(self.multiple_image) def setMaxColumns(self, max_cols): self.max_columns = max_cols def images_load(self, images): """ Take list of images and display in main window """ num = len(images) if num == 0: return width = self.width() maxcol = self.max_columns if num < maxcol: maxcol = num colwidth = width / maxcol # Set proper widget for display of multiple or single images if num > 1: self.setCurrentWidget(self.multiple_image) # Clear the layout while self.multiple_image_layout.count(): self.multiple_image_layout.removeAt(0) # Clear the scene for child in self.panel.childItems(): child.setParent(None) else: self.setCurrentWidget(self.single_image) self.single_image_scene.clear() self.single_image_scene.setSceneRect( self.single_image_scene.itemsBoundingRect()) # Display images or image row = 0 col = -1 for index, image in images.items(): col += 1 if col >= maxcol: col = 0 row += 1 # Used any cached reads if index not in self.cached_images.keys(): image_reader = QImageReader() image_reader.setDecideFormatFromContent(True) image_reader.setFileName(image.getFilename()) self.cached_images[index] = image_reader.read() cached_image = self.cached_images[index] pixmap = QPixmap(cached_image) if num > 1: pixmap = pixmap.scaledToWidth(colwidth - 20) rec = MultiImageWidget(pixmap, basename(image.getFilename()), index) rec.imageClicked.connect(self.emitter) self.multiple_image_layout.addItem(rec, row, col) else: self.single_image_scene.addPixmap(pixmap) adjusted = self.multiple_image_scene.itemsBoundingRect() adjusted.adjust(0, 0, 0, 8 * row) self.multiple_image_scene.setSceneRect(adjusted)
def __init__(self, parent_node: Node, flow, config=None): super(NodeInstance, self).__init__() self.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemSendsScenePositionChanges) self.setAcceptHoverEvents(True) # GENERAL ATTRIBUTES self.parent_node = parent_node self.flow = flow self.movement_state = None self.movement_pos_from = None self.inputs = [] self.outputs = [] self.default_actions = {'remove': {'method': self.action_remove, 'data': 123}, 'update shape': {'method': self.update_shape}} # for context menus self.special_actions = {} # only gets written in custom NodeInstance-subclasses self.personal_logs = [] # 'initializing' will be set to False below. It's needed for the ports setup, to prevent shape updating stuff self.initializing = True self.temp_state_data = None # UI self.width = -1 self.height = -1 self.title_label = TitleLabel(self) self.main_widget = None self.main_widget_proxy: FlowProxyWidget = None if self.parent_node.has_main_widget: self.main_widget = self.parent_node.main_widget_class(self) self.main_widget_proxy = FlowProxyWidget(self.flow) self.main_widget_proxy.setWidget(self.main_widget) self.body_layout: QGraphicsLinearLayout = None self.inputs_layout: QGraphicsLinearLayout = None self.outputs_layout: QGraphicsLinearLayout = None self.layout: QGraphicsLinearLayout = self.setup_ui() self.widget = QGraphicsWidget(self) self.widget.setLayout(self.layout) # LOADING CONFIG if config: # self.setPos(config['position x'], config['position y']) self.setup_ports(config['inputs'], config['outputs']) if self.main_widget: try: self.main_widget.set_data(config['main widget data']) except KeyError: pass self.special_actions = self.set_special_actions_data(config['special actions']) self.temp_state_data = config['state data'] else: self.setup_ports() # TOOLTIP if self.parent_node.description != '': self.setToolTip('<html><head/><body><p>'+self.parent_node.description+'</p></body></html>') self.setCursor(Qt.SizeAllCursor) self.initializing = False
class NodeInstance(QGraphicsItem): def __init__(self, parent_node: Node, flow, config=None): super(NodeInstance, self).__init__() self.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemSendsScenePositionChanges) self.setAcceptHoverEvents(True) # GENERAL ATTRIBUTES self.parent_node = parent_node self.flow = flow self.movement_state = None self.movement_pos_from = None self.inputs = [] self.outputs = [] self.default_actions = {'remove': {'method': self.action_remove, 'data': 123}, 'update shape': {'method': self.update_shape}} # for context menus self.special_actions = {} # only gets written in custom NodeInstance-subclasses self.personal_logs = [] # 'initializing' will be set to False below. It's needed for the ports setup, to prevent shape updating stuff self.initializing = True self.temp_state_data = None # UI self.width = -1 self.height = -1 self.title_label = TitleLabel(self) self.main_widget = None self.main_widget_proxy: FlowProxyWidget = None if self.parent_node.has_main_widget: self.main_widget = self.parent_node.main_widget_class(self) self.main_widget_proxy = FlowProxyWidget(self.flow) self.main_widget_proxy.setWidget(self.main_widget) self.body_layout: QGraphicsLinearLayout = None self.inputs_layout: QGraphicsLinearLayout = None self.outputs_layout: QGraphicsLinearLayout = None self.layout: QGraphicsLinearLayout = self.setup_ui() self.widget = QGraphicsWidget(self) self.widget.setLayout(self.layout) # LOADING CONFIG if config: # self.setPos(config['position x'], config['position y']) self.setup_ports(config['inputs'], config['outputs']) if self.main_widget: try: self.main_widget.set_data(config['main widget data']) except KeyError: pass self.special_actions = self.set_special_actions_data(config['special actions']) self.temp_state_data = config['state data'] else: self.setup_ports() # TOOLTIP if self.parent_node.description != '': self.setToolTip('<html><head/><body><p>'+self.parent_node.description+'</p></body></html>') self.setCursor(Qt.SizeAllCursor) self.initializing = False def setup_ui(self): """Creates the empty layouts for the NI's widget.""" # main layout layout = QGraphicsLinearLayout(Qt.Vertical) layout.setSpacing(5) if self.parent_node.design_style == 'extended': layout.addItem(self.title_label) layout.setAlignment(self.title_label, Qt.AlignTop) # inputs self.inputs_layout = QGraphicsLinearLayout(Qt.Vertical) self.inputs_layout.setSpacing(2) # outputs self.outputs_layout = QGraphicsLinearLayout(Qt.Vertical) self.outputs_layout.setSpacing(2) # body self.body_layout = QGraphicsLinearLayout(Qt.Horizontal) self.body_layout.setSpacing(4) self.body_layout.addItem(self.inputs_layout) self.body_layout.setAlignment(self.inputs_layout, Qt.AlignVCenter | Qt.AlignLeft) self.body_layout.addStretch() self.body_layout.addItem(self.outputs_layout) self.body_layout.setAlignment(self.outputs_layout, Qt.AlignVCenter | Qt.AlignRight) if self.main_widget is not None: if self.parent_node.main_widget_pos == 'between ports': self.body_layout.insertItem(1, self.main_widget_proxy) self.body_layout.insertStretch(2) layout.addItem(self.body_layout) elif self.parent_node.main_widget_pos == 'under ports': layout.addItem(self.body_layout) layout.addItem(self.main_widget_proxy) layout.setAlignment(self.main_widget_proxy, Qt.AlignHCenter) else: layout.addItem(self.body_layout) return layout def rebuild_ui(self): for inp in self.inputs: self.inputs_layout.removeAt(0) for out in self.outputs: self.outputs_layout.removeAt(0) self.layout = self.setup_ui() self.widget.setLayout(self.layout) # add inputs for inp in self.inputs: self.add_input_to_layout(inp) for out in self.outputs: self.add_output_to_layout(out) # __ _ __ __ # ____ _ / / ____ _ ____ _____ (_) / /_ / /_ ____ ___ # / __ `/ / / / __ `/ / __ \ / ___/ / / / __/ / __ \ / __ `__ \ # / /_/ / / / / /_/ / / /_/ / / / / / / /_ / / / / / / / / / / # \__,_/ /_/ \__, / \____/ /_/ /_/ \__/ /_/ /_/ /_/ /_/ /_/ # /____/ def update(self, input_called=-1, output_called=-1): Debugger.debug('update in', self.parent_node.title, 'on input', input_called) try: self.update_event(input_called) except Exception as e: Debugger.debug('EXCEPTION IN', self.parent_node.title, 'NI:', e) def update_event(self, input_called=-1): # API (gets overwritten) """Gets called when an input received a signal. This is where the magic begins in subclasses.""" pass def data_outputs_updated(self): """Sends update signals to all data outputs causing connected NIs to update.""" Debugger.debug('updating data outputs in', self.parent_node.title) for o in self.outputs: if o.type_ == 'data': o.updated_val() Debugger.debug('data outputs in', self.parent_node.title, 'updated') def input(self, index): # API """Returns the value of a data input. If the input is connected, the value of the connected output is used: If not, the value of the widget is used.""" Debugger.debug('input called in', self.parent_node.title, 'NI:', index) return self.inputs[index].get_val() def set_output_val(self, index, val): # API """Sets the value of a data output. self.data_outputs_updated() has to be called manually after all values are set.""" self.outputs[index].set_val(val) def exec_output(self, index): # API """Executes an execution output, sending a signal to all connected execution inputs causing the connected NIs to update.""" self.outputs[index].exec() def about_to_remove_from_scene(self): """Called from Flow when the NI gets removed from the scene to stop all running threads.""" if self.main_widget: self.main_widget.removing() self.removing() self.disable_personal_logs() def removing(self): # API (gets overwritten) """Method to stop all threads in hold of the NI itself.""" pass # _ # ____ _ ____ (_) # / __ `/ / __ \ / / # / /_/ / / /_/ / / / # \__,_/ / .___/ /_/ # /_/ # # There are methods in the 'algorithm' section that are part of the API too # LOGGING def new_log(self, title): """Requesting a new personal Log. Handy method for subclasses.""" new_log = self.flow.parent_script.logger.new_log(self, title) self.personal_logs.append(new_log) return new_log def disable_personal_logs(self): """Disables personal Logs. They remain visible unless the user closes them via the appearing button.""" for log in self.personal_logs: log.disable() def enable_personal_logs(self): """Resets personal Logs to normal state (hiding close button, changing style sheet).""" for log in self.personal_logs: log.enable() def log_message(self, message: str, target='global'): """Access to global_tools Script Logs ('global' or 'error').""" self.flow.parent_script.logger.log_message(message, target) # SHAPE def update_shape(self): """Causes recompilation of the whole shape.""" if self.main_widget is not None: # maybe the main_widget got resized self.main_widget_proxy.setMaximumSize(self.main_widget.size()) self.widget.adjustSize() self.layout.activate() self.layout.invalidate() # both very essential; repositions everything in case content has changed (inputs/outputs/widget) self.width = self.boundingRect().width() self.height = self.boundingRect().height() rect = QRectF(QPointF(-self.width/2, -self.height/2), QPointF(self.width/2, self.height/2)) self.widget.setPos(rect.left(), rect.top()) if not self.parent_node.design_style == 'extended': self.title_label.setPos(QPointF(-self.title_label.boundingRect().width()/2, -self.title_label.boundingRect().height()/2)) self.flow.viewport().update() # PORTS def create_new_input(self, type_, label, widget_type='', widget_name='', widget_pos='under', pos=-1): """Creates and adds a new input. Handy for subclasses.""" Debugger.debug('create_new_input called with widget pos:', widget_pos) pi = InputPortInstance(self, type_, label, widget_type=widget_type, widget_name=widget_name, widget_pos=widget_pos) if pos == -1: self.inputs.append(pi) self.add_input_to_layout(pi) else: self.inputs.insert(pos, pi) self.insert_input_into_layout(pos, pi) if not self.initializing: self.update_shape() def create_new_input_from_config(self, input_config): """Called only at NI creation.""" pi = InputPortInstance(self, configuration=input_config) self.inputs.append(pi) self.add_input_to_layout(pi) def add_input_to_layout(self, i): if self.inputs_layout.count() > 0: self.inputs_layout.addStretch() self.inputs_layout.addItem(i) self.inputs_layout.setAlignment(i, Qt.AlignLeft) def insert_input_into_layout(self, index, i): self.inputs_layout.insertItem(index, i) self.inputs_layout.setAlignment(i, Qt.AlignLeft) self.inputs_layout.addStretch() def delete_input(self, i): """Disconnects and removes input. Handy for subclasses.""" inp: InputPortInstance = None if type(i) == int: inp = self.inputs[i] elif type(i) == InputPortInstance: inp = i for cpi in inp.connected_port_instances: self.flow.connect_gates(inp.gate, cpi.gate) # for some reason, I have to remove all widget items manually from the scene too. setting the items to # ownedByLayout(True) does not work, I don't know why. self.scene().removeItem(inp.gate) self.scene().removeItem(inp.label) if inp.proxy is not None: self.scene().removeItem(inp.proxy) self.inputs_layout.removeItem(inp) self.inputs.remove(inp) # just a temporary workaround for the issues discussed here: # https://forum.qt.io/topic/116268/qgraphicslayout-not-properly-resizing-to-change-of-content self.rebuild_ui() if not self.initializing: self.update_shape() def create_new_output(self, type_, label, pos=-1): """Creates and adds a new output. Handy for subclasses.""" pi = OutputPortInstance(self, type_, label) if pos == -1: self.outputs.append(pi) self.add_output_to_layout(pi) else: self.outputs.insert(pos, pi) self.insert_output_into_layot(pos, pi) if not self.initializing: self.update_shape() def create_new_output_from_config(self, output_config=None): """Called only at NI creation.""" pi = OutputPortInstance(self, configuration=output_config) self.outputs.append(pi) self.add_output_to_layout(pi) def add_output_to_layout(self, o): if self.outputs_layout.count() > 0: self.outputs_layout.addStretch() self.outputs_layout.addItem(o) self.outputs_layout.setAlignment(o, Qt.AlignRight) def insert_output_into_layout(self, index, o): self.outputs_layout.insertItem(index, o) self.outputs_layout.setAlignment(o, Qt.AlignRight) self.outputs_layout.addStretch() def delete_output(self, o): """Disconnects and removes output. Handy for subclasses.""" out: OutputPortInstance = None if type(o) == int: out = self.outputs[o] elif type(o) == OutputPortInstance: out = o for cpi in out.connected_port_instances: self.flow.connect_gates(out.gate, cpi.gate) # see delete_input() for info! self.scene().removeItem(out.gate) self.scene().removeItem(out.label) self.outputs_layout.removeItem(out) self.outputs.remove(out) # just a temporary workaround for the issues discussed here: # https://forum.qt.io/topic/116268/qgraphicslayout-not-properly-resizing-to-change-of-content self.rebuild_ui() if not self.initializing: self.update_shape() # GET, SET DATA def get_data(self): """ IMPORTANT This method gets subclassed and specified. If the NI has states (so, the behavior depends on certain values), all these values must be stored in JSON-able format in a dict here. This dictionary will be used to reload the node's state when loading a project or pasting copied/cut nodes in the Flow (the states get copied too), see self.set_data(self, data) below. Unfortunately, I can't use pickle or something like that due to PySide2 which runs on C++, not Python. :return: Dictionary representing all values necessary to determine the NI's current state """ return {} def set_data(self, data): """ IMPORTANT If the NI has states, it's state should get reloaded here according to what was previously provided by the same class in get_data(), see above. :param data: Dictionary representing all values necessary to determine the NI's current state """ pass def get_default_stylesheet(self): """Handy method for subclasses to access the application window's stylesheet for UI content.""" return Design.ryven_stylesheet # -------------------------------------------------------------------------------------- # UI STUFF ---------------------------------------- def boundingRect(self): # remember: (0, 0) shall be the NI's center! rect = QRectF() w = self.layout.geometry().width() h = self.layout.geometry().height() rect.setLeft(-w/2) rect.setTop(-h/2) rect.setWidth(w) rect.setHeight(h) return rect # PAINTING def paint(self, painter, option, widget=None): # unfortunately, the boundingRect() is only not 0 when paint() is called the first time if self.width == -1 or self.height == -1: self.update_shape() painter.setRenderHint(QPainter.Antialiasing) brush = QBrush(QColor(100, 100, 100, 150)) # QBrush(QColor('#3B9CD9')) painter.setBrush(brush) if self.parent_node.design_style == 'extended': if Design.flow_style == 'dark std': self.draw_dark_extended_background(painter) elif Design.flow_style == 'dark tron': self.draw_tron_extended_background(painter) elif self.parent_node.design_style == 'minimalistic': if Design.flow_style == 'dark std': self.draw_dark_minimalistic(painter) elif Design.flow_style == 'dark tron': if option.state & QStyle.State_MouseOver: # use special dark background color when mouse hovers self.draw_tron_minimalistic(painter, background_color=self.parent_node.color.darker()) else: self.draw_tron_minimalistic(painter) def draw_dark_extended_background(self, painter): c = self.parent_node.color # main rect body_gradient = QRadialGradient(self.boundingRect().topLeft(), pythagoras(self.height, self.width)) body_gradient.setColorAt(0, QColor(c.red() / 10 + 100, c.green() / 10 + 100, c.blue() / 10 + 100, 200)) body_gradient.setColorAt(1, QColor(c.red() / 10 + 100, c.green() / 10 + 100, c.blue() / 10 + 100, 0)) painter.setBrush(body_gradient) painter.setPen(Qt.NoPen) painter.drawRoundedRect(self.boundingRect(), 12, 12) header_gradient = QLinearGradient(self.get_header_rect().topRight(), self.get_header_rect().bottomLeft()) header_gradient.setColorAt(0, QColor(c.red(), c.green(), c.blue(), 255)) header_gradient.setColorAt(1, QColor(c.red(), c.green(), c.blue(), 0)) painter.setBrush(header_gradient) painter.setPen(Qt.NoPen) painter.drawRoundedRect(self.get_header_rect(), 12, 12) def draw_tron_extended_background(self, painter): # main rect c = QColor('#212224') painter.setBrush(c) pen = QPen(self.parent_node.color) pen.setWidth(2) painter.setPen(pen) body_path = self.get_extended_body_path_TRON_DESIGN(10) painter.drawPath(body_path) # painter.drawRoundedRect(self.boundingRect(), 12, 12) c = self.parent_node.color header_gradient = QLinearGradient(self.get_header_rect().topRight(), self.get_header_rect().bottomLeft()) header_gradient.setColorAt(0, QColor(c.red(), c.green(), c.blue(), 255)) header_gradient.setColorAt(0.5, QColor(c.red(), c.green(), c.blue(), 100)) header_gradient.setColorAt(1, QColor(c.red(), c.green(), c.blue(), 0)) painter.setBrush(header_gradient) header_path = self.get_extended_header_path_TRON_DESIGN(10) painter.drawPath(header_path) def get_extended_body_path_TRON_DESIGN(self, corner_size): path = QPainterPath() path.moveTo(+self.width/2, -self.height/2+corner_size) path.lineTo(+self.width/2-corner_size, -self.height/2) path.lineTo(-self.width/2+corner_size, -self.height/2) path.lineTo(-self.width/2, -self.height/2+corner_size) path.lineTo(-self.width/2, +self.height/2-corner_size) path.lineTo(-self.width/2+corner_size, +self.height/2) path.lineTo(+self.width/2-corner_size, +self.height/2) path.lineTo(+self.width/2, +self.height/2-corner_size) path.closeSubpath() return path def get_extended_header_path_TRON_DESIGN(self, corner_size): header_height = 35 * (self.parent_node.title.count('\n')+1) header_bottom = -self.height/2+header_height path = QPainterPath() path.moveTo(+self.width/2, -self.height/2+corner_size) path.lineTo(+self.width/2-corner_size, -self.height/2) path.lineTo(-self.width/2+corner_size, -self.height/2) path.lineTo(-self.width/2, -self.height/2+corner_size) path.lineTo(-self.width/2, header_bottom-corner_size) path.lineTo(-self.width/2+corner_size, header_bottom) path.lineTo(+self.width/2-corner_size, header_bottom) path.lineTo(+self.width/2, header_bottom-corner_size) path.closeSubpath() return path def draw_dark_minimalistic(self, painter): path = QPainterPath() path.moveTo(-self.width / 2, 0) path.cubicTo(-self.width / 2, -self.height / 2, -self.width / 2, -self.height / 2, 0, -self.height / 2) path.cubicTo(+self.width / 2, -self.height / 2, +self.width / 2, -self.height / 2, +self.width / 2, 0) path.cubicTo(+self.width / 2, +self.height / 2, +self.width / 2, +self.height / 2, 0, +self.height / 2) path.cubicTo(-self.width / 2, +self.height / 2, -self.width / 2, +self.height / 2, -self.width / 2, 0) path.closeSubpath() c = self.parent_node.color body_gradient = QLinearGradient(self.boundingRect().bottomLeft(), self.boundingRect().topRight()) body_gradient.setColorAt(0, QColor(c.red(), c.green(), c.blue(), 150)) body_gradient.setColorAt(1, QColor(c.red(), c.green(), c.blue(), 80)) painter.setBrush(body_gradient) painter.setPen(QPen(QColor(30, 43, 48))) painter.drawPath(path) def draw_tron_minimalistic(self, painter, background_color=QColor('#36383B')): path = QPainterPath() path.moveTo(-self.width / 2, 0) corner_size = 10 path.lineTo(-self.width / 2 + corner_size / 2, -self.height / 2 + corner_size / 2) path.lineTo(0, -self.height / 2) path.lineTo(+self.width / 2 - corner_size / 2, -self.height / 2 + corner_size / 2) path.lineTo(+self.width / 2, 0) path.lineTo(+self.width / 2 - corner_size / 2, +self.height / 2 - corner_size / 2) path.lineTo(0, +self.height / 2) path.lineTo(-self.width / 2 + corner_size / 2, +self.height / 2 - corner_size / 2) path.closeSubpath() painter.setBrush(background_color) pen = QPen(self.parent_node.color) pen.setWidth(2) painter.setPen(pen) painter.drawPath(path) def get_header_rect(self): header_height = 1.4 * self.title_label.boundingRect().height() # 35 * (self.parent_node.title.count('\n')+1) header_rect = QRectF() header_rect.setTopLeft(QPointF(-self.width/2, -self.height/2)) header_rect.setWidth(self.width) header_rect.setHeight(header_height) return header_rect def get_context_menu(self): menu = QMenu(self.flow) for a in self.get_actions(self.get_extended_default_actions(), menu): # menu needed for 'parent' if type(a) == NodeInstanceAction: menu.addAction(a) elif type(a) == QMenu: menu.addMenu(a) menu.addSeparator() for a in self.get_actions(self.special_actions, menu): # menu needed for 'parent' if type(a) == NodeInstanceAction: menu.addAction(a) elif type(a) == QMenu: menu.addMenu(a) return menu def itemChange(self, change, value): """This method ensures that all connections, selection borders etc. that get drawn in the Flow are constantly redrawn during a NI drag. Should get disabled when running in performance mode - not implemented yet.""" if change == QGraphicsItem.ItemPositionChange: if PerformanceMode.mode == 'pretty': self.flow.viewport().update() if self.movement_state == MovementEnum.mouse_clicked: self.movement_state = MovementEnum.position_changed return QGraphicsItem.itemChange(self, change, value) def hoverEnterEvent(self, event): self.title_label.set_NI_hover_state(hovering=True) QGraphicsItem.hoverEnterEvent(self, event) def hoverLeaveEvent(self, event): self.title_label.set_NI_hover_state(hovering=False) QGraphicsItem.hoverLeaveEvent(self, event) def mousePressEvent(self, event): """Used for Moving-Commands in Flow - may be replaced later with a nicer determination of a moving action.""" self.movement_state = MovementEnum.mouse_clicked self.movement_pos_from = self.pos() return QGraphicsItem.mousePressEvent(self, event) def mouseReleaseEvent(self, event): """Used for Moving-Commands in Flow - may be replaced later with a nicer determination of a moving action.""" if self.movement_state == MovementEnum.position_changed: self.flow.selected_components_moved(self.pos()-self.movement_pos_from) self.movement_state = None return QGraphicsItem.mouseReleaseEvent(self, event) # ACTIONS def get_extended_default_actions(self): actions_dict = self.default_actions.copy() for index in range(len(self.inputs)): inp = self.inputs[index] if inp.type_ == 'exec': actions_dict['exec input '+str(index)] = {'method': self.action_exec_input, 'data': {'input index': index}} return actions_dict def action_exec_input(self, data): self.update(data['input index']) def get_actions(self, actions_dict, menu): actions = [] for k in actions_dict: v_dict = actions_dict[k] try: method = v_dict['method'] data = None try: data = v_dict['data'] except KeyError: pass action = NodeInstanceAction(k, menu, data) action.custom_triggered.connect(method) actions.append(action) except KeyError: action_menu = QMenu(k, menu) sub_actions = self.get_actions(v_dict, action_menu) for a in sub_actions: action_menu.addAction(a) actions.append(action_menu) return actions def action_remove(self, data): self.flow.remove_node_instance_triggered(self) def get_special_actions_data(self, actions): cleaned_actions = actions.copy() for key in cleaned_actions: v = cleaned_actions[key] if callable(v): cleaned_actions[key] = v.__name__ elif type(v) == dict: cleaned_actions[key] = self.get_special_actions_data(v) else: cleaned_actions[key] = v return cleaned_actions def set_special_actions_data(self, actions_data): actions = {} for key in actions_data: if type(actions_data[key]) != dict: try: # maybe the developer changed some special actions... actions[key] = getattr(self, actions_data[key]) except AttributeError: pass else: actions[key] = self.set_special_actions_data(actions_data[key]) return actions # PORTS def setup_ports(self, inputs_config=None, outputs_config=None): if not inputs_config and not outputs_config: for i in range(len(self.parent_node.inputs)): inp = self.parent_node.inputs[i] self.create_new_input(inp.type_, inp.label, widget_type=self.parent_node.inputs[i].widget_type, widget_name=self.parent_node.inputs[i].widget_name, widget_pos =self.parent_node.inputs[i].widget_pos) for o in range(len(self.parent_node.outputs)): out = self.parent_node.outputs[o] self.create_new_output(out.type_, out.label) else: # when loading saved NIs, the port instances might not be synchronised to the parent's ports anymore for i in range(len(inputs_config)): self.create_new_input_from_config(input_config=inputs_config[i]) for o in range(len(outputs_config)): self.create_new_output_from_config(output_config=outputs_config[o]) def get_input_widget_class(self, widget_name): """Returns a reference to the widget class of a given name for instantiation.""" custom_node_input_widget_classes = self.flow.parent_script.main_window.custom_node_input_widget_classes widget_class = custom_node_input_widget_classes[self.parent_node][widget_name] return widget_class def add_input_to_scene(self, i): self.flow.scene().addItem(i.gate) self.flow.scene().addItem(i.label) if i.widget: self.flow.scene().addItem(i.proxy) def del_and_remove_input_from_scene(self, i_index): i = self.inputs[i_index] for p in self.inputs[i_index].connected_port_instances: self.flow.connect_gates(i.gate, p.gate) self.flow.scene().removeItem(i.gate) self.flow.scene().removeItem(i.label) if i.widget: self.flow.scene().removeItem(i.proxy) i.widget.removing() self.inputs.remove(i) def add_output_to_scene(self, o): self.flow.scene().addItem(o.gate) self.flow.scene().addItem(o.label) def del_and_remove_output_from_scene(self, o_index): o = self.outputs[o_index] for p in self.outputs[o_index].connected_port_instances: self.flow.connect_gates(o.gate, p.gate) self.flow.scene().removeItem(o.gate) self.flow.scene().removeItem(o.label) self.outputs.remove(o) # GENERAL def initialized(self): """Gets called at the very end of all initialization processes/at the very end of the constructor.""" if self.temp_state_data is not None: self.set_data(self.temp_state_data) self.update() def is_active(self): for i in self.inputs: if i.type_ == 'exec': return True for o in self.outputs: if o.type_ == 'exec': return True return False def has_main_widget(self): """Might be used later in CodePreview_Widget to enable not only showing the NI's class but also it's main_widget's class.""" return self.main_widget is not None def get_input_widgets(self): """Might be used later in CodePreview_Widget to enable not only showing the NI's class but its input widgets' classes.""" input_widgets = [] for i in range(len(self.inputs)): inp = self.inputs[i] if inp.widget is not None: input_widgets.append({i: inp.widget}) return input_widgets def get_json_data(self): """Returns all metadata of the NI including position, package etc. in a JSON-able dict format. Used to rebuild the Flow when loading a project.""" # general attributes node_instance_dict = {'parent node title': self.parent_node.title, 'parent node type': self.parent_node.type_, 'parent node package': self.parent_node.package, 'parent node description': self.parent_node.description, 'position x': self.pos().x(), 'position y': self.pos().y()} if self.main_widget: node_instance_dict['main widget data'] = self.main_widget.get_data() node_instance_dict['state data'] = self.get_data() node_instance_dict['special actions'] = self.get_special_actions_data(self.special_actions) # inputs node_instance_inputs_list = [] for i in self.inputs: input_dict = i.get_json_data() node_instance_inputs_list.append(input_dict) node_instance_dict['inputs'] = node_instance_inputs_list # outputs node_instance_outputs_list = [] for o in self.outputs: output_dict = o.get_json_data() node_instance_outputs_list.append(output_dict) node_instance_dict['outputs'] = node_instance_outputs_list return node_instance_dict