Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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