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.main_widget = None self.main_widget_proxy: FlowProxyWidget = None self.default_actions = { 'remove': { 'method': self.action_remove, 'data': 123 }, 'compute shape': { 'method': self.compute_content_positions } } # for context menus self.gen_data_on_request = False self.personal_logs = [] self.special_actions = { } # only gets written in custom NodeInstance-subclasses - dynamic self.width = -1 self.height = -1 self.display_name_font = QFont('Poppins', 15) if parent_node.design_style == 'extended' else \ QFont('K2D', 20, QFont.Bold, True) self.display_name_FM = QFontMetricsF(self.display_name_font) # self.port_label_font = QFont("Source Code Pro", 10, QFont.Bold, True) # '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 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) self.main_widget_proxy.setWidget(self.main_widget) 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 self.setToolTip(self.parent_node.description) self.setCursor(Qt.SizeAllCursor) self.initializing = False # __ _ __ __ # ____ _ / / ____ _ ____ _____ (_) / /_ / /_ ____ ___ # / __ `/ / / / __ `/ / __ \ / ___/ / / / __/ / __ \ / __ `__ \ # / /_/ / / / / /_/ / / /_/ / / / / / / /_ / / / / / / / / / / # \__,_/ /_/ \__, / \____/ /_/ /_/ \__/ /_/ /_/ /_/ /_/ /_/ # /____/ 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_tools'): """Access to global_tools Script Logs ('global_tools' or 'error').""" self.flow.parent_script.logger.log_message(self, message, target) # SHAPE def update_shape(self): """Just a handy method for subclasses. Causes recompilation of the whole shape.""" self.compute_content_positions() 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 = PortInstance(self, 'input', type_, label, widget_type=widget_type, widget_name=widget_name, widget_pos=widget_pos) if pos == -1: self.inputs.append(pi) else: if pos == -1: self.inputs.insert(0, pi) else: self.inputs.insert(pos, pi) if self.scene(): self.add_input_to_scene(pi) if not self.initializing: self.update_shape() def create_new_input_from_config(self, input_config): """Called only at NI creation.""" pi = PortInstance(self, 'input', configuration=input_config) self.inputs.append(pi) def delete_input(self, i): """Disconnects and removes input. Handy for subclasses.""" if type(i) == int: self.del_and_remove_input_from_scene(i) elif type(i) == PortInstance: self.del_and_remove_input_from_scene(self.inputs.index(i)) 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 = PortInstance(self, 'output', type_, label) if pos == -1: self.outputs.append(pi) else: if pos == -1: self.outputs.insert(0, pi) else: self.outputs.insert(pos, pi) if self.scene(): self.add_output_to_scene(pi) if not self.initializing: self.update_shape() def create_new_output_from_config(self, output_config=None): """Called only at NI creation.""" pi = PortInstance(self, 'output', configuration=output_config) self.outputs.append(pi) def delete_output(self, o): """Disconnects and removes output. Handy for subclasses.""" if type(o) == int: self.del_and_remove_output_from_scene(o) else: self.del_and_remove_output_from_scene(self.outputs.index(o)) 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 # -------------------------------------------------------------------------------------- # UI STUFF ---------------------------------------- def boundingRect(self): return QRectF(-self.width / 2, -self.height / 2, self.width, self.height) # PAINTING def paint(self, painter, option, widget=None): painter.setRenderHint(QPainter.Antialiasing) brush = QBrush(QColor(100, 100, 100, 150)) # QBrush(QColor('#3B9CD9')) painter.setBrush(brush) std_pen = QPen( QColor(30, 43, 48) ) # QColor(30, 43, 48) # used for header title and minimal std dark border std_pen.setWidthF(1.5) # painter.setPen(std_pen) if self.parent_node.design_style == 'extended': header_pen = std_pen # differs from std_pen in tron design if Design.flow_style == 'dark std': self.draw_dark_extended_background(painter) if option.state & QStyle.State_MouseOver: # make title color white when mouse hovers c = self.parent_node.color.lighter() header_pen = QPen(c) header_pen.setWidth(2) elif Design.flow_style == 'dark tron': self.draw_tron_extended_background(painter) c = self.parent_node.color if option.state & QStyle.State_MouseOver: # make title color lighter when mouse hovers c = self.parent_node.color.lighter() header_pen = QPen(c) header_pen.setWidth(2) # HEADER painter.setFont(self.display_name_font) painter.setPen(header_pen) painter.drawText(self.get_title_rect(), Qt.AlignVCenter | Qt.AlignLeft, self.parent_node.title) painter.setBrush(Qt.NoBrush) painter.setPen(QPen(Qt.white, 1)) elif self.parent_node.design_style == 'minimalistic': if Design.flow_style == 'dark std': self.draw_dark_minimalistic(painter, std_pen) if option.state & QStyle.State_MouseOver: # make title color light when mouse hovers pen = QPen(self.parent_node.color.lighter()) painter.setPen(pen) 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) # HEADER painter.setFont(self.display_name_font) painter.drawText(self.boundingRect(), Qt.AlignCenter, self.parent_node.title) 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, pen): 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(pen) 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 = 35 * (self.parent_node.title.count('\n') + 1) header_rect = QRectF() header_rect.setTopLeft(QPointF(-self.width / 2, -self.height / 2)) header_rect.setRight(self.width / 2) header_rect.setBottom(-self.height / 2 + header_height) return header_rect def get_title_rect(self): title_rect_offset_factor = 0.56 header_rect = self.get_header_rect() rect = QRectF() rect.setTop(header_rect.top() + (header_rect.height() / 2) * (1 - title_rect_offset_factor)) rect.setLeft(header_rect.left() + 10) rect.setHeight(header_rect.height() * title_rect_offset_factor) w = header_rect.width() * title_rect_offset_factor title_width = self.display_name_FM.width( get_longest_line(self.parent_node.title)) rect.setWidth(w if w > title_width else title_width) return 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: # self.flow.viewport().update() if self.movement_state == MovementEnum.mouse_clicked: self.movement_state = MovementEnum.position_changed return QGraphicsItem.itemChange(self, change, value) 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) # # SHAPE def del_and_remove_content_from_scene(self): # everything get's reset here """OLD: SHOULD GET REMOVED, I THINK""" for i in range(len(self.inputs)): self.del_and_remove_input_from_scene(0) for o in range(len(self.outputs)): self.del_and_remove_output_from_scene(0) # lists are cleared here self.width = -1 self.height = -1 def compute_content_positions(self): """BAD - This might become unnecessary once I implemented use of QGraphicsLayout""" for i in self.inputs: i.compute_size_and_positions() for o in self.outputs: o.compute_size_and_positions() display_name_height = self.display_name_FM.height() * ( self.parent_node.title.count('\n') + 1) display_name_width = self.display_name_FM.width( get_longest_line(self.parent_node.title)) display_name_width_extended = self.display_name_FM.width( '__' + get_longest_line(self.parent_node.title) + '__') # label_FM = QFontMetricsF(self.port_label_font) # all sizes and buffers space_between_io = 10 # the following function creates additional space at the top and the bottom of the NI - the more ports, the more space left_largest_width = 0 right_largest_width = 0 height_buffer_between_ports = 0 #10 # adds vertical buffer between single ports horizontal_buffer_to_border = 10 # adds a little bit of space between port and border of the NI left_ports_edge_height = -height_buffer_between_ports right_ports_edge_height = -height_buffer_between_ports for i in self.inputs: if i.width > left_largest_width: left_largest_width = i.width left_ports_edge_height += i.height + height_buffer_between_ports for o in self.outputs: if o.width > right_largest_width: right_largest_width = o.width right_ports_edge_height += o.height + height_buffer_between_ports ports_edge_height = left_ports_edge_height if left_ports_edge_height > right_ports_edge_height else right_ports_edge_height ports_edge_width = left_largest_width + space_between_io + right_largest_width + 2 * horizontal_buffer_to_border body_height = 0 body_width = 0 body_top = 0 body_left = 0 body_right = 0 if self.parent_node.design_style == 'minimalistic': height_buffer = 10 body_height = ports_edge_height if ports_edge_height > display_name_height else display_name_height self.height = body_height + height_buffer self.width = display_name_width_extended if display_name_width_extended > ports_edge_width else ports_edge_width if self.main_widget: if self.parent_node.main_widget_pos == 'under ports': self.width = self.width if self.width > self.main_widget.width( ) + 2 * horizontal_buffer_to_border else self.main_widget.width( ) + 2 * horizontal_buffer_to_border self.height += self.main_widget.height( ) + height_buffer_between_ports elif self.parent_node.main_widget_pos == 'between ports': #self.width += self.main_widget.width() self.width = display_name_width_extended if \ display_name_width_extended > ports_edge_width+self.main_widget.width() else \ ports_edge_width+self.main_widget.width() self.height = self.height if self.height > self.main_widget.height( ) + height_buffer else self.main_widget.height( ) + height_buffer body_top = -self.height / 2 + height_buffer / 2 body_left = -self.width / 2 + horizontal_buffer_to_border body_right = self.width / 2 - horizontal_buffer_to_border elif self.parent_node.design_style == 'extended': header_height = self.get_header_rect().height( ) #50 * (self.parent_node.title.count('\n')+1) vertical_body_buffer = 16 # half above, half below body_height = ports_edge_height self.height = header_height + body_height + vertical_body_buffer self.width = display_name_width_extended if display_name_width_extended > ports_edge_width else ports_edge_width if self.main_widget: if self.parent_node.main_widget_pos == 'under ports': self.width = self.width if self.width > self.main_widget.width( ) + 2 * horizontal_buffer_to_border else self.main_widget.width( ) + 2 * horizontal_buffer_to_border self.height += self.main_widget.height( ) + height_buffer_between_ports elif self.parent_node.main_widget_pos == 'between ports': self.width = display_name_width_extended if \ display_name_width_extended > ports_edge_width+self.main_widget.width() else \ ports_edge_width+self.main_widget.width() self.height = self.height if self.height > self.main_widget.height() + header_height + vertical_body_buffer else \ self.main_widget.height() + header_height + vertical_body_buffer body_top = -self.height / 2 + header_height + vertical_body_buffer / 2 body_left = -self.width / 2 + horizontal_buffer_to_border body_right = self.width / 2 - horizontal_buffer_to_border # here, the width and height are final self.set_content_positions( body_height=body_height, body_top=body_top, body_left=body_left, body_right=body_right, left_ports_edge_height=left_ports_edge_height, right_ports_edge_height=right_ports_edge_height, height_buffer_between_ports=height_buffer_between_ports, left_largest_width=left_largest_width, right_largest_width=right_largest_width, space_between_io=space_between_io) def set_content_positions(self, body_height, body_top, body_left, body_right, left_ports_edge_height, right_ports_edge_height, height_buffer_between_ports, left_largest_width, right_largest_width, space_between_io): """BAD - This might become unnecessary once I implemented use of QGraphicsLayout""" # set positions # # calculating the vertical space between two inputs - without their heights, just between them space_between_inputs = (body_height - left_ports_edge_height) / ( len(self.inputs) - 1) if len( self.inputs) > 2 else body_height - left_ports_edge_height offset = 0 if len(self.inputs) == 1: offset = (body_height - left_ports_edge_height) / 2 for x in range(len(self.inputs)): i = self.inputs[x] y = body_top + i.height / 2 + offset port_pos_x = body_left + i.width / 2 port_pos_y = y i.gate.setPos(port_pos_x + i.gate.port_local_pos.x(), port_pos_y + i.gate.port_local_pos.y()) i.label.setPos(port_pos_x + i.label.port_local_pos.x(), port_pos_y + i.label.port_local_pos.y()) if i.widget: i.proxy.setPos( port_pos_x + i.widget.port_local_pos.x() - i.widget.width() / 2, port_pos_y + i.widget.port_local_pos.y() - i.widget.height() / 2) offset += i.height + height_buffer_between_ports + space_between_inputs space_between_outputs = (body_height - right_ports_edge_height) / ( len(self.outputs) - 1) if len( self.outputs) > 2 else body_height - right_ports_edge_height offset = 0 if len(self.outputs) == 1: offset = (body_height - right_ports_edge_height) / 2 for x in range(len(self.outputs)): o = self.outputs[x] y = body_top + o.height / 2 + offset port_pos_x = body_right - o.width / 2 port_pos_y = y o.gate.setPos(port_pos_x + o.gate.port_local_pos.x(), port_pos_y + o.gate.port_local_pos.y()) o.label.setPos(port_pos_x + o.label.port_local_pos.x(), port_pos_y + o.label.port_local_pos.y()) offset += o.height + height_buffer_between_ports + space_between_outputs if self.main_widget: if self.parent_node.main_widget_pos == 'under ports': self.main_widget_proxy.setPos( -self.main_widget.width() / 2, body_top + body_height + height_buffer_between_ports ) # self.height/2 - height_buffer/2 - self.main_widget.height()) elif self.parent_node.main_widget_pos == 'between ports': body_incl_widget_height = body_height if body_height > self.main_widget.height( ) else self.main_widget.height() self.main_widget_proxy.setPos( body_left + left_largest_width + space_between_io / 2, body_top + body_incl_widget_height / 2 - self.main_widget.height() / 2) # 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
class Flow(QGraphicsView): def __init__(self, main_window, parent_script, config=None): super(Flow, self).__init__() # SHORTCUTS place_new_node_shortcut = QShortcut(QKeySequence('Shift+P'), self) place_new_node_shortcut.activated.connect( self.place_new_node_by_shortcut) move_selected_nodes_left_shortcut = QShortcut( QKeySequence('Shift+Left'), self) move_selected_nodes_left_shortcut.activated.connect( self.move_selected_nodes_left) move_selected_nodes_up_shortcut = QShortcut(QKeySequence('Shift+Up'), self) move_selected_nodes_up_shortcut.activated.connect( self.move_selected_nodes_up) move_selected_nodes_right_shortcut = QShortcut( QKeySequence('Shift+Right'), self) move_selected_nodes_right_shortcut.activated.connect( self.move_selected_nodes_right) move_selected_nodes_down_shortcut = QShortcut( QKeySequence('Shift+Down'), self) move_selected_nodes_down_shortcut.activated.connect( self.move_selected_nodes_down) select_all_shortcut = QShortcut(QKeySequence('Ctrl+A'), self) select_all_shortcut.activated.connect(self.select_all) copy_shortcut = QShortcut(QKeySequence.Copy, self) copy_shortcut.activated.connect(self.copy) cut_shortcut = QShortcut(QKeySequence.Cut, self) cut_shortcut.activated.connect(self.cut) paste_shortcut = QShortcut(QKeySequence.Paste, self) paste_shortcut.activated.connect(self.paste) # UNDO/REDO self.undo_stack = QUndoStack(self) self.undo_action = self.undo_stack.createUndoAction(self, 'undo') self.undo_action.setShortcuts(QKeySequence.Undo) self.redo_action = self.undo_stack.createRedoAction(self, 'redo') self.redo_action.setShortcuts(QKeySequence.Redo) undo_shortcut = QShortcut(QKeySequence.Undo, self) undo_shortcut.activated.connect(self.undo_activated) redo_shortcut = QShortcut(QKeySequence.Redo, self) redo_shortcut.activated.connect(self.redo_activated) # GENERAL ATTRIBUTES self.parent_script = parent_script self.all_node_instances: [NodeInstance] = [] self.all_node_instance_classes = main_window.all_node_instance_classes # ref self.all_nodes = main_window.all_nodes # ref self.gate_selected: PortInstanceGate = None self.dragging_connection = False self.ignore_mouse_event = False # for stylus - see tablet event self.last_mouse_move_pos: QPointF = None self.node_place_pos = QPointF() self.left_mouse_pressed_in_flow = False self.mouse_press_pos: QPointF = None self.tablet_press_pos: QPointF = None self.auto_connection_gate = None # stores the gate that we may try to auto connect to a newly placed NI self.panning = False self.pan_last_x = None self.pan_last_y = None self.current_scale = 1 self.total_scale_div = 1 # SETTINGS self.algorithm_mode = Flow_AlgorithmMode() self.viewport_update_mode = Flow_ViewportUpdateMode() # CREATE UI scene = QGraphicsScene(self) scene.setItemIndexMethod(QGraphicsScene.NoIndex) scene.setSceneRect(0, 0, 10 * self.width(), 10 * self.height()) self.setScene(scene) self.setCacheMode(QGraphicsView.CacheBackground) self.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate) self.setRenderHint(QPainter.Antialiasing) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setDragMode(QGraphicsView.RubberBandDrag) scene.selectionChanged.connect(self.selection_changed) self.setAcceptDrops(True) self.centerOn( QPointF(self.viewport().width() / 2, self.viewport().height() / 2)) # NODE CHOICE WIDGET self.node_choice_proxy = FlowProxyWidget(self) self.node_choice_proxy.setZValue(1000) self.node_choice_widget = NodeChoiceWidget( self, main_window.all_nodes) # , main_window.node_images) self.node_choice_proxy.setWidget(self.node_choice_widget) self.scene().addItem(self.node_choice_proxy) self.hide_node_choice_widget() # ZOOM WIDGET self.zoom_proxy = FlowProxyWidget(self) self.zoom_proxy.setFlag(QGraphicsItem.ItemIgnoresTransformations, True) self.zoom_proxy.setZValue(1001) self.zoom_widget = FlowZoomWidget(self) self.zoom_proxy.setWidget(self.zoom_widget) self.scene().addItem(self.zoom_proxy) self.set_zoom_proxy_pos() # STYLUS self.stylus_mode = '' self.current_drawing = None self.drawing = False self.drawings = [] self.stylus_modes_proxy = FlowProxyWidget(self) self.stylus_modes_proxy.setFlag( QGraphicsItem.ItemIgnoresTransformations, True) self.stylus_modes_proxy.setZValue(1001) self.stylus_modes_widget = FlowStylusModesWidget(self) self.stylus_modes_proxy.setWidget(self.stylus_modes_widget) self.scene().addItem(self.stylus_modes_proxy) self.set_stylus_proxy_pos() self.setAttribute(Qt.WA_TabletTracking) # DESIGN THEME Design.flow_theme_changed.connect(self.theme_changed) if config: config: dict # algorithm mode if config.keys().__contains__('algorithm mode'): if config['algorithm mode'] == 'data flow': self.parent_script.widget.ui.algorithm_data_flow_radioButton.setChecked( True) self.algorithm_mode.mode_data_flow = True else: # 'exec flow' self.parent_script.widget.ui.algorithm_exec_flow_radioButton.setChecked( True) self.algorithm_mode.mode_data_flow = False # viewport update mode if config.keys().__contains__('viewport update mode'): if config['viewport update mode'] == 'sync': self.parent_script.widget.ui.viewport_update_mode_sync_radioButton.setChecked( True) self.viewport_update_mode.sync = True else: # 'async' self.parent_script.widget.ui.viewport_update_mode_async_radioButton.setChecked( True) self.viewport_update_mode.sync = False node_instances = self.place_nodes_from_config(config['nodes']) self.connect_nodes_from_config(node_instances, config['connections']) if list(config.keys()).__contains__( 'drawings' ): # not all (old) project files have drawings arr self.place_drawings_from_config(config['drawings']) self.undo_stack.clear() def theme_changed(self, t): self.viewport().update() def algorithm_mode_data_flow_toggled(self, checked): self.algorithm_mode.mode_data_flow = checked def viewport_update_mode_sync_toggled(self, checked): self.viewport_update_mode.sync = checked def selection_changed(self): selected_items = self.scene().selectedItems() selected_node_instances = list( filter(find_NI_in_object, selected_items)) if len(selected_node_instances) == 1: self.parent_script.show_NI_code(selected_node_instances[0]) elif len(selected_node_instances) == 0: self.parent_script.show_NI_code(None) def contextMenuEvent(self, event): QGraphicsView.contextMenuEvent(self, event) # in the case of the menu already being shown by a widget under the mouse, the event is accepted here if event.isAccepted(): return for i in self.items(event.pos()): if find_type_in_object(i, NodeInstance): ni: NodeInstance = i menu: QMenu = ni.get_context_menu() menu.exec_(event.globalPos()) event.accept() def undo_activated(self): """Triggered by ctrl+z""" self.undo_stack.undo() self.viewport().update() def redo_activated(self): """Triggered by ctrl+y""" self.undo_stack.redo() self.viewport().update() def mousePressEvent(self, event): Debugger.debug('mouse press event received, point:', event.pos()) # to catch tablet events (for some reason, it results in a mousePrEv too) if self.ignore_mouse_event: self.ignore_mouse_event = False return # there might be a proxy widget meant to receive the event instead of the flow QGraphicsView.mousePressEvent(self, event) # to catch any Proxy that received the event. Checking for event.isAccepted() or what is returned by # QGraphicsView.mousePressEvent(...) both didn't work so far, so I do it manually if self.ignore_mouse_event: self.ignore_mouse_event = False return if event.button() == Qt.LeftButton: if self.node_choice_proxy.isVisible(): self.hide_node_choice_widget() else: if find_type_in_object(self.itemAt(event.pos()), PortInstanceGate): self.gate_selected = self.itemAt(event.pos()) self.dragging_connection = True self.left_mouse_pressed_in_flow = True elif event.button() == Qt.RightButton: if len(self.items(event.pos())) == 0: self.node_choice_widget.reset_list() self.show_node_choice_widget(event.pos()) elif event.button() == Qt.MidButton: self.panning = True self.pan_last_x = event.x() self.pan_last_y = event.y() event.accept() self.mouse_press_pos = self.mapToScene(event.pos()) def mouseMoveEvent(self, event): QGraphicsView.mouseMoveEvent(self, event) if self.panning: # middle mouse pressed self.pan(event.pos()) event.accept() self.last_mouse_move_pos = self.mapToScene(event.pos()) if self.dragging_connection: self.viewport().repaint() def mouseReleaseEvent(self, event): # there might be a proxy widget meant to receive the event instead of the flow QGraphicsView.mouseReleaseEvent(self, event) if self.ignore_mouse_event or \ (event.button() == Qt.LeftButton and not self.left_mouse_pressed_in_flow): self.ignore_mouse_event = False return elif event.button() == Qt.MidButton: self.panning = False # connection dropped over specific gate if self.dragging_connection and self.itemAt(event.pos()) and \ find_type_in_object(self.itemAt(event.pos()), PortInstanceGate): self.connect_gates__cmd(self.gate_selected, self.itemAt(event.pos())) # connection dropped over NodeInstance - auto connect elif self.dragging_connection and find_type_in_objects( self.items(event.pos()), NodeInstance): # find node instance ni_under_drop = None for item in self.items(event.pos()): if find_type_in_object(item, NodeInstance): ni_under_drop = item break # connect self.try_conn_gate_and_ni(self.gate_selected, ni_under_drop) # connection dropped somewhere else - show node choice widget elif self.dragging_connection: self.auto_connection_gate = self.gate_selected self.show_node_choice_widget(event.pos()) self.left_mouse_pressed_in_flow = False self.dragging_connection = False self.gate_selected = None self.viewport().repaint() def keyPressEvent(self, event): QGraphicsView.keyPressEvent(self, event) if event.isAccepted(): return if event.key() == Qt.Key_Escape: # do I need that... ? self.clearFocus() self.setFocus() return True elif event.key() == Qt.Key_Delete: self.remove_selected_components() def wheelEvent(self, event): if event.modifiers() == Qt.CTRL and event.angleDelta().x() == 0: self.zoom(event.pos(), self.mapToScene(event.pos()), event.angleDelta().y()) event.accept() return True QGraphicsView.wheelEvent(self, event) def tabletEvent(self, event): """tabletEvent gets called by stylus operations. LeftButton: std, no button pressed RightButton: upper button pressed""" # if in edit mode and not panning or starting a pan, pass on to std mouseEvent handlers above if self.stylus_mode == 'edit' and not self.panning and not \ (event.type() == QTabletEvent.TabletPress and event.button() == Qt.RightButton): return # let the mousePress/Move/Release-Events handle it if event.type() == QTabletEvent.TabletPress: self.tablet_press_pos = event.pos() self.ignore_mouse_event = True if event.button() == Qt.LeftButton: if self.stylus_mode == 'comment': new_drawing = self.create_and_place_drawing__cmd( self.mapToScene(self.tablet_press_pos), config=self.stylus_modes_widget.get_pen_settings()) self.current_drawing = new_drawing self.drawing = True elif event.button() == Qt.RightButton: self.panning = True self.pan_last_x = event.x() self.pan_last_y = event.y() elif event.type() == QTabletEvent.TabletMove: self.ignore_mouse_event = True if self.panning: self.pan(event.pos()) elif event.pointerType() == QTabletEvent.Eraser: if self.stylus_mode == 'comment': for i in self.items(event.pos()): if find_type_in_object(i, DrawingObject): self.remove_drawing(i) break elif self.stylus_mode == 'comment' and self.drawing: mapped = self.mapToScene( QPoint(event.posF().x(), event.posF().y())) # rest = QPointF(event.posF().x()%1, event.posF().y()%1) # exact = QPointF(mapped.x()+rest.x()%1, mapped.y()+rest.y()%1) # TODO: use exact position (event.posF() ). Problem: mapToScene() only uses QPoint, not QPointF. The # calculation above didn't work if self.current_drawing.try_to_append_point(mapped): self.current_drawing.stroke_weights.append( event.pressure()) self.current_drawing.update() self.viewport().update() elif event.type() == QTabletEvent.TabletRelease: if self.panning: self.panning = False if self.stylus_mode == 'comment' and self.drawing: Debugger.debug('drawing obj finished') self.current_drawing.finished() self.current_drawing = None self.drawing = False def dragEnterEvent(self, event): if event.mimeData().hasFormat('text/plain'): event.acceptProposedAction() def dragMoveEvent(self, event): if event.mimeData().hasFormat('text/plain'): event.acceptProposedAction() def dropEvent(self, event): text = event.mimeData().text() item: QListWidgetItem = event.mimeData() Debugger.debug('drop received in Flow:', text) j_obj = None type = '' try: j_obj = json.loads(text) type = j_obj['type'] except Exception: return if type == 'variable': self.show_node_choice_widget( event.pos(), # only show get_var and set_var nodes [ n for n in self.all_nodes if find_type_in_object(n, GetVariable_Node) or find_type_in_object(n, SetVariable_Node) ]) def drawBackground(self, painter, rect): painter.fillRect(rect.intersected(self.sceneRect()), QColor('#333333')) painter.setPen(Qt.NoPen) painter.drawRect(self.sceneRect()) self.set_stylus_proxy_pos( ) # has to be called here instead of in drawForeground to prevent lagging self.set_zoom_proxy_pos() def drawForeground(self, painter, rect): """Draws all connections and borders around selected items.""" pen = QPen() if Design.flow_theme == 'dark std': # pen.setColor('#BCBBF2') pen.setWidth(5) pen.setCapStyle(Qt.RoundCap) elif Design.flow_theme == 'dark tron': # pen.setColor('#452666') pen.setWidth(4) pen.setCapStyle(Qt.RoundCap) elif Design.flow_theme == 'ghostly' or Design.flow_theme == 'blender': pen.setWidth(2) pen.setCapStyle(Qt.RoundCap) # DRAW CONNECTIONS for ni in self.all_node_instances: for o in ni.outputs: for cpi in o.connected_port_instances: if o.type_ == 'data': pen.setStyle(Qt.DashLine) elif o.type_ == 'exec': pen.setStyle(Qt.SolidLine) path = self.connection_path( o.gate.get_scene_center_pos(), cpi.gate.get_scene_center_pos()) w = path.boundingRect().width() h = path.boundingRect().height() gradient = QRadialGradient(path.boundingRect().center(), pythagoras(w, h) / 2) r = 0 g = 0 b = 0 if Design.flow_theme == 'dark std': r = 188 g = 187 b = 242 elif Design.flow_theme == 'dark tron': r = 0 g = 120 b = 180 elif Design.flow_theme == 'ghostly' or Design.flow_theme == 'blender': r = 0 g = 17 b = 25 gradient.setColorAt(0.0, QColor(r, g, b, 255)) gradient.setColorAt(0.75, QColor(r, g, b, 200)) gradient.setColorAt(0.95, QColor(r, g, b, 0)) gradient.setColorAt(1.0, QColor(r, g, b, 0)) pen.setBrush(gradient) painter.setPen(pen) painter.drawPath(path) # DRAW CURRENTLY DRAGGED CONNECTION if self.dragging_connection: pen = QPen('#101520') pen.setWidth(3) pen.setStyle(Qt.DotLine) painter.setPen(pen) gate_pos = self.gate_selected.get_scene_center_pos() if self.gate_selected.parent_port_instance.direction == 'output': painter.drawPath( self.connection_path(gate_pos, self.last_mouse_move_pos)) else: painter.drawPath( self.connection_path(self.last_mouse_move_pos, gate_pos)) # DRAW SELECTED NIs BORDER for ni in self.selected_node_instances(): pen = QPen(QColor('#245d75')) pen.setWidth(3) painter.setPen(pen) painter.setBrush(Qt.NoBrush) size_factor = 1.2 x = ni.pos().x() - ni.boundingRect().width() / 2 * size_factor y = ni.pos().y() - ni.boundingRect().height() / 2 * size_factor w = ni.boundingRect().width() * size_factor h = ni.boundingRect().height() * size_factor painter.drawRoundedRect(x, y, w, h, 10, 10) # DRAW SELECTED DRAWINGS BORDER for p_o in self.selected_drawings(): pen = QPen(QColor('#a3cc3b')) pen.setWidth(2) painter.setPen(pen) painter.setBrush(Qt.NoBrush) size_factor = 1.05 x = p_o.pos().x() - p_o.width / 2 * size_factor y = p_o.pos().y() - p_o.height / 2 * size_factor w = p_o.width * size_factor h = p_o.height * size_factor painter.drawRoundedRect(x, y, w, h, 6, 6) painter.drawEllipse(p_o.pos().x(), p_o.pos().y(), 2, 2) def get_viewport_img(self): self.hide_proxies() img = QImage(self.viewport().rect().width(), self.viewport().height(), QImage.Format_ARGB32) img.fill(Qt.transparent) painter = QPainter(img) painter.setRenderHint(QPainter.Antialiasing) self.render(painter, self.viewport().rect(), self.viewport().rect()) self.show_proxies() return img def get_whole_scene_img(self): self.hide_proxies() img = QImage(self.sceneRect().width() / self.total_scale_div, self.sceneRect().height() / self.total_scale_div, QImage.Format_RGB32) img.fill(Qt.transparent) painter = QPainter(img) painter.setRenderHint(QPainter.Antialiasing) rect = QRectF() rect.setLeft(-self.viewport().pos().x()) rect.setTop(-self.viewport().pos().y()) rect.setWidth(img.rect().width()) rect.setHeight(img.rect().height()) # rect is right... but it only renders from the viewport's point down-and rightwards, not from topleft (0,0) ... self.render(painter, rect, rect.toRect()) self.show_proxies() return img # PROXY POSITIONS def set_zoom_proxy_pos(self): self.zoom_proxy.setPos( self.mapToScene(self.viewport().width() - self.zoom_widget.width(), 0)) def set_stylus_proxy_pos(self): self.stylus_modes_proxy.setPos( self.mapToScene( self.viewport().width() - self.stylus_modes_widget.width() - self.zoom_widget.width(), 0)) def hide_proxies(self): self.stylus_modes_proxy.hide() self.zoom_proxy.hide() def show_proxies(self): self.stylus_modes_proxy.show() self.zoom_proxy.show() # NODE CHOICE WIDGET def show_node_choice_widget(self, pos, nodes=None): """Opens the node choice dialog in the scene.""" # calculating position self.node_place_pos = self.mapToScene(pos) dialog_pos = QPoint(pos.x() + 1, pos.y() + 1) # ensure that the node_choice_widget stays in the viewport if dialog_pos.x() + self.node_choice_widget.width( ) / self.total_scale_div > self.viewport().width(): dialog_pos.setX(dialog_pos.x() - (dialog_pos.x() + self.node_choice_widget.width() / self.total_scale_div - self.viewport().width())) if dialog_pos.y() + self.node_choice_widget.height( ) / self.total_scale_div > self.viewport().height(): dialog_pos.setY(dialog_pos.y() - (dialog_pos.y() + self.node_choice_widget.height() / self.total_scale_div - self.viewport().height())) dialog_pos = self.mapToScene(dialog_pos) # open nodes dialog # the dialog emits 'node_chosen' which is connected to self.place_node, # so this all continues at self.place_node below self.node_choice_widget.update_list( nodes if nodes is not None else self.all_nodes) self.node_choice_widget.update_view() self.node_choice_proxy.setPos(dialog_pos) self.node_choice_proxy.show() self.node_choice_widget.refocus() def hide_node_choice_widget(self): self.node_choice_proxy.hide() self.node_choice_widget.clearFocus() self.auto_connection_gate = None # PAN def pan(self, new_pos): self.horizontalScrollBar().setValue( self.horizontalScrollBar().value() - (new_pos.x() - self.pan_last_x)) self.verticalScrollBar().setValue(self.verticalScrollBar().value() - (new_pos.y() - self.pan_last_y)) self.pan_last_x = new_pos.x() self.pan_last_y = new_pos.y() # ZOOM def zoom_in(self, amount): local_viewport_center = QPoint(self.viewport().width() / 2, self.viewport().height() / 2) self.zoom(local_viewport_center, self.mapToScene(local_viewport_center), amount) def zoom_out(self, amount): local_viewport_center = QPoint(self.viewport().width() / 2, self.viewport().height() / 2) self.zoom(local_viewport_center, self.mapToScene(local_viewport_center), -amount) def zoom(self, p_abs, p_mapped, angle): by = 0 velocity = 2 * (1 / self.current_scale) + 0.5 if velocity > 3: velocity = 3 direction = '' if angle > 0: by = 1 + (angle / 360 * 0.1 * velocity) direction = 'in' elif angle < 0: by = 1 - (-angle / 360 * 0.1 * velocity) direction = 'out' else: by = 1 scene_rect_width = self.mapFromScene( self.sceneRect()).boundingRect().width() scene_rect_height = self.mapFromScene( self.sceneRect()).boundingRect().height() if direction == 'in': if self.current_scale * by < 3: self.scale(by, by) self.current_scale *= by elif direction == 'out': if scene_rect_width * by >= self.viewport().size().width( ) and scene_rect_height * by >= self.viewport().size().height(): self.scale(by, by) self.current_scale *= by w = self.viewport().width() h = self.viewport().height() wf = self.mapToScene(QPoint(w - 1, 0)).x() - self.mapToScene( QPoint(0, 0)).x() hf = self.mapToScene(QPoint(0, h - 1)).y() - self.mapToScene( QPoint(0, 0)).y() lf = p_mapped.x() - p_abs.x() * wf / w tf = p_mapped.y() - p_abs.y() * hf / h self.ensureVisible(lf, tf, wf, hf, 0, 0) target_rect = QRectF(QPointF(lf, tf), QSizeF(wf, hf)) self.total_scale_div = target_rect.width() / self.viewport().width() self.ensureVisible(target_rect, 0, 0) # NODE PLACING: ----- def create_node_instance(self, node, config): return self.get_node_instance_class_from_node(node)(node, self, config) def add_node_instance(self, ni, pos=None): self.scene().addItem(ni) ni.enable_personal_logs() if pos: ni.setPos(pos) # select new NI self.scene().clearSelection() ni.setSelected(True) self.all_node_instances.append(ni) def add_node_instances(self, node_instances): for ni in node_instances: self.add_node_instance(ni) def remove_node_instance(self, ni): ni.about_to_remove_from_scene() # to stop running threads self.scene().removeItem(ni) self.all_node_instances.remove(ni) def place_new_node_by_shortcut(self): # Shift+P point_in_viewport = None selected_NIs = self.selected_node_instances() if len(selected_NIs) > 0: x = selected_NIs[-1].pos().x() + 150 y = selected_NIs[-1].pos().y() self.node_place_pos = QPointF(x, y) point_in_viewport = self.mapFromScene(QPoint(x, y)) else: # place in center viewport_x = self.viewport().width() / 2 viewport_y = self.viewport().height() / 2 point_in_viewport = QPointF(viewport_x, viewport_y).toPoint() self.node_place_pos = self.mapToScene(point_in_viewport) self.node_choice_widget.reset_list() self.show_node_choice_widget(point_in_viewport) def place_nodes_from_config(self, nodes_config, offset_pos: QPoint = QPoint(0, 0)): new_node_instances = [] for n_c in nodes_config: # find parent node by title, type, package name and description as identifiers parent_node_title = n_c['parent node title'] parent_node_package_name = n_c['parent node package'] parent_node = None for pn in self.all_nodes: pn: Node = pn if pn.title == parent_node_title and \ pn.package == parent_node_package_name: parent_node = pn break new_NI = self.create_node_instance(parent_node, n_c) self.add_node_instance( new_NI, QPoint(n_c['position x'], n_c['position y']) + offset_pos) new_node_instances.append(new_NI) return new_node_instances def place_node__cmd(self, node: Node, config=None): new_NI = self.create_node_instance(node, config) place_command = PlaceNodeInstanceInScene_Command( self, new_NI, self.node_place_pos) self.undo_stack.push(place_command) if self.auto_connection_gate: self.try_conn_gate_and_ni(self.auto_connection_gate, place_command.node_instance) return place_command.node_instance def remove_node_instance_triggered( self, node_instance): # called from context menu of NodeInstance if node_instance in self.selected_node_instances(): self.undo_stack.push( RemoveComponents_Command(self, self.scene().selectedItems())) else: self.undo_stack.push( RemoveComponents_Command(self, [node_instance])) def get_node_instance_class_from_node(self, node): return self.all_node_instance_classes[node] def get_custom_input_widget_classes(self): return self.parent_script.main_window.custom_node_input_widget_classes def connect_nodes_from_config(self, node_instances, connections_config): for c in connections_config: c_parent_node_instance_index = c['parent node instance index'] c_output_port_index = c['output port index'] c_connected_node_instance = c['connected node instance'] c_connected_input_port_index = c['connected input port index'] if c_connected_node_instance is not None: # which can be the case when pasting parent_node_instance = node_instances[ c_parent_node_instance_index] connected_node_instance = node_instances[ c_connected_node_instance] self.connect_gates( parent_node_instance.outputs[c_output_port_index].gate, connected_node_instance. inputs[c_connected_input_port_index].gate) # DRAWINGS def create_drawing(self, config=None): new_drawing = DrawingObject(self, config) return new_drawing def add_drawing(self, drawing_obj, pos=None): self.scene().addItem(drawing_obj) if pos: drawing_obj.setPos(pos) self.drawings.append(drawing_obj) def add_drawings(self, drawings): for d in drawings: self.add_drawing(d) def remove_drawing(self, drawing): self.scene().removeItem(drawing) self.drawings.remove(drawing) def place_drawings_from_config(self, drawings, offset_pos=QPoint(0, 0)): """ :param offset_pos: position difference between the center of all selected items when they were copied/cut and the current mouse pos which is supposed to be the new center :param drawings: the drawing objects """ new_drawings = [] for d_config in drawings: x = d_config['pos x'] + offset_pos.x() y = d_config['pos y'] + offset_pos.y() new_drawing = self.create_drawing(config=d_config) self.add_drawing(new_drawing, QPointF(x, y)) new_drawings.append(new_drawing) return new_drawings def create_and_place_drawing__cmd(self, pos, config=None): new_drawing_obj = self.create_drawing(config) place_command = PlaceDrawingObject_Command(self, pos, new_drawing_obj) self.undo_stack.push(place_command) return new_drawing_obj def move_selected_copmonents__cmd(self, x, y): new_rel_pos = QPointF(x, y) # if one node item would leave the scene (f.ex. pos.x < 0), stop left = False for i in self.scene().selectedItems(): new_pos = i.pos() + new_rel_pos if new_pos.x() - i.width / 2 < 0 or \ new_pos.x() + i.width / 2 > self.scene().width() or \ new_pos.y() - i.height / 2 < 0 or \ new_pos.y() + i.height / 2 > self.scene().height(): left = True break if not left: # moving the items items_group = self.scene().createItemGroup( self.scene().selectedItems()) items_group.moveBy(new_rel_pos.x(), new_rel_pos.y()) self.scene().destroyItemGroup(items_group) # saving the command self.undo_stack.push( MoveComponents_Command(self, self.scene().selectedItems(), p_from=-new_rel_pos, p_to=QPointF(0, 0))) self.viewport().repaint() def move_selected_nodes_left(self): self.move_selected_copmonents__cmd(-40, 0) def move_selected_nodes_up(self): self.move_selected_copmonents__cmd(0, -40) def move_selected_nodes_right(self): self.move_selected_copmonents__cmd(+40, 0) def move_selected_nodes_down(self): self.move_selected_copmonents__cmd(0, +40) def selected_components_moved(self, pos_diff): items_list = self.scene().selectedItems() self.undo_stack.push( MoveComponents_Command(self, items_list, p_from=-pos_diff, p_to=QPointF(0, 0))) def selected_node_instances(self): selected_NIs = [] for i in self.scene().selectedItems(): if find_type_in_object(i, NodeInstance): selected_NIs.append(i) return selected_NIs def selected_drawings(self): selected_drawings = [] for i in self.scene().selectedItems(): if find_type_in_object(i, DrawingObject): selected_drawings.append(i) return selected_drawings def select_all(self): for i in self.scene().items(): if i.ItemIsSelectable: i.setSelected(True) self.viewport().repaint() def select_components(self, comps): self.scene().clearSelection() for c in comps: c.setSelected(True) def copy(self): # ctrl+c data = { 'nodes': self.get_node_instances_json_data(self.selected_node_instances()), 'connections': self.get_connections_json_data(self.selected_node_instances()), 'drawings': self.get_drawings_json_data(self.selected_drawings()) } QGuiApplication.clipboard().setText(json.dumps(data)) def cut(self): # called from shortcut ctrl+x data = { 'nodes': self.get_node_instances_json_data(self.selected_node_instances()), 'connections': self.get_connections_json_data(self.selected_node_instances()), 'drawings': self.get_drawings_json_data(self.selected_drawings()) } QGuiApplication.clipboard().setText(json.dumps(data)) self.remove_selected_components() def paste(self): data = {} try: data = json.loads(QGuiApplication.clipboard().text()) except Exception as e: return self.clear_selection() # calculate offset positions = [] for d in data['drawings']: positions.append({'x': d['pos x'], 'y': d['pos y']}) for n in data['nodes']: positions.append({'x': n['position x'], 'y': n['position y']}) offset_for_middle_pos = QPointF(0, 0) if len(positions) > 0: rect = QRectF(positions[0]['x'], positions[0]['y'], 0, 0) for p in positions: x = p['x'] y = p['y'] if x < rect.left(): rect.setLeft(x) if x > rect.right(): rect.setRight(x) if y < rect.top(): rect.setTop(y) if y > rect.bottom(): rect.setBottom(y) offset_for_middle_pos = self.last_mouse_move_pos - rect.center() self.undo_stack.push(Paste_Command(self, data, offset_for_middle_pos)) def add_component(self, e): if find_type_in_object(e, NodeInstance): self.add_node_instance(e) elif find_type_in_object(e, DrawingObject): self.add_drawing(e) def remove_component(self, e): if find_type_in_object(e, NodeInstance): self.remove_node_instance(e) elif find_type_in_object(e, DrawingObject): self.remove_drawing(e) def remove_selected_components(self): self.undo_stack.push( RemoveComponents_Command(self, self.scene().selectedItems())) self.viewport().update() # NODE SELECTION: ---- def clear_selection(self): self.scene().clearSelection() # CONNECTIONS: ---- def connect_gates__cmd(self, parent_gate: PortInstanceGate, child_gate: PortInstanceGate): self.undo_stack.push( ConnectGates_Command(self, parent_port=parent_gate.parent_port_instance, child_port=child_gate.parent_port_instance)) def connect_gates(self, parent_gate: PortInstanceGate, child_gate: PortInstanceGate): parent_port_instance: PortInstance = parent_gate.parent_port_instance child_port_instance: PortInstance = child_gate.parent_port_instance # if they, their directions and their parent node instances are not equal and if their types are equal if parent_port_instance.direction != child_port_instance.direction and \ parent_port_instance.parent_node_instance != child_port_instance.parent_node_instance and \ parent_port_instance.type_ == child_port_instance.type_: try: # remove connection if port instances are already connected index = parent_port_instance.connected_port_instances.index( child_port_instance) parent_port_instance.connected_port_instances.remove( child_port_instance) parent_port_instance.disconnected() child_port_instance.connected_port_instances.remove( parent_port_instance) child_port_instance.disconnected() except ValueError: # connect port instances # remove all connections from parent port instance if it's a data input if parent_port_instance.direction == 'input' and parent_port_instance.type_ == 'data': for cpi in parent_port_instance.connected_port_instances: self.connect_gates__cmd( parent_gate, cpi.gate) # actually disconnects the gates # remove all connections from child port instance it it's a data input if child_port_instance.direction == 'input' and child_port_instance.type_ == 'data': for cpi in child_port_instance.connected_port_instances: self.connect_gates__cmd( child_gate, cpi.gate) # actually disconnects the gates parent_port_instance.connected_port_instances.append( child_port_instance) child_port_instance.connected_port_instances.append( parent_port_instance) parent_port_instance.connected() child_port_instance.connected() self.viewport().repaint() def try_conn_gate_and_ni(self, parent_gate: PortInstanceGate, child_ni: NodeInstance): parent_port_instance: PortInstance = parent_gate.parent_port_instance if parent_port_instance.direction == 'output': for inp in child_ni.inputs: if parent_port_instance.type_ == inp.type_: self.connect_gates__cmd(parent_gate, inp.gate) return elif parent_port_instance.direction == 'input': for out in child_ni.outputs: if parent_port_instance.type_ == out.type_: self.connect_gates__cmd(parent_gate, out.gate) return @staticmethod def connection_path(p1: QPointF, p2: QPointF): """Returns the nice looking QPainterPath of a connection for two given points.""" path = QPainterPath() path.moveTo(p1) distance_x = abs(p1.x()) - abs(p2.x()) distance_y = abs(p1.y()) - abs(p2.y()) if ((p1.x() < p2.x() - 30) or math.sqrt((distance_x**2) + (distance_y**2)) < 100) and (p1.x() < p2.x()): path.cubicTo(p1.x() + ((p2.x() - p1.x()) / 2), p1.y(), p1.x() + ((p2.x() - p1.x()) / 2), p2.y(), p2.x(), p2.y()) elif p2.x() < p1.x() - 100 and abs(distance_x) / 2 > abs(distance_y): path.cubicTo(p1.x() + 100 + (p1.x() - p2.x()) / 10, p1.y(), p1.x() + 100 + (p1.x() - p2.x()) / 10, p1.y() - (distance_y / 2), p1.x() - (distance_x / 2), p1.y() - (distance_y / 2)) path.cubicTo(p2.x() - 100 - (p1.x() - p2.x()) / 10, p2.y() + (distance_y / 2), p2.x() - 100 - (p1.x() - p2.x()) / 10, p2.y(), p2.x(), p2.y()) else: path.cubicTo(p1.x() + 100 + (p1.x() - p2.x()) / 3, p1.y(), p2.x() - 100 - (p1.x() - p2.x()) / 3, p2.y(), p2.x(), p2.y()) return path # GET JSON DATA def get_json_data(self): flow_dict = { 'algorithm mode': 'data flow' if self.algorithm_mode.mode_data_flow else 'exec flow', 'viewport update mode': 'sync' if self.viewport_update_mode.sync else 'async', 'nodes': self.get_node_instances_json_data(self.all_node_instances), 'connections': self.get_connections_json_data(self.all_node_instances), 'drawings': self.get_drawings_json_data(self.drawings) } return flow_dict def get_node_instances_json_data(self, node_instances): script_node_instances_list = [] for ni in node_instances: node_instance_dict = ni.get_json_data() script_node_instances_list.append(node_instance_dict) return script_node_instances_list def get_connections_json_data(self, node_instances, only_with_connections_to=None): script_ni_connections_list = [] for ni in node_instances: for out in ni.outputs: if len(out.connected_port_instances) > 0: for connected_port in out.connected_port_instances: # this only applies when saving config data through deleting node instances: if only_with_connections_to is not None and \ connected_port.parent_node_instance not in only_with_connections_to and \ ni not in only_with_connections_to: continue # because I am not allowed to save connections between nodes connected to each other and both # connected to the deleted node, only the connections to the deleted node shall be saved connection_dict = { 'parent node instance index': node_instances.index(ni), 'output port index': ni.outputs.index(out) } # yes, very important: when copying components, there might be connections going outside the # selected lists, these should be ignored. When saving a project, all components are considered, # so then the index values will never be none connected_ni_index = node_instances.index(connected_port.parent_node_instance) if \ node_instances.__contains__(connected_port.parent_node_instance) else \ None connection_dict[ 'connected node instance'] = connected_ni_index connected_ip_index = connected_port.parent_node_instance.inputs.index(connected_port) if \ connected_ni_index is not None else None connection_dict[ 'connected input port index'] = connected_ip_index script_ni_connections_list.append(connection_dict) return script_ni_connections_list def get_drawings_json_data(self, drawings): drawings_list = [] for drawing in drawings: drawing_dict = drawing.get_json_data() drawings_list.append(drawing_dict) return drawings_list
class NodeInstance(QGraphicsItem): def __init__(self, parent_node: Node, flow, config=None): super(NodeInstance, self).__init__() self.parent_node = parent_node self.flow = flow # general attributes self.inputs = [] self.outputs = [] self.main_widget = None self.main_widget_proxy: FlowProxyWidget = None self.default_actions = { 'remove': { 'method': self.action_remove, 'data': 123 }, 'compute shape': { 'method': self.compute_content_positions } } # holds information for context menus self.gen_data_on_request = False self.personal_logs = [] self.special_actions = { } # only gets written in custom NodeInstance-subclasses - dynamic self.width = -1 self.height = -1 self.display_name_font = QFont('Poppins', 15) if parent_node.design_style == 'extended' else \ QFont('K2D', 20, QFont.Bold, True) self.display_name_FM = QFontMetricsF(self.display_name_font) # self.port_label_font = QFont("Source Code Pro", 10, QFont.Bold, True) # gets set to false a few lines below. needed for setup ports (to prevent shape updating stuff) self.initializing = True self.temp_state_data = 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) self.main_widget_proxy.setWidget(self.main_widget) 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 self.setToolTip(self.parent_node.description) self.initializing = False # # # # # # -------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------- # __ _ __ __ # ____ _ / / ____ _ ____ _____ (_) / /_ / /_ ____ ___ # / __ `/ / / / __ `/ / __ \ / ___/ / / / __/ / __ \ / __ `__ \ # / /_/ / / / / /_/ / / /_/ / / / / / / /_ / / / / / / / / / / # \__,_/ /_/ \__, / \____/ /_/ /_/ \__/ /_/ /_/ /_/ /_/ /_/ # /____/ def update(self, input_called=-1, output_called=-1): GlobalStorage.debug('update in', self.parent_node.title, 'on input', input_called) try: self.update_event(input_called) except Exception as e: GlobalStorage.debug('EXCEPTION IN', self.parent_node.title, 'NI:', e) def update_event(self, input_called=-1): # API (gets overwritten) pass def data_outputs_updated(self): GlobalStorage.debug('updating data outputs in', self.parent_node.title) for o in self.outputs: if o.type_ == 'data': o.updated_val() GlobalStorage.debug('data outputs in', self.parent_node.title, 'updated') def input(self, index): # API GlobalStorage.debug('input called in', self.parent_node.title, 'NI:', index) return self.inputs[index].get_val() def set_output_val(self, index, val): # API self.outputs[index].set_val(val) def exec_output(self, index): # API self.outputs[index].exec() # gets called from the flow after the content was removed from the scene; -> to stop threads etc. def about_to_remove_from_flow(self): if self.main_widget: self.main_widget.removing() self.removing() self.disable_personal_logs() def removing(self): # API (gets overwritten) pass # -------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------- # # # # # -------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------- # _ # ____ _ ____ (_) # / __ `/ / __ \ / / # / /_/ / / /_/ / / / # \__,_/ / .___/ /_/ # /_/ # # not everything but the most stuff. In 'algorithm' section are methods that are part of the API too # LOGGING def new_log(self, title): # just a handy convenience function 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): for log in self.personal_logs: log.remove() def log_message(self, message: str, target='global'): self.flow.parent_script.logger.log_message(self, message, target) # SHAPE def update_shape(self): # just a handy name for custom subclasses self.compute_content_positions() self.flow.viewport().update() # PORTS def create_new_input(self, type_, label, widget_type='', widget_name='', widget_pos='under', pos=-1): # GlobalStorage.debug('creating new input ---- type:', widget_type, 'label:', label, 'widget pos:', widget_pos) GlobalStorage.debug('create_new_input called with widget pos:', widget_pos) pi = PortInstance(self, 'input', type_, label, widget_type=widget_type, widget_name=widget_name, widget_pos=widget_pos) if pos == -1: self.inputs.append(pi) else: if pos == -1: self.inputs.insert(0, pi) else: self.inputs.insert(pos, pi) if self.scene(): self.add_input_to_scene(pi) if not self.initializing: self.update_shape() def create_new_input_from_config(self, input_config): pi = PortInstance(self, 'input', configuration=input_config) self.inputs.append(pi) def delete_input(self, i): # just a handy name for custom subclasses if type(i) == int: self.del_and_remove_input_from_scene(i) elif type(i) == PortInstance: self.del_and_remove_input_from_scene(self.inputs.index(i)) if not self.initializing: self.update_shape() def create_new_output(self, type_, label, pos=-1): # GlobalStorage.debug('creating new output in', self.parent_node.title) pi = PortInstance(self, 'output', type_, label) if pos == -1: self.outputs.append(pi) else: if pos == -1: self.outputs.insert(0, pi) else: self.outputs.insert(pos, pi) if self.scene(): self.add_output_to_scene(pi) if not self.initializing: self.update_shape() def create_new_output_from_config(self, output_config=None): pi = PortInstance(self, 'output', configuration=output_config) self.outputs.append(pi) def delete_output(self, o): # just a handy name for custom subclasses if type(o) == int: self.del_and_remove_output_from_scene(o) else: self.del_and_remove_output_from_scene(self.outputs.index(o)) if not self.initializing: self.update_shape() # GET, SET DATA def get_data(self): # used in custom subclasses return {} def set_data(self, data): # used in custom subclasses pass # -------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------- # # # # # # UI STUFF ---------------------------------------- def boundingRect(self): return QRectF(-self.width / 2, -self.height / 2, self.width, self.height) def paint(self, painter, option, widget=None): painter.setRenderHint(QPainter.Antialiasing) brush = QBrush(QColor(100, 100, 100, 150)) # QBrush(QColor('#3B9CD9')) painter.setBrush(brush) std_pen = QPen( QColor(30, 43, 48) ) # QColor(30, 43, 48) # used for header title and minimal std dark border std_pen.setWidthF(1.5) # painter.setPen(std_pen) if self.parent_node.design_style == 'extended': if GlobalStorage.storage['design style'] == 'dark std': c = self.parent_node.color # main rect body_gradient = QRadialGradient( self.boundingRect().topLeft(), self.flow.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) elif GlobalStorage.storage['design style'] == 'dark tron': # 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) painter.setFont(self.display_name_font) painter.setPen(std_pen) painter.drawText(self.get_title_rect(), Qt.AlignVCenter | Qt.AlignLeft, self.parent_node.title) painter.setBrush(Qt.NoBrush) painter.setPen(QPen(Qt.white, 1)) # painter.drawRect(self.get_header_rect()) elif self.parent_node.design_style == 'minimalistic': path = QPainterPath() path.moveTo(-self.width / 2, 0) if GlobalStorage.storage['design style'] == 'dark std': 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()) # 2*self.flow.pythagoras(self.height, self.width)) 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(std_pen) elif GlobalStorage.storage['design style'] == 'dark tron': 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() c = QColor('#36383B') painter.setBrush(c) pen = QPen(self.parent_node.color) pen.setWidth(2) painter.setPen(pen) painter.drawPath(path) painter.setFont(self.display_name_font) painter.drawText(self.boundingRect(), Qt.AlignCenter, self.parent_node.title) 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 get_header_rect(self): header_height = 35 * (self.parent_node.title.count('\n') + 1) header_rect = QRectF() header_rect.setTopLeft(QPointF(-self.width / 2, -self.height / 2)) header_rect.setRight(self.width / 2) header_rect.setBottom(-self.height / 2 + header_height) return header_rect def get_title_rect(self): title_rect_offset_factor = 0.56 header_rect = self.get_header_rect() rect = QRectF() rect.setTop(header_rect.top() + (header_rect.height() / 2) * (1 - title_rect_offset_factor)) rect.setLeft(header_rect.left() + 10) rect.setHeight(header_rect.height() * title_rect_offset_factor) w = header_rect.width() * title_rect_offset_factor title_width = self.display_name_FM.width( self.get_longest_line(self.parent_node.title)) rect.setWidth(w if w > title_width else title_width) return 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 # -------------------------------------------------------------------------------------- # 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): self.del_and_remove_content_from_scene() # resetting everything here 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]) #self.add_content_to_scene_and_compute_shape() #self.compute_content_positions() # self.set_port_positions() def get_input_widget_class(self, widget_name): 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): # index = i if type(i) == int else self.inputs.index(i) i = self.inputs[i_index] # GlobalStorage.debug('removing input',index,'in node instance',self.parent_node.title) for p in self.inputs[i_index].connected_port_instances: # p.connected_port_instances.remove(self.inputs[i_index]) 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) # GlobalStorage.debug('label added to scene:', o.label.scene()) def del_and_remove_output_from_scene(self, o_index): # index = o if type(o) == int else self.outputs.index(o) o = self.outputs[o_index] for p in self.outputs[o_index].connected_port_instances: # p.connected_port_instances.remove(self.outputs[o_index]) self.flow.connect_gates(o.gate, p.gate) self.flow.scene().removeItem(o.gate) self.flow.scene().removeItem(o.label) self.outputs.remove(o) # -------------------------------------------------------------------------------------- # SHAPE def add_content_to_scene_and_compute_shape(self): # EXPLANATION: When a NodeInstance is created, it is not placed in a scene yet (and shall not be). But when a # NodeInstance gets created, it instantly creates all stuff (Ports etc.), so this stuff - in the case of a # new placement of the NI into a scene - has to be added manually once. After that, all add_new_input()-or # similar calls result in an instant placement of the new elements in the scene. # GlobalStorage.debug('adding content and computing shape in', self.parent_node.title) # GlobalStorage.debug(self.height) for i in self.inputs: self.add_input_to_scene(i) for o in self.outputs: self.add_output_to_scene(o) if self.main_widget_proxy: self.scene().addItem(self.main_widget_proxy) self.compute_content_positions() def del_and_remove_content_from_scene(self): # everything get's reset here for i in range(len(self.inputs)): self.del_and_remove_input_from_scene(0) for o in range(len(self.outputs)): self.del_and_remove_output_from_scene(0) # lists are cleared here self.width = -1 self.height = -1 def compute_content_positions(self): for i in self.inputs: i.compute_size_and_positions() for o in self.outputs: o.compute_size_and_positions() display_name_height = self.display_name_FM.height() * ( self.parent_node.title.count('\n') + 1) display_name_width = self.display_name_FM.width( self.get_longest_line(self.parent_node.title)) display_name_width_extended = self.display_name_FM.width( '__' + self.get_longest_line(self.parent_node.title) + '__') # label_FM = QFontMetricsF(self.port_label_font) # all sizes and buffers space_between_io = 10 # the following function creates additional space at the top and the bottom of the NI - the more ports, the more space left_largest_width = 0 right_largest_width = 0 height_buffer_between_ports = 0 #10 # adds vertical buffer between single ports horizontal_buffer_to_border = 10 # adds a little bit of space between port and border of the NI left_ports_edge_height = -height_buffer_between_ports right_ports_edge_height = -height_buffer_between_ports for i in self.inputs: if i.width > left_largest_width: left_largest_width = i.width left_ports_edge_height += i.height + height_buffer_between_ports for o in self.outputs: if o.width > right_largest_width: right_largest_width = o.width right_ports_edge_height += o.height + height_buffer_between_ports ports_edge_height = left_ports_edge_height if left_ports_edge_height > right_ports_edge_height else right_ports_edge_height ports_edge_width = left_largest_width + space_between_io + right_largest_width + 2 * horizontal_buffer_to_border body_height = 0 body_width = 0 body_top = 0 body_left = 0 body_right = 0 if self.parent_node.design_style == 'minimalistic': height_buffer = 10 body_height = ports_edge_height if ports_edge_height > display_name_height else display_name_height self.height = body_height + height_buffer self.width = display_name_width_extended if display_name_width_extended > ports_edge_width else ports_edge_width if self.main_widget: if self.parent_node.main_widget_pos == 'under ports': self.width = self.width if self.width > self.main_widget.width( ) + 2 * horizontal_buffer_to_border else self.main_widget.width( ) + 2 * horizontal_buffer_to_border self.height += self.main_widget.height( ) + height_buffer_between_ports elif self.parent_node.main_widget_pos == 'between ports': #self.width += self.main_widget.width() self.width = display_name_width_extended if \ display_name_width_extended > ports_edge_width+self.main_widget.width() else \ ports_edge_width+self.main_widget.width() self.height = self.height if self.height > self.main_widget.height( ) + height_buffer else self.main_widget.height( ) + height_buffer body_top = -self.height / 2 + height_buffer / 2 body_left = -self.width / 2 + horizontal_buffer_to_border body_right = self.width / 2 - horizontal_buffer_to_border elif self.parent_node.design_style == 'extended': header_height = self.get_header_rect().height( ) #50 * (self.parent_node.title.count('\n')+1) vertical_body_buffer = 16 # half above, half below body_height = ports_edge_height self.height = header_height + body_height + vertical_body_buffer self.width = display_name_width_extended if display_name_width_extended > ports_edge_width else ports_edge_width if self.main_widget: if self.parent_node.main_widget_pos == 'under ports': self.width = self.width if self.width > self.main_widget.width( ) + 2 * horizontal_buffer_to_border else self.main_widget.width( ) + 2 * horizontal_buffer_to_border self.height += self.main_widget.height( ) + height_buffer_between_ports elif self.parent_node.main_widget_pos == 'between ports': self.width = display_name_width_extended if \ display_name_width_extended > ports_edge_width+self.main_widget.width() else \ ports_edge_width+self.main_widget.width() self.height = self.height if self.height > self.main_widget.height() + header_height + vertical_body_buffer else \ self.main_widget.height() + header_height + vertical_body_buffer body_top = -self.height / 2 + header_height + vertical_body_buffer / 2 body_left = -self.width / 2 + horizontal_buffer_to_border body_right = self.width / 2 - horizontal_buffer_to_border # here, the width and height are final self.set_content_positions( body_height=body_height, body_top=body_top, body_left=body_left, body_right=body_right, left_ports_edge_height=left_ports_edge_height, right_ports_edge_height=right_ports_edge_height, height_buffer_between_ports=height_buffer_between_ports, left_largest_width=left_largest_width, right_largest_width=right_largest_width, space_between_io=space_between_io) def set_content_positions(self, body_height, body_top, body_left, body_right, left_ports_edge_height, right_ports_edge_height, height_buffer_between_ports, left_largest_width, right_largest_width, space_between_io): # set positions # # calculating the vertical space between two inputs - without their heights, just between them space_between_inputs = (body_height - left_ports_edge_height) / ( len(self.inputs) - 1) if len( self.inputs) > 2 else body_height - left_ports_edge_height offset = 0 if len(self.inputs) == 1: offset = (body_height - left_ports_edge_height) / 2 for x in range(len(self.inputs)): i = self.inputs[x] y = body_top + i.height / 2 + offset port_pos_x = body_left + i.width / 2 port_pos_y = y i.gate.setPos(port_pos_x + i.gate.port_local_pos.x(), port_pos_y + i.gate.port_local_pos.y()) i.label.setPos(port_pos_x + i.label.port_local_pos.x(), port_pos_y + i.label.port_local_pos.y()) if i.widget: # GlobalStorage.debug(self.parent_node.title) i.proxy.setPos( port_pos_x + i.widget.port_local_pos.x() - i.widget.width() / 2, port_pos_y + i.widget.port_local_pos.y() - i.widget.height() / 2) offset += i.height + height_buffer_between_ports + space_between_inputs space_between_outputs = (body_height - right_ports_edge_height) / ( len(self.outputs) - 1) if len( self.outputs) > 2 else body_height - right_ports_edge_height offset = 0 if len(self.outputs) == 1: offset = (body_height - right_ports_edge_height) / 2 for x in range(len(self.outputs)): o = self.outputs[x] y = body_top + o.height / 2 + offset port_pos_x = body_right - o.width / 2 port_pos_y = y o.gate.setPos(port_pos_x + o.gate.port_local_pos.x(), port_pos_y + o.gate.port_local_pos.y()) o.label.setPos(port_pos_x + o.label.port_local_pos.x(), port_pos_y + o.label.port_local_pos.y()) offset += o.height + height_buffer_between_ports + space_between_outputs if self.main_widget: if self.parent_node.main_widget_pos == 'under ports': self.main_widget_proxy.setPos( -self.main_widget.width() / 2, body_top + body_height + height_buffer_between_ports ) # self.height/2 - height_buffer/2 - self.main_widget.height()) elif self.parent_node.main_widget_pos == 'between ports': body_incl_widget_height = body_height if body_height > self.main_widget.height( ) else self.main_widget.height() self.main_widget_proxy.setPos( body_left + left_largest_width + space_between_io / 2, body_top + body_incl_widget_height / 2 - self.main_widget.height() / 2) # -------------------------------------------------------------------------------------- # GENERAL def initialized(self): if self.temp_state_data is not None: self.set_data(self.temp_state_data) self.update() def get_longest_line(self, s: str): lines = s.split('\n') lines = [line.replace('\n', '') for line in lines] longest_line_found = '' for line in lines: if len(line) > len(longest_line_found): longest_line_found = line return line 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 get_json_data(self): # 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