Repository: leon-thomm/ryvencore (language: python)

Issue:
Yet another threading approach
improved new threading system, and conducted first successful threaded editor tests. ryven completely works with this version again


Discussion / hints:

Fix patch:
diff --git a/src/Connection.py b/src/Connection.py
index 32c2b32..d7106b7 100644
--- a/src/Connection.py
+++ b/src/Connection.py
@@ -1,16 +1,23 @@
-from PySide2.QtCore import QPointF, QObject
+from PySide2.QtCore import QPointF, QObject, Signal
 from PySide2.QtGui import QPainterPath
 
 from .ConnectionItem import ExecConnectionItem, DataConnectionItem
 
 
 class Connection(QObject):
+
+    # activation_request = Signal(object)
+
     def __init__(self, params):
         super().__init__()
 
-        self.out, self.inp, _ = params
+        self.out, self.inp, self.flow = params
         self.item = None
+        # self.activation_request.connect(self.flow.connection_activation_request)
+
 
+    # def queue(self):
+    #     self.activation_request.emit(self)
 
     def activate(self):
         pass
@@ -18,30 +25,12 @@ def activate(self):
 
 
 class ExecConnection(Connection):
-    def __init__(self, params: tuple):
-        super().__init__(params)
-
-        _, _, session_design = params
-        self.item = ExecConnectionItem(
-            out_item=self.out.item,
-            inp_item=self.inp.item,
-            type_=self.inp.type_,
-            session_design=session_design)
 
     def activate(self):
         self.inp.update()
 
 
 class DataConnection(Connection):
-    def __init__(self, params):
-        super().__init__(params)
-
-        _, _, session_design = params
-        self.item = DataConnectionItem(
-            out_item=self.out.item,
-            inp_item=self.inp.item,
-            type_=self.inp.type_,
-            session_design=session_design)
 
     def get_val(self):
         return self.out.get_val()
diff --git a/src/ConnectionItem.py b/src/ConnectionItem.py
index 91d2566..7a3400f 100644
--- a/src/ConnectionItem.py
+++ b/src/ConnectionItem.py
@@ -1,19 +1,20 @@
 # import math
 
 from PySide2.QtCore import QRectF, QPointF
-from PySide2.QtGui import QPainter, QColor, QRadialGradient, QPainterPath
+from PySide2.QtGui import QPainter, QColor, QRadialGradient, QPainterPath, QPen, Qt
 from PySide2.QtWidgets import QGraphicsItem, QStyleOptionGraphicsItem
 
 from .global_tools.math import pythagoras, sqrt
 
 
 class ConnectionItem(QGraphicsItem):
-    def __init__(self, out_item, inp_item, type_, session_design):
+    def __init__(self, connection, session_design):
         super().__init__()
 
-        self.out = out_item
-        self.inp = inp_item
-        self.type_ = type_
+        self.connection = connection
+        self.out = self.connection.out
+        self.inp = self.connection.inp
+        # self.type_ = type_
         self.session_design = session_design
         self.changed = False
         self.path: QPainterPath = None
@@ -22,8 +23,8 @@ def __init__(self, out_item, inp_item, type_, session_design):
         self.recompute()
 
     def boundingRect(self):
-        op = self.out.pin.get_scene_center_pos()
-        ip = self.inp.pin.get_scene_center_pos()
+        op = self.out.item.pin.get_scene_center_pos()
+        ip = self.inp.item.pin.get_scene_center_pos()
         top = min(0, (ip-self.pos()).y())
         left = min(0, (op-self.pos()).x())
         w = abs(ip.x()-op.x())
@@ -32,7 +33,7 @@ def boundingRect(self):
 
 
     def recompute(self):
-        self.setPos(self.out.pin.get_scene_center_pos())
+        self.setPos(self.out.item.pin.get_scene_center_pos())
         self.changed = True
 
 
@@ -46,19 +47,24 @@ class ExecConnectionItem(ConnectionItem):
 
     def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget=...) -> None:
 
-        pen = self.session_design.flow_theme.get_flow_conn_pen_inst(self.type_)
+        theme = self.session_design.flow_theme
+
+        pen = QPen(theme.exec_conn_color, theme.exec_conn_width)
+        pen.setStyle(theme.exec_conn_pen_style)
+        pen.setCapStyle(Qt.RoundCap)
+
         c = pen.color()
 
         # highlight hovered connections
-        if self.out.pin.hovered or self.inp.pin.hovered:
+        if self.out.item.pin.hovered or self.inp.item.pin.hovered:
             c = QColor('#c5c5c5')
             pen.setWidth(5)
 
         if self.changed or not self.path:
             self.changed = False
 
-            self.path = self.connection_path(self.out.pin.get_scene_center_pos() - self.scenePos(),
-                                             self.inp.pin.get_scene_center_pos() - self.scenePos())
+            self.path = self.connection_path(self.out.item.pin.get_scene_center_pos() - self.scenePos(),
+                                             self.inp.item.pin.get_scene_center_pos() - self.scenePos())
 
             w = self.path.boundingRect().width()
             h = self.path.boundingRect().height()
@@ -81,11 +87,16 @@ class DataConnectionItem(ConnectionItem):
 
     def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget=...) -> None:
 
-        pen = self.session_design.flow_theme.get_flow_conn_pen_inst(self.type_)
+        theme = self.session_design.flow_theme
+
+        pen = QPen(theme.data_conn_color, theme.data_conn_width)
+        pen.setStyle(theme.data_conn_pen_style)
+        pen.setCapStyle(Qt.RoundCap)
+
         c = pen.color()
 
         # highlight hovered connections
-        if self.out.pin.hovered or self.inp.pin.hovered:
+        if self.out.item.pin.hovered or self.inp.item.pin.hovered:
             c = QColor('#c5c5c5')
             pen.setWidth(5)
 
@@ -93,8 +104,8 @@ def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget=...)
         if self.changed or not self.path:
             self.changed = False
 
-            self.path = self.connection_path(self.out.pin.get_scene_center_pos() - self.scenePos(),
-                                             self.inp.pin.get_scene_center_pos() - self.scenePos())
+            self.path = self.connection_path(self.out.item.pin.get_scene_center_pos() - self.scenePos(),
+                                             self.inp.item.pin.get_scene_center_pos() - self.scenePos())
 
             w = self.path.boundingRect().width()
             h = self.path.boundingRect().height()
diff --git a/src/Design.py b/src/Design.py
index 9c846fb..bd5e764 100644
--- a/src/Design.py
+++ b/src/Design.py
@@ -1,49 +1,10 @@
-from PySide2.QtCore import QObject, Signal, Qt
-from PySide2.QtGui import QPen, QColor
+import json
 
-from .NodeItemPainter import NIPainter_DarkStd, NIPainter_DarkTron, NIPainter_Ghostly, NIPainter_Blender, \
-    NIPainter_Easy, NIPainter_Peasy, NIPainter_Ueli
-
-
-class FlowTheme:
-    """ **This FlowTheme system will be changed**
-
-    A FlowTheme holds all design information for the flow. And it defines what
-    themes exist. Notice, that all drawing of NodeItems and PortInstances
-    is done by NIPainter classes, while for each FlowTheme there exists exactly
-    one NIPainter class.
-
-    HOW TO CREATE NEW THEMES
-    - Create a new subclass of NIPainter in NodeItemPainter.py, and implement
-    all methods that are passed in NIPainter (take a look at the other already
-    existing NIPainter subclasses there for reference)
-    - Add a new theme entry to the flow_themes array of DesignContainer and
-    reference the new NIPainter subclass for your new theme you just made"""
-
-    def __init__(self, name,
-                 flow_conn_exec_color, flow_conn_exec_width, flow_conn_exec_pen_style,
-                 flow_conn_data_color, flow_conn_data_width, flow_conn_data_pen_style,
-                 node_item_painter,
-                 flow_background_color=QColor('#333333')):
-        self.name = name
-        self.node_item_painter = node_item_painter
-
-        self.flow_conn_exec_pen = QPen(flow_conn_exec_color, flow_conn_exec_width)
-        self.flow_conn_exec_pen.setStyle(flow_conn_exec_pen_style)
-        self.flow_conn_exec_pen.setCapStyle(Qt.RoundCap)
-
-        self.flow_conn_data_pen = QPen(flow_conn_data_color, flow_conn_data_width)
-        self.flow_conn_data_pen.setStyle(flow_conn_data_pen_style)
-        self.flow_conn_data_pen.setCapStyle(Qt.RoundCap)
-
-        self.flow_background_color = flow_background_color
-
-    def get_flow_conn_pen_inst(self, connection_type):
-        if connection_type == 'data':
-            return self.flow_conn_data_pen.__copy__()
-        else:
-            return self.flow_conn_exec_pen.__copy__()
+from PySide2.QtCore import QObject, Signal
 
+from .FlowTheme import FlowTheme_Toy, FlowTheme_DarkTron, FlowTheme_Ghost, FlowTheme_Blender, \
+    FlowTheme_Simple, FlowTheme_Ueli, FlowTheme_Samuel1, FlowTheme, FlowTheme_Samuel1_Light, \
+    FlowTheme_Samuel2, FlowTheme_Samuel2_Light
 
 
 class Design(QObject):
@@ -52,80 +13,58 @@ class Design(QObject):
 
     flow_theme_changed = Signal(str)
 
-    def __init__(self, performance_mode: str = 'pretty', animations_enabled: bool = True):
+    def __init__(self):
         super().__init__()
 
+        self.flow_themes = []
+        self.flow_theme: FlowTheme = None
+        self.performance_mode: str = None
+        self.node_item_shadows_enabled: bool = None
+        self.animations_enabled: bool = None
+        self.node_selection_stylesheet = default_node_selection_stylesheet
+
+        # load standard default values
+        self.register_flow_themes()
+        self._default_flow_theme = self.flow_themes[-1]
+        self.set_performance_mode('pretty')
+        self.set_flow_theme(self._default_flow_theme)
+
+
+    def register_flow_themes(self):
         self.flow_themes = [
-            FlowTheme('dark std',
-                      QColor(188, 187, 242),
-                      5,
-                      Qt.SolidLine,
-                      QColor(188, 187, 242),
-                      5,
-                      Qt.DashLine,
-                      NIPainter_DarkStd),
-            FlowTheme('dark tron',
-                      QColor(0, 120, 180),
-                      4,
-                      Qt.SolidLine,
-                      QColor(0, 120, 180),
-                      4,
-                      Qt.DashLine,
-                      NIPainter_DarkTron),
-            FlowTheme('ghostly',
-                      QColor(0, 17, 25),
-                      2,
-                      Qt.SolidLine,
-                      QColor(0, 17, 25),
-                      2,
-                      Qt.DashLine,
-                      NIPainter_Ghostly),
-            FlowTheme('blender',
-                      QColor(0, 17, 25),
-                      2,
-                      Qt.SolidLine,
-                      QColor(0, 17, 25),
-                      2,
-                      Qt.DashLine,
-                      NIPainter_Blender),
-            FlowTheme('easy',
-                      QColor('#989c9f'),
-                      2,
-                      Qt.SolidLine,
-                      QColor('#989c9f'),
-                      2,
-                      Qt.DashLine,
-                      NIPainter_Easy,
-                      QColor('#212429')),
-            FlowTheme('peasy',
-                      QColor('#989c9f'),
-                      2,
-                      Qt.SolidLine,
-                      QColor('#989c9f'),
-                      2,
-                      Qt.DashLine,
-                      NIPainter_Peasy,
-                      QColor('#3f4044')),
-            FlowTheme('ueli',
-                      QColor('#989c9f'),
-                      2,
-                      Qt.SolidLine,
-                      QColor('#989c9f'),
-                      2,
-                      Qt.DashLine,
-                      NIPainter_Ueli,
-                      QColor('#3f4044'))
+            FlowTheme_Toy(),
+            FlowTheme_DarkTron(),
+            FlowTheme_Ghost(),
+            FlowTheme_Blender(),
+            FlowTheme_Simple(),
+            FlowTheme_Ueli(),
+            FlowTheme_Samuel1(),
+            FlowTheme_Samuel2(),
+            FlowTheme_Samuel1_Light(),
+            FlowTheme_Samuel2_Light()
         ]
 
-        self._default_flow_theme = self.flow_themes[-1]
-        self.flow_theme = None
+    def load_from_config(self, filepath: str):
+        """Loads design configs from a config json file"""
+
+        f = open(filepath, 'r')
+        data = f.read()
+        f.close()
+
+        IMPORT_DATA = json.loads(data)
+
+        if 'flow themes' in IMPORT_DATA:
+            # load flow theme configs
+            FTID = IMPORT_DATA['flow themes']
+            for flow_theme in self.flow_themes:
+                flow_theme.load(FTID)
 
-        self.performance_mode = ''
-        self.node_item_shadows_enabled = False
-        self.set_performance_mode(performance_mode)
+        if 'init flow theme' in IMPORT_DATA:
+            self._default_flow_theme = self.flow_theme_by_name(IMPORT_DATA.get('init flow theme'))
+            self.set_flow_theme(self._default_flow_theme)
 
-        self.animations_enabled = animations_enabled
-        self.node_choice_stylesheet = default_node_choice_stylesheet
+        if 'init performance mode' in IMPORT_DATA:
+            self.set_performance_mode(IMPORT_DATA['init performance mode'])
 
     def available_flow_themes(self) -> dict:
         return {theme.name: theme for theme in self.flow_themes}
@@ -140,10 +79,10 @@ def set_flow_theme(self, theme: FlowTheme = None, name: str = ''):
         """You can either specify the theme by name, or directly provide a FlowTheme object."""
         if theme:
             self.flow_theme = theme
-        elif name != '':
+        elif name and name != '':
             self.flow_theme = self.flow_theme_by_name(name)
         else:
-            self.flow_theme = self._default_flow_theme
+            return
 
         self.flow_theme_changed.emit(self.flow_theme.name)
 
@@ -165,13 +104,13 @@ def set_node_item_shadows(self, b: bool):
         self.node_item_shadows_enabled = b
 
     def set_node_choice_stylesheet(self, s: str):
-        self.node_choice_stylesheet = s
+        self.node_selection_stylesheet = s
 
 
 
 
 
-default_node_choice_stylesheet = '''
+default_node_selection_stylesheet = '''
 QWidget {
 	background-color: #2b2b2b;
 	border-radius: 3px;
diff --git a/src/DrawingObject.py b/src/DrawingObject.py
index 1cd17f5..8c393c0 100644
--- a/src/DrawingObject.py
+++ b/src/DrawingObject.py
@@ -6,14 +6,14 @@
 
 
 class DrawingObject(QGraphicsItem):
-    def __init__(self, flow, config=None):
+    def __init__(self, flow_view, config=None):
         super(DrawingObject, self).__init__()
 
         self.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable |
                       QGraphicsItem.ItemSendsScenePositionChanges)
         self.setAcceptHoverEvents(True)
 
-        self.flow = flow
+        self.flow_view = flow_view
         self.color = None
         self.base_stroke_weight = None
         self.type = 'pen'  # so far the only available, but I already save it so I could add more types in the future
@@ -144,7 +144,7 @@ def boundingRect(self):
 
     def itemChange(self, change, value):
         if change == QGraphicsItem.ItemPositionChange:
-            self.flow.viewport().update()
+            self.flow_view.viewport().update()
             if self.movement_state == MovementEnum.mouse_clicked:
                 self.movement_state = MovementEnum.position_changed
 
@@ -159,7 +159,7 @@ def mousePressEvent(self, event):
     def mouseReleaseEvent(self, event):
         """Used for Moving-Commands in Flow - may be replaced later with a nicer determination of a move action."""
         if self.movement_state == MovementEnum.position_changed:
-            self.flow.selected_components_moved(self.pos() - self.movement_pos_from)
+            self.flow_view.selected_components_moved(self.pos() - self.movement_pos_from)
         self.movement_state = None
         return QGraphicsItem.mouseReleaseEvent(self, event)
 
diff --git a/src/Flow.py b/src/Flow.py
index 3d0f903..bb2fd33 100644
--- a/src/Flow.py
+++ b/src/Flow.py
@@ -1,775 +1,67 @@
-from PySide2.QtCore import Qt, QPointF, QPoint, QRectF, QSizeF, Signal, QTimer, QObject
-from PySide2.QtGui import QPainter, QPen, QColor, QKeySequence, QTabletEvent, QImage, QGuiApplication, QFont
-from PySide2.QtWidgets import QGraphicsView, QGraphicsScene, QShortcut, QMenu, QGraphicsItem, QUndoStack
-
-from .FlowCommands import MoveComponents_Command, PlaceNodeItemInScene_Command, \
-    PlaceDrawingObject_Command, RemoveComponents_Command, ConnectPorts_Command, Paste_Command
-from .FlowProxyWidget import FlowProxyWidget
-from .FlowStylusModesWidget import FlowStylusModesWidget
-from .FlowWorkerThread import FlowWorkerThread
-from .FlowZoomWidget import FlowZoomWidget
-from .Node import Node
-from .NodeObjPort import NodeObjPort, NodeObjOutput, NodeObjInput
-from .node_choice_widget.NodeChoiceWidget import NodeChoiceWidget
-from .NodeItem import NodeItem
-from .PortItem import PortItem, PortItemPin
-from .Connection import Connection
-from .ConnectionItem import default_cubic_connection_path
-from .DrawingObject import DrawingObject
-from .global_tools.Debugger import Debugger
-from .RC import PortObjPos, FlowAlg
-from .RC import FlowVPUpdateMode as VPUpdateMode
-
-import json
-
+from PySide2.QtCore import QObject, Signal
 
-class Flow(QGraphicsView):
-    """Manages all GUI of flows"""
-
-    nodes_selection_changed = Signal(list)
-    algorithm_mode_changed = Signal(str)
-    viewport_update_mode_changed = Signal(str)
-    trigger_port_connected = Signal(NodeObjPort)
-    trigger_port_disconnected = Signal(NodeObjPort)
+from .Connection import Connection, DataConnection, ExecConnection
+from .Node import Node
+from .NodeObjPort import NodeObjOutput, NodeObjInput, NodeObjPort
+from .RC import FlowAlg, PortObjPos
 
 
-    def __init__(self, session, script, flow_size: list = None, config=None, parent=None):
-        super(Flow, self).__init__(parent=parent)
+class Flow(QObject):
+    """
+    The abstract flow holding all abstract objects and implementations for editing.
+    To enable threading, the communication between the Flow and the FlowView only
+    uses signals and slots.
+    Note that undo/redo operations are implemented in the FlowView.
+    """
 
+    node_added = Signal(Node)
+    node_removed = Signal(Node)
+    connection_added = Signal(Connection)
+    connection_removed = Signal(Connection)
+    connection_request_valid = Signal(bool)
+    nodes_created_from_config = Signal(list)
+    connections_created_from_config = Signal(list)
+    test_signal = Signal()
 
-        # 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)
+    algorithm_mode_changed = Signal(int)
 
-        # SHORTCUTS
-        self._init_shortcuts()
+    def __init__(self, session, script, parent=None):
+        super().__init__(parent=parent)
 
-        # GENERAL ATTRIBUTES
-        self.script = script
         self.session = session
-        self.node_items: [NodeItem] = []
+        self.script = script
         self.nodes: [Node] = []
-        # self.connection_items = []
+        self.nodes_initialized = {}
+        self._num_queued_building_nodes = 0
         self.connections: [Connection] = []
-        self.selected_pin: PortItemPin = None
-        self.dragging_connection = False
-        self.ignore_mouse_event = False  # for stylus - see tablet event
-        self._showing_framerate = False
-        self._last_mouse_move_pos: QPointF = None
-        self._node_place_pos = QPointF()
-        self._left_mouse_pressed_in_flow = False
-        self._right_mouse_pressed_in_flow = False
-        self._mouse_press_pos: QPointF = None
-        self._auto_connection_pin = 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
-
-        if self.session.threading_enabled:
-            self.worker_thread = FlowWorkerThread(self.thread())
-            FWT = self.worker_thread
-            self.trigger_port_connected.connect(FWT.interface.trigger_port_connected)
-            self.trigger_port_disconnected.connect(FWT.interface.trigger_port_disconnected)
-            self.worker_thread.start()
-
-        # SETTINGS
-        self.alg_mode = FlowAlg.DATA    # Flow_AlgorithmMode()
-        self.vp_update_mode: VPUpdateMode = VPUpdateMode.SYNC  # Flow_ViewportUpdateMode()
-
-        # CREATE UI
-        scene = QGraphicsScene(self)
-        scene.setItemIndexMethod(QGraphicsScene.NoIndex)
-        if flow_size is None:
-            scene.setSceneRect(0, 0, 10 * self.width(), 10 * self.height())
-        else:
-            scene.setSceneRect(0, 0, flow_size[0], flow_size[1])
-
-        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._scene_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, self.session.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)
-
-        # # TOUCH GESTURES
-        # recognizer = PanGestureRecognizer()
-        # pan_gesture_id = QGestureRecognizer.registerRecognizer(recognizer) <--- CRASH HERE
-        # self.grabGesture(pan_gesture_id)
-
-        # DESIGN THEME
-        self.session.design.flow_theme_changed.connect(self._theme_changed)
-
-        if config is not None:
-            # algorithm mode
-            mode = config['algorithm mode']
-            if mode == 'data' or mode == 'data flow':  # mode == FlowAlg.DATA
-                # self.alg_mode = FlowAlg.DATA
-                self.set_algorithm_mode('data')
-            elif mode == 'exec' or mode == 'exec flow':  # mode == FlowAlg.EXEC
-                # self.alg_mode = FlowAlg.EXEC
-                self.set_algorithm_mode('exec')
-
-            # viewport update mode
-            vpum = config['viewport update mode']
-            if vpum == 'sync':  # vpum == VPUpdateMode.SYNC
-                # self.vp_update_mode = VPUpdateMode.SYNC
-                self.set_viewport_update_mode('sync')
-            elif vpum == 'async':  # vpum == VPUpdateMode.ASYNC
-                self.vp_update_mode = VPUpdateMode.ASYNC
-                self.set_viewport_update_mode('async')
-
-            self.nodes, self.node_items = self.place_nodes_from_config(config['nodes'])
-            self.connect_nodes_from_config(self.nodes, 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()
-
-
-        # FRAMERATE TRACKING
-        self.num_frames = 0
-        self.framerate = 0
-        self.framerate_timer = QTimer(self)
-        self.framerate_timer.timeout.connect(self._on_framerate_timer_timeout)
-
-        self.show_framerate(m_sec_interval=100)  # for testing
-
-
-    def show_framerate(self, show: bool = True, m_sec_interval: int = 1000):
-        self._showing_framerate = show
-        self.framerate_timer.setInterval(m_sec_interval)
-        self.framerate_timer.start()
-
-    def _on_framerate_timer_timeout(self):
-        self.framerate = self.num_frames
-        self.num_frames = 0
-
-    def _init_shortcuts(self):
-        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_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)
-
-    def _theme_changed(self, t):
-        # TODO: repaint background. how?
-        self.viewport().update()
-
-    def _scene_selection_changed(self):
-        self.nodes_selection_changed.emit(self.selected_nodes())
-
-    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 isinstance(i, NodeItem):
-                ni: NodeItem = 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.write('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 isinstance(self.itemAt(event.pos()), PortItemPin):
-                    self.selected_pin = self.itemAt(event.pos())
-                    self.dragging_connection = True
-
-            self._left_mouse_pressed_in_flow = True
-
-        elif event.button() == Qt.RightButton:
-            self._right_mouse_pressed_in_flow = True
-            event.accept()
-
-        # elif event.button() == Qt.MidButton:
-        # elif event.button() == Qt.RightButton:
-        #     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)
-
-        # print('qwer')
-        # print(event.button())
-
-        if self._right_mouse_pressed_in_flow:    # PAN
-
-            if not self._panning:
-                self._panning = True
-                self._pan_last_x = event.x()
-                self._pan_last_y = event.y()
-
-            self.pan(event.pos())
-            event.accept()
-
-        # 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)
-
-        node_item_at_event_pos = None
-        for item in self.items(event.pos()):
-            if isinstance(item, NodeItem):
-                node_item_at_event_pos = item
-
-        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 self._panning:
-        # elif event.button() == Qt.MidButton:
-            self._panning = False
-
-        elif event.button() == Qt.RightButton:
-            self._right_mouse_pressed_in_flow = False
-            if self._mouse_press_pos == self._last_mouse_move_pos and \
-                    len(self.items(event.pos())) == 0:
-
-                self._node_choice_widget.reset_list()
-                self.show_node_choice_widget(event.pos())
-                return
-
-
-        if self.dragging_connection:
-
-            # connection dropped over specific pin
-            if self.itemAt(event.pos()) and isinstance(self.itemAt(event.pos()), PortItemPin):
-
-                self.connect_node_ports__cmd(
-                    self.selected_pin.port,
-                    self.itemAt(event.pos()).port
-                )
-
-            # connection dropped above NodeItem -> auto connect
-            elif node_item_at_event_pos:
-                # find node item
-                ni_under_drop = None
-                for item in self.items(event.pos()):
-                    if isinstance(item, NodeItem):
-                        ni_under_drop = item
-                        self.auto_connect(self.selected_pin.port, ni_under_drop.node)
-                        break
-
-            # connection dropped somewhere else -> show node choice widget
-            else:
-                self._auto_connection_pin = self.selected_pin
-                self.show_node_choice_widget(event.pos())
-
-            self.dragging_connection = False
-            self.selected_pin = None
-
-        if event.button() == Qt.LeftButton:
-            self._left_mouse_pressed_in_flow = False
-        elif event.button() == Qt.RightButton:
-            self._right_mouse_pressed_in_flow = False
-
-        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
-
-        scaled_event_pos: QPointF = event.posF()/self._current_scale
-
-        if event.type() == QTabletEvent.TabletPress:
-            self.ignore_mouse_event = True
-
-            if event.button() == Qt.LeftButton:
-                if self.stylus_mode == 'comment':
-                    view_pos = self.mapToScene(self.viewport().pos())
-                    new_drawing = self._create_and_place_drawing__cmd(
-                        view_pos + scaled_event_pos,
-                        config={**self._stylus_modes_widget.get_pen_settings(), 'viewport pos': view_pos}
-                    )
-                    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 isinstance(i, DrawingObject):
-                            self.remove_drawing(i)
-                            break
-            elif self.stylus_mode == 'comment' and self._drawing:
-                if self._current_drawing.append_point(scaled_event_pos):
-                    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.write('drawing obj finished')
-                self._current_drawing.finish()
-                self._current_drawing = None
-                self._drawing = False
-
-    # https://forum.qt.io/topic/121473/qgesturerecognizer-registerrecognizer-crashes-using-pyside2
-    #
-    # def event(self, event) -> bool:
-    #     # if event.type() == QEvent.Gesture:
-    #     #     if event.gesture(PanGesture) is not None:
-    #     #         return self.pan_gesture(event)
-    #
-    #     return QGraphicsView.event(self, event)
-    #
-    # def pan_gesture(self, event: QGestureEvent) -> bool:
-    #     pan: PanGesture = event.gesture(PanGesture)
-    #     print(pan)
-    #     return True
-
-    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.write('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.session.nodes if find_type_in_object(n, GetVar_Node) or
-    #                                       find_type_in_object(n, SetVar_Node)])
-
-    def drawBackground(self, painter, rect):
-        painter.fillRect(rect.intersected(self.sceneRect()), self.session.design.flow_theme.flow_background_color)
-        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):
-
-        if self._showing_framerate:
-            self.num_frames += 1
-            pen = QPen(QColor('#A9D5EF'))
-            pen.setWidthF(2)
-            painter.setPen(pen)
-
-            pos = self.mapToScene(10, 23)
-            painter.setFont(QFont('Poppins', round(11 * self._total_scale_div)))
-            painter.drawText(pos, "{:.2f}".format(self.framerate))
-
-
-        # DRAW CURRENTLY DRAGGED CONNECTION
-        if self.dragging_connection:
-            pen = QPen('#101520')
-            pen.setWidth(3)
-            pen.setStyle(Qt.DotLine)
-            painter.setPen(pen)
-
-            pin_pos = self.selected_pin.get_scene_center_pos()
-            spp = self.selected_pin.port
-            cursor_pos = self._last_mouse_move_pos
-
-            pos1 = pin_pos if spp.io_pos == PortObjPos.OUTPUT else cursor_pos
-            pos2 = pin_pos if spp.io_pos == PortObjPos.INPUT else cursor_pos
-
-            if spp.type_ == 'data':
-                painter.drawPath(
-                    default_cubic_connection_path(pos1, pos2)
-                )
-            elif spp.type_ == 'exec':
-                painter.drawPath(
-                    default_cubic_connection_path(pos1, pos2)
-                )
-
-
-        # DRAW SELECTED NIs BORDER
-        for ni in self.selected_node_items():
-            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) -> QImage:
-        """Returns a clear image of the viewport"""
-
-        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) -> QImage:
-        """Returns an image of the whole scene, scaled accordingly to current scale factor.
-        A bug makes this only work from the viewport position down and right, so the user has to scroll to
-        the top left corner in order to get the full scene"""
-
-        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.session.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_pin = 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(self, node_class, config) -> Node:
-        """Creates and returns a new Node object."""
-
-        node = node_class(params=(self, self.session.design, config))
-        node.finish_initialization()
-
-        if self.session.threading_enabled:
-            node.moveToThread(self.worker_thread)
-            # from here, node lives in the worker thread but it's doesn't
-
-        return node
-
-    def add_node_item(self, ni: NodeItem, pos=None):
-        """Adds a NodeItem to the scene."""
-
-        self.scene().addItem(ni)
-        ni.node.enable_logs()
-        if pos:
-            ni.setPos(pos)
-
-        # select new NI
-        self.scene().clearSelection()
-        ni.setSelected(True)
+        self._temp_config_data = None
+        self._build_connections_queue = None
 
-        self.node_items.append(ni)
-        self.nodes.append(ni.node)
+        self.alg_mode = FlowAlg.DATA
 
-    def add_node_items(self, node_items: [NodeItem]):
-        """Adds a list of NodeItems to the scene."""
 
-        for ni in node_items:
-            self.add_node_item(ni)
+    def load(self, config):
+        # algorithm mode
+        mode = config['algorithm mode']
+        if mode == 'data' or mode == 'data flow':
+            self.set_algorithm_mode('data')
+        elif mode == 'exec' or mode == 'exec flow':
+            self.set_algorithm_mode('exec')
 
-    def remove_node_item(self, ni):
-        """Removes a NodeItem from the scene."""
+        # build flow
+        new_nodes = self.create_nodes_from_config(config['nodes'])
+        self.connect_nodes_from_config(new_nodes, config['connections'])
 
-        ni.node.about_to_remove_from_scene()
 
-        self.scene().removeItem(ni)
-
-        self.node_items.remove(ni)
-        self.nodes.remove(ni.node)
-
-    def _place_new_node_by_shortcut(self):  # Shift+P
-        point_in_viewport = None
-        selected_NIs = self.selected_node_items()
-        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: list, offset_pos: QPoint = QPoint(0, 0)):
-        """Creates Nodes and places them in the scene from nodes_config.
-        The exact config list is included in what is returned by the config_data() method at 'nodes'."""
+    def create_nodes_from_config(self, nodes_config: list):
+        """
+        Creates Nodes from nodes_config, previously returned by config_data.
+        """
 
         nodes = []
-        node_items = []
 
         for n_c in nodes_config:
-            
+
             # find class
             node_class = None
             if 'parent node title' in n_c:  # backwards compatibility
@@ -784,47 +76,45 @@ def place_nodes_from_config(self, nodes_config: list, offset_pos: QPoint = QPoin
                         break
 
             node = self.create_node(node_class, n_c)
-            self.add_node_item(node.item, QPoint(n_c['position x'], n_c['position y']) + offset_pos)
             nodes.append(node)
-            node_items.append(node.item)
 
-        self.nodes += nodes
-        self.node_items += node_items
+        self.nodes_created_from_config.emit(nodes)
+        return nodes
 
-        return nodes, node_items
 
-    def place_node__cmd(self, node_class, config=None):
+    def create_node(self, node_class, config=None):
+        node = node_class((self, self.session, config))
+        self.add_node(node)
+        return node
 
-        node = self.create_node(node_class, config)
 
-        place_command = PlaceNodeItemInScene_Command(self, node.item, self._node_place_pos)
+    def add_node(self, node: Node):
+        if not node.initialized:
+            node.finish_initialization()
 
-        self._undo_stack.push(place_command)
+        self.nodes.append(node)
 
-        if self._auto_connection_pin:
-            self.auto_connect(self._auto_connection_pin.port,
-                              node)
+        self.node_added.emit(node)
 
-        return node
 
-    # def remove_node_instance_triggered(self, node_instance):  # called from context menu of NodeInstance
-    #     if node_instance in self.selected_node_items():
-    #         self.__undo_stack.push(
-    #             RemoveComponents_Command(self, self.scene().selectedItems()))
-    #     else:
-    #         self.__undo_stack.push(RemoveComponents_Command(self, [node_instance]))
+    def node_placed(self, node: Node):
+        """Triggered after the FlowWidget added the item to the scene."""
+
+        node.load_config_data()
+        node.place_event()
+        node.update()
 
-    # 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.script.main_window.custom_node_input_widget_classes
+    def remove_node(self, node: Node):
+        node.prepare_removal()
+        self.nodes.remove(node)
+        self.node_removed.emit(node)
 
-    def connect_nodes_from_config(self, nodes: [Node], connections_config: list):
-        """Connects Nodes according to the config list. This list is included in what is returned by the
-        config_data() method at 'connections'."""
 
-        for c in connections_config:
+    def connect_nodes_from_config(self, nodes: [Node], config: list):
+        connections = []
+
+        for c in config:
 
             c_parent_node_index = -1
             if 'parent node instance index' in c:  # backwards compatibility
@@ -846,340 +136,96 @@ def connect_nodes_from_config(self, nodes: [Node], connections_config: list):
                 parent_node = nodes[c_parent_node_index]
                 connected_node = nodes[c_connected_node_index]
 
-                self.connect_ports(parent_node.outputs[c_output_port_index],
-                                   connected_node.inputs[c_connected_input_port_index])
+                c = self.connect_nodes(parent_node.outputs[c_output_port_index],
+                                       connected_node.inputs[c_connected_input_port_index])
+                connections.append(c)
 
-    # DRAWINGS
-    def create_drawing(self, config=None) -> DrawingObject:
-        """Creates and returns a new DrawingObject."""
+        self.connections_created_from_config.emit(connections)
 
-        new_drawing = DrawingObject(self, config)
-        return new_drawing
+        return connections
 
-    def add_drawing(self, drawing_obj, posF=None):
-        """Adds a DrawingObject to the scene."""
 
-        self.scene().addItem(drawing_obj)
-        if posF:
-            drawing_obj.setPos(posF)
-        self.drawings.append(drawing_obj)
+    def check_connection_validity(self, p1: NodeObjPort, p2: NodeObjPort, emit=True) -> bool:
+        """
+        Checks whether a considered connect action is legal.
+        """
+        valid = True
 
-    def add_drawings(self, drawings):
-        """Adds a list of DrawingObjects to the scene."""
+        if p1.node == p2.node:
+            valid = False
 
-        for d in drawings:
-            self.add_drawing(d)
+        if p1.io_pos == p2.io_pos or p1.type_ != p2.type_:
+            valid = False
 
-    def remove_drawing(self, drawing):
-        """Removes a drawing from the scene."""
-
-        self.scene().removeItem(drawing)
-        self.drawings.remove(drawing)
+        if emit:
+            # used by FlowWidget
+            self.connection_request_valid.emit(valid)
 
-    def place_drawings_from_config(self, drawings_config: list, offset_pos=QPoint(0, 0)):
-        """Creates and places drawings from drawings. The same list is returned by the config_data() method
-        at 'drawings'."""
+        return valid
 
-        new_drawings = []
-        for d_config in drawings_config:
-            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, posF, config=None):
-        new_drawing_obj = self.create_drawing(config)
-        place_command = PlaceDrawingObject_Command(self, posF, 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
-            w = i.boundingRect().width()
-            h = i.boundingRect().height()
-            if new_pos.x() - w / 2 < 0 or \
-                    new_pos.x() + w / 2 > self.scene().width() or \
-                    new_pos.y() - h / 2 < 0 or \
-                    new_pos.y() + h / 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_items(self) -> [NodeItem]:
-        """Returns a list of the currently selected NodeItems."""
-
-        selected_NIs = []
-        for i in self.scene().selectedItems():
-            if isinstance(i, NodeItem):
-                selected_NIs.append(i)
-        return selected_NIs
-
-    def selected_nodes(self) -> [Node]:
-        return [item.node for item in self.selected_node_items()]
-
-    def selected_drawings(self) -> [DrawingObject]:
-        """Returns a list of the currently selected drawings."""
-
-        selected_drawings = []
-        for i in self.scene().selectedItems():
-            if isinstance(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_nodes_config_data(self.selected_nodes()),
-                'connections': self._get_connections_config_data(self.selected_nodes()),
-                'drawings': self._get_drawings_config_data(self.selected_drawings())}
-        QGuiApplication.clipboard().setText(json.dumps(data))
-
-    def _cut(self):  # called from shortcut ctrl+x
-        data = {'nodes': self._get_nodes_config_data(self.selected_nodes()),
-                'connections': self._get_connections_config_data(self.selected_nodes()),
-                'drawings': self._get_drawings_config_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 isinstance(e, NodeItem):
-            self.add_node_item(e)
-        elif isinstance(e, DrawingObject):
-            self.add_drawing(e)
-
-    def remove_components(self, comps):
-        for c in comps:
-            self.remove_component(c)
-
-    def remove_component(self, e):
-        if isinstance(e, NodeItem):
-            self.remove_node_item(e)
-        elif isinstance(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_node_ports__cmd(self, p1: NodeObjPort, p2: NodeObjPort):
-        """Connects if possible, disconnects if ports are already connected"""
-
-        out = None
-        inp = None
-        if p1.io_pos == PortObjPos.OUTPUT and p2.io_pos == PortObjPos.INPUT:
-            out = p1
-            inp = p2
-        elif p1.io_pos == PortObjPos.INPUT and p2.io_pos == PortObjPos.OUTPUT:
-            out = p2
-            inp = p1
-        else:
-            # ports have same direction
-            return
-
-        if out.type_ != inp.type_:
-            return
-
-        self._undo_stack.push(ConnectPorts_Command(self, out=out, inp=inp))
-
-    def connect_ports(self, out: NodeObjOutput = None, inp: NodeObjInput = None, connection: Connection = None):
+
+    def connect_nodes(self, p1: NodeObjPort, p2: NodeObjPort):
         """
-        DEFAULT: connects out and inp if they are not connected, otherwise they get disconnected
-        connected() or disconnected() is triggered afterwards of the ports
-        IF CONNECTION PROVIDED: the connection gets added to the scene and connected() is triggered on the ports.
+        connects or disconnects nodes
         """
 
-        inp = inp if not connection else connection.inp
-        out = out if not connection else connection.out
+        if not self.check_connection_validity(p1, p2, emit=False):
+            raise Exception('Illegal connection request. Use check_connection_validity to check if a request is legal.')
 
+        out = p1
+        inp = p2
+        if out.io_pos == PortObjPos.INPUT:
+            out, inp = inp, out
 
         for c in out.connections:
             if c.inp == inp:
-                # disconnect
+                # DISCONNECT
                 self.remove_connection(c)
-                out.disconnected()
-                inp.disconnected()
                 return
 
-        if inp.node == out.node:
-            return
-
-
-        # CONNECT
-
-        # remove all connections from input port instance if it's a data input
-        if inp.type_ == 'data':
-            for c in inp.connections:
-                self.connect_node_ports__cmd(c.out, inp)
-
-
-        if connection:
-            self.add_connection(connection)
-            if self.session.threading_enabled:
-                self.trigger_port_connected.emit(out)
-                self.trigger_port_connected.emit(inp)
-            else:
-                connection.out.connected()
-                connection.inp.connected()
-            return
-
+        # if inp.type_ == 'data':
+        #     for c in inp.connections:
+        #         self.remove_connection(c)
 
-        c = self.new_connection(out, inp)
+        c = DataConnection((out, inp, self)) if out.type_ == 'data' else ExecConnection((out, inp, self))
         self.add_connection(c)
 
-        if self.session.threading_enabled:
-            self.trigger_port_connected.emit(out)
-            self.trigger_port_connected.emit(inp)
-        else:
-            c.out.connected()
-            c.inp.connected()
-
-    def new_connection(self, out: NodeObjOutput, inp: NodeObjInput) -> Connection:
-        """Creates the connection object"""
-        c = None
-        if inp.type_ == 'data':
-            c = self.session.flow_data_conn_class((out, inp, self.session.design))
-        elif inp.type_ == 'exec':
-            c = self.session.flow_exec_conn_class((out, inp, self.session.design))
-        c.item.setZValue(10)
-        if self.session.threading_enabled:
-            c.moveToThread(self.worker_thread)
         return c
 
+
     def add_connection(self, c: Connection):
-        """Adds the connection object to the scene and sets it in the ports"""
         c.out.connections.append(c)
         c.inp.connections.append(c)
-
+        c.out.connected()
+        c.inp.connected()
         self.connections.append(c)
+        self.connection_added.emit(c)
 
-        self.scene().addItem(c.item)
-        self.viewport().repaint()
 
     def remove_connection(self, c: Connection):
-        """Removes the connection object from the scene and from the ports"""
         c.out.connections.remove(c)
         c.inp.connections.remove(c)
-
+        c.out.disconnected()
+        c.inp.disconnected()
         self.connections.remove(c)
+        self.connection_removed.emit(c)
+
+
+    # def connection_activation_request(self, c: Connection):
+    #     c.activate()
 
-        self.scene().removeItem(c.item)
-        self.viewport().repaint()
-
-    def auto_connect(self, p: NodeObjPort, n: Node):
-        if p.io_pos == PortObjPos.OUTPUT:
-            for inp in n.inputs:
-                if p.type_ == inp.type_:
-                    # connect exactly once
-                    self.connect_node_ports__cmd(p, inp)
-                    return
-        elif p.io_pos == PortObjPos.INPUT:
-            for out in n.outputs:
-                if p.type_ == out.type_:
-                    # connect exactly once
-                    self.connect_node_ports__cmd(p, out)
-                    return
-
-    # MODES API
 
     def algorithm_mode(self) -> str:
-        """Returns the current algorithm mode of the flow as string"""
+        """returns the current algorithm mode of the flow as string"""
+
         return FlowAlg.stringify(self.alg_mode)
 
+
     def set_algorithm_mode(self, mode: str):
         """
-        Sets the algorithm mode of the flow
-        :mode: 'data' or 'exec'
+        sets the algorithm mode of the flow
         """
+
         if mode == 'data':
             self.alg_mode = FlowAlg.DATA
         elif mode == 'exec':
@@ -1187,78 +233,57 @@ def set_algorithm_mode(self, mode: str):
 
         self.algorithm_mode_changed.emit(self.algorithm_mode())
 
-    def viewport_update_mode(self) -> str:
-        """Returns the current viewport update mode as string (sync or async) of the flow"""
-        return VPUpdateMode.stringify(self.vp_update_mode)
 
-    def set_viewport_update_mode(self, mode: str):
+    def generate_config_data(self):
         """
-        Sets the viewport update mode of the flow
-        :mode: 'sync' or 'async'
+        generates the abstract config data and saves as well as returns it as tuple (flow config, nodes, connections)
         """
-        if mode == 'sync':
-            self.vp_update_mode = VPUpdateMode.SYNC
-        elif mode == 'async':
-            self.vp_update_mode = VPUpdateMode.ASYNC
-
-        self.viewport_update_mode_changed.emit(self.viewport_update_mode())
-
-    def config_data(self) -> dict:
-        print(self.nodes)
-        flow_dict = {'algorithm mode': FlowAlg.stringify(self.alg_mode),
-                     'viewport update mode': VPUpdateMode.stringify(self.vp_update_mode),
-                     'nodes': self._get_nodes_config_data(self.nodes),
-                     'connections': self._get_connections_config_data(self.nodes),
-                     'drawings': self._get_drawings_config_data(self.drawings)}
-        return flow_dict
-
-    def _get_nodes_config_data(self, nodes):
-        nodes_data = []
-        for n in nodes:
-            nodes_data.append(n.config_data(n.item.config_data()))
 
-        return nodes_data
+        cfg = {'algorithm mode': FlowAlg.stringify(self.alg_mode)}, \
+            self.generate_nodes_config(self.nodes), \
+            self.generate_connections_config(self.nodes)
 
-    def _get_connections_config_data(self, nodes, only_with_connections_to=None):
-        script_ni_connections_list = []
-        for n in nodes:
-            for out in n.outputs:
-                if len(out.connections) > 0:
-                    for c in out.connections:
-                        connected_port = c.inp
-
-                        # this only applies when saving config data through deleting nodes:
-                        if only_with_connections_to is not None and \
-                                connected_port.node not in only_with_connections_to and \
-                                n 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 index': nodes.index(n),
-                                           'output port index': n.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_node_index = nodes.index(connected_port.node) if \
-                            connected_port.node in nodes else None
-
-                        connection_dict['connected node'] = connected_node_index
+        self._temp_config_data = cfg
+        return cfg
 
-                        connected_ip_index = connected_port.node.inputs.index(connected_port) if \
-                            connected_node_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_config_data(self, drawings):
-        drawings_list = []
-        for drawing in drawings:
-            drawing_dict = drawing.config_data()
-
-            drawings_list.append(drawing_dict)
-
-        return drawings_list
+    def generate_nodes_config(self, nodes: [Node]):
+        cfg = {}
+        for n in nodes:
+            cfg[n] = n.config_data()
+        self._temp_config_data = cfg
+        # self.nodes_config_generated.emit(cfg)
+        return cfg
+
+
+    def generate_connections_config(self, nodes: [Node]):
+        cfg = {}
+        for i in range(len(nodes)):
+            n = nodes[i]
+            for j in range(len(n.outputs)):
+                out = n.outputs[j]
+
+                for c in out.connections:
+                    connected_port = c.inp
+                    connected_node = connected_port.node
+
+                    # When copying components, there might be connections going outside the selected list of nodes.
+                    # These should be ignored.
+                    if connected_node not in nodes:
+                        continue
+
+                    connected_port_index = connected_node.inputs.index(connected_port)
+                    connected_node_index = nodes.index(connected_node)
+
+                    c_dict = {
+                        'parent node index': i,
+                        'output port index': j,
+                        'connected node': connected_node_index,
+                        'connected input port index': connected_port_index
+                    }
+
+                    cfg[c] = c_dict
+
+        self._temp_config_data = cfg
+        # self.connections_config_generated.emit(cfg)
+        return cfg
diff --git a/src/FlowCommands.py b/src/FlowCommands.py
index 80218a7..b6610ab 100644
--- a/src/FlowCommands.py
+++ b/src/FlowCommands.py
@@ -1,93 +1,167 @@
+from PySide2.QtCore import Signal, QObject
 from PySide2.QtWidgets import QUndoCommand
 
+from .DrawingObject import DrawingObject
+from .Node import Node
+from .Connection import Connection
 from .NodeItem import NodeItem
+from .NodeObjPort import NodeObjPort
 
 
-class MoveComponents_Command(QUndoCommand):
-    def __init__(self, flow, items_list, p_from, p_to):
-        super(MoveComponents_Command, self).__init__()
+class FlowUndoCommand(QUndoCommand, QObject):
+    """
+    The main difference to normal QUndoCommands is the activation feature. This allows the flow widget to add the
+    undo command to the undo stack before redo() is called. This is important since some of these commands can cause
+    other commands to be added while they are performing redo()
+    """
+
+    def __init__(self, flow_view):
+
+        self.flow_view = flow_view
+        self.flow = flow_view.flow
+        self._activated = False
+
+        QUndoCommand.__init__(self)
+        QObject.__init__(self)
+
+    def activate(self):
+        self._activated = True
+        self.redo()
+
+    def redo(self) -> None:
+        if not self._activated:
+            return
+        else:
+            self.redo_()
+
+    def undo(self) -> None:
+        self.undo_()
+
+    def redo_(self):
+        """subclassed"""
+        pass
+
+    def undo_(self):
+        """subclassed"""
+        pass
+
+
+class MoveComponents_Command(FlowUndoCommand):
+    def __init__(self, flow_view, items_list, p_from, p_to):
+        super(MoveComponents_Command, self).__init__(flow_view)
 
-        self.flow = flow
         self.items_list = items_list
         self.p_from = p_from
         self.p_to = p_to
         self.last_item_group_pos = p_to
 
-    def undo(self):
+    def undo_(self):
         items_group = self.items_group()
         items_group.setPos(self.p_from)
         self.last_item_group_pos = items_group.pos()
         self.destroy_items_group(items_group)
 
-    def redo(self):
+    def redo_(self):
         items_group = self.items_group()
         items_group.setPos(self.p_to - self.last_item_group_pos)
         self.destroy_items_group(items_group)
 
 
     def items_group(self):
-        return self.flow.scene().createItemGroup(self.items_list)
+        return self.flow_view.scene().createItemGroup(self.items_list)
 
     def destroy_items_group(self, items_group):
-        self.flow.scene().destroyItemGroup(items_group)
+        self.flow_view.scene().destroyItemGroup(items_group)
 
 
-class PlaceNodeItemInScene_Command(QUndoCommand):
-    def __init__(self, flow, node_item, pos):
-        super(PlaceNodeItemInScene_Command, self).__init__()
+class PlaceNode_Command(FlowUndoCommand):
 
-        self.flow = flow
-        self.node_item = node_item
-        self.NI_pos = pos
+    create_node_request = Signal(object)
+    add_node_request = Signal(Node)
+    remove_node_request = Signal(Node)
 
-    def undo(self):
-        self.flow.remove_node_item(self.node_item)
+    def __init__(self, flow_view, node_class, pos):
+        super().__init__(flow_view)
 
-    def redo(self):
-        self.flow.add_node_item(self.node_item, self.NI_pos)
+        self.node_class = node_class
+        self.node = None
+        self.item_pos = pos
 
+        self.create_node_request.connect(self.flow.create_node)
+        self.add_node_request.connect(self.flow.add_node)
+        self.remove_node_request.connect(self.flow.remove_node)
 
-class PlaceDrawingObject_Command(QUndoCommand):
-    def __init__(self, flow, posF, drawing_obj):
-        super(PlaceDrawingObject_Command, self).__init__()
+        self.flow_view.node_placed.connect(self.node_placed_in_flow)
 
-        self.flow = flow
+    def undo_(self):
+        self.remove_node_request.emit(self.node)
 
-        self.drawing_obj = drawing_obj
+    def redo_(self):
+        if self.node:
+            self.add_node_request.emit(self.node)
+        else:
+            self.flow_view.node_placed.connect(self.node_placed_in_flow)
+            self.create_node_request.emit(self.node_class)
+
+        # --> node_placed_in_flow()
+
+    def node_placed_in_flow(self, node):
+        self.flow_view.node_placed.disconnect(self.node_placed_in_flow)
+        self.node = node
+
+
+class PlaceDrawing_Command(FlowUndoCommand):
+    def __init__(self, flow_view, posF, drawing):
+        super().__init__(flow_view)
+
+        self.drawing = drawing
         self.drawing_obj_place_pos = posF
         self.drawing_obj_pos = self.drawing_obj_place_pos
 
-    def undo(self):
-        """Important: The drawing_obj_pos is not anymore the drawing_obj_place_pos here anymore because after the
-        drawing object was completed, it's actual position got recalculated according to all points and differs from
-        the initial pen press pos (=drawing_obj_place_pos). See DrawingObject.finished()."""
+    def undo_(self):
+        # The drawing_obj_pos is not anymore the drawing_obj_place_pos because after the
+        # drawing object was completed, its actual position got recalculated according to all points and differs from
+        # the initial pen press pos (=drawing_obj_place_pos). See DrawingObject.finished().
+
+        self.drawing_obj_pos = self.drawing.pos()
 
-        self.drawing_obj_pos = self.drawing_obj.pos()
+        self.flow_view.remove_component(self.drawing)
 
-        self.flow.remove_component(self.drawing_obj)
+    def redo_(self):
+        self.flow_view.add_drawing(self.drawing, self.drawing_obj_pos)
 
-    def redo(self):
-        self.flow.add_drawing(self.drawing_obj, self.drawing_obj_pos)
 
+class RemoveComponents_Command(FlowUndoCommand):
 
-class RemoveComponents_Command(QUndoCommand):
-    def __init__(self, flow, items):
-        super(RemoveComponents_Command, self).__init__()
+    add_node_request = Signal(Node)
+    remove_node_request = Signal(Node)
+
+    add_connection_request = Signal(Connection)
+    remove_connection_request = Signal(Connection)
+
+    def __init__(self, flow_view, items):
+        super().__init__(flow_view)
 
-        self.flow = flow
         self.items = items
-        self.broken_connections = []  # the connections that go beyond the removed ports and need to be restored in undo
+        self.broken_connections = []  # the connections that go beyond the removed nodes and need to be restored in undo
         self.internal_connections = set()
-        # self.connections = set()
+
+        # static connections
+        self.add_node_request.connect(self.flow.add_node)
+        self.remove_node_request.connect(self.flow.remove_node)
+        self.add_connection_request.connect(self.flow.add_connection)
+        self.remove_connection_request.connect(self.flow.remove_connection)
 
         self.node_items = []
         self.nodes = []
+        self.drawings = []
         for i in self.items:
             if isinstance(i, NodeItem):
                 self.node_items.append(i)
                 self.nodes.append(i.node)
+            elif isinstance(i, DrawingObject):
+                self.drawings.append(i)
 
-        self.connected_nodes_indices_not_in_del_selection = []
         for n in self.nodes:
             for i in n.inputs:
                 for c in i.connections:
@@ -106,118 +180,197 @@ def __init__(self, flow, items):
                     else:
                         self.internal_connections.add(c)
 
-    def undo(self):
-        for i in self.items:
-            self.flow.add_component(i)
-        # reconnect
+    def undo_(self):
+        # add connections
         self.restore_broken_connections()
-        for c in self.internal_connections:
-            self.flow.add_connection(c)
+        self.restore_internal_connections()
 
-        self.flow.select_components(self.items)
+        # add nodes
+        for n in self.nodes:
+            self.add_node_request.emit(n)
+
+        # add drawings
+        for d in self.drawings:
+            self.flow_view.add_drawing(d)
+
+    def redo_(self):
 
-    def redo(self):
-        # disconnect
+        # remove connections
         self.remove_broken_connections()
+        self.remove_internal_connections()
+
+        # remove nodes
+        for n in self.nodes:
+            self.remove_node_request.emit(n)
+
+        # remove drawings
+        for d in self.drawings:
+            self.flow_view.remove_drawing(d)
+
+    def restore_internal_connections(self):
         for c in self.internal_connections:
-            self.flow.remove_connection(c)
+            self.add_connection_request.emit(c)
 
-        for i in self.items:
-            self.flow.remove_component(i)
+    def remove_internal_connections(self):
+        for c in self.internal_connections:
+            self.remove_connection_request.emit(c)
 
     def restore_broken_connections(self):
-        for b_c in self.broken_connections:
-            self.flow.connect_ports(connection=b_c)
+        for c in self.broken_connections:
+            self.add_connection_request.emit(c)
 
     def remove_broken_connections(self):
-        for b_c in self.broken_connections:
-            self.flow.connect_ports(b_c.out, b_c.inp)
+        for c in self.broken_connections:
+            self.remove_connection_request.emit(c)
+
+
+class ConnectPorts_Command(FlowUndoCommand):
 
+    connect_request = Signal(NodeObjPort, NodeObjPort)
+    add_connection_request = Signal(Connection)
+    remove_connection_request = Signal(Connection)
 
-class ConnectPorts_Command(QUndoCommand):
-    def __init__(self, flow, out, inp):
-        super(ConnectPorts_Command, self).__init__()
+    def __init__(self, flow_view, out, inp):
+        super().__init__(flow_view)
+
+        # CAN ALSO LEAD TO DISCONNECT INSTEAD OF CONNECT!!
 
-        self.flow = flow
         self.out = out
         self.inp = inp
         self.connection = None
         self.connecting = True
 
+        # static connections
+        self.connect_request.connect(self.flow.connect_nodes)
+        self.add_connection_request.connect(self.flow.add_connection)
+        self.remove_connection_request.connect(self.flow.remove_connection)
+
         for c in self.out.connections:
             if c.inp == self.inp:
                 self.connection = c
                 self.connecting = False
 
-    def undo(self):
+
+    def undo_(self):
         if self.connecting:
-            # disconnect
-            self.flow.connect_ports(self.out, self.inp)
+            # remove connection
+            self.remove_connection_request.emit(self.connection)
         else:
-            # connect
-            self.flow.connect_ports(connection=self.connection)
+            # recreate former connection
+            self.add_connection_request.emit(self.connection)
 
-    def redo(self):
+    def redo_(self):
         if self.connecting:
+            if self.connection:
+                self.add_connection_request.emit(self.connection)
+            else:
+                # connection hasn't been created yet
+                self.flow.connection_added.connect(self.connection_created)
+                self.connect_request.emit(self.out, self.inp)
+        else:
+            # remove existing connection
+            self.remove_connection_request.emit(self.connection)
 
-            if not self.connection:
-                self.connection = self.flow.new_connection(self.out, self.inp)
+    def connection_created(self, c):
+        self.flow.connection_added.disconnect(self.connection_created)
+        self.connection = c
 
-            # connect using the new or cached connection
-            self.flow.connect_ports(connection=self.connection)
 
-        else:
-            # disconnect
-            self.flow.connect_ports(self.out, self.inp)
 
 
+class Paste_Command(FlowUndoCommand):
 
-class Paste_Command(QUndoCommand):
-    def __init__(self, flow, data, offset_for_middle_pos):
-        super(Paste_Command, self).__init__()
+    create_nodes_request = Signal(list)
+    create_connections_request = Signal(list, list)
 
-        self.flow = flow
-        self.data = data
-        self.offset_for_middle_pos = offset_for_middle_pos
-        self.pasted_items = None
-        self.pasted_nodes = None
-        self.pasted_drawing_objects = None
+    add_node_request = Signal(Node)
+    remove_node_request = Signal(Node)
 
-        # TODO: also cache pasted_connections; replace self.flow.connect_pins in undo with self.flow.remove_connection
-        #  and replace self.flow.connect_nodes_from_config with self.flow.add_connection(s) in redo
-        #  problem: Flow.connect_nodes_from_config has to return the new connections which might get messy...
+    add_connection_request = Signal(Connection)
+    remove_connection_request = Signal(Connection)
 
-    def undo(self):
-        self.flow.remove_components(self.pasted_items)
 
-        # remove connections
-        for ni in self.pasted_nodes:
-            for o in ni.outputs:
-                for i in range(len(o.connections)):
-                    c = o.connections[0]
-                    # disconnect pins here so they can be connected in connect_nodes_from_config in redo below again
-                    self.flow.connect_ports(c.out, c.inp)
-
-    def redo(self):
-        if self.pasted_items is None:
-            new_nodes, new_node_items = self.flow.place_nodes_from_config(
-                self.data['nodes'],
-                offset_pos=self.offset_for_middle_pos.toPoint()
-            )
-
-            self.flow.connect_nodes_from_config(new_nodes, self.data['connections'])
-
-            new_drawing_objects = self.flow.place_drawings_from_config(self.data['drawings'],
-                                                                       offset_pos=self.offset_for_middle_pos.toPoint())
-
-            self.pasted_items = new_node_items + new_drawing_objects
-            self.pasted_nodes = new_nodes
-            self.pasted_drawing_objects = new_drawing_objects
-        else:
-            self.flow.add_node_items([n.item for n in self.pasted_nodes])
-            self.flow.add_drawings(self.pasted_drawing_objects)
-
-            # not keeping them cached for now...
-            self.flow.connect_nodes_from_config(self.pasted_nodes, self.data['connections'])
+    def __init__(self, flow_view, data, offset_for_middle_pos):
+        super().__init__(flow_view)
 
-        self.flow.select_components(self.pasted_items)
+        self.data = data
+        self.modify_data_positions(offset_for_middle_pos)
+        self.pasted_components = None
+
+        # static connections
+        self.add_node_request.connect(self.flow.add_node)
+        self.remove_node_request.connect(self.flow.remove_node)
+        self.add_connection_request.connect(self.flow.add_connection)
+        self.remove_connection_request.connect(self.flow.remove_connection)
+
+
+    def modify_data_positions(self, offset):
+        """adds the offset to the components' positions in data"""
+
+        for node in self.data['nodes']:
+            node['pos x'] = node['pos x'] + offset.x()
+            node['pos y'] = node['pos y'] + offset.y()
+        for drawing in self.data['drawings']:
+            drawing['pos x'] = drawing['pos x'] + offset.x()
+            drawing['pos y'] = drawing['pos y'] + offset.y()
+
+
+    def connect_to_flow(self):
+        """creates temporary connections to retrieve the created components once"""
+        self.flow.nodes_created_from_config.connect(self.nodes_created)
+        self.flow.connections_created_from_config.connect(self.connections_created)
+        self.create_nodes_request.connect(self.flow.create_nodes_from_config)
+        self.create_connections_request.connect(self.flow.connect_nodes_from_config)
+
+    def disconnect_from_flow(self):
+        self.flow.nodes_created_from_config.disconnect(self.nodes_created)
+        self.flow.connections_created_from_config.disconnect(self.connections_created)
+        self.create_nodes_request.disconnect(self.flow.create_nodes_from_config)
+        self.create_connections_request.disconnect(self.flow.connect_nodes_from_config)
+
+
+    def redo_(self):
+        if self.pasted_components is None:
+            # create components
+            self.connect_to_flow()
+            self.create_nodes_request.emit(self.data['nodes'])
+            # --> nodes_created()
+        else:
+            self.add_existing_components()
+
+    def undo_(self):
+        # remove components and their items from flow
+        for n in self.pasted_components['nodes']:
+            self.remove_node_request.emit(n)
+        for c in self.pasted_components['connections']:
+            self.remove_connection_request.emit(c)
+        for d in self.pasted_components['drawings']:
+            self.flow_view.remove_drawing(d)
+
+    def add_existing_components(self):
+        # add existing components and items to flow
+        for n in self.pasted_components['nodes']:
+            self.add_node_request.emit(n)
+        for c in self.pasted_components['connections']:
+            self.add_connection_request.emit(c)
+        for d in self.pasted_components['drawings']:
+            self.flow_view.add_drawing(d)
+
+
+
+    def nodes_created(self, nodes):
+        self.pasted_components = {'nodes': nodes}
+        self.create_connections_request.emit(nodes, self.data['connections'])
+        # --> connections_created()
+
+    def connections_created(self, connections):
+        self.disconnect_from_flow()
+        self.pasted_components['connections'] = connections
+
+        self.create_drawings()
+
+    def create_drawings(self):
+        drawings = []
+        for d in self.data['drawings']:
+            self.flow_view.create_drawing(d)
+        self.pasted_components['drawings'] = drawings
diff --git a/src/FlowSessionThreadInterface.py b/src/FlowSessionThreadInterface.py
new file mode 100644
index 0000000..911c6b7
--- /dev/null
+++ b/src/FlowSessionThreadInterface.py
@@ -0,0 +1,10 @@
+from PySide2.QtCore import QObject, Signal
+
+
+class FlowSessionThreadInterface(QObject):
+
+    def trigger_node_action(self, method, data=None):
+        if data:
+            method(data)
+        else:
+            method()
diff --git a/src/FlowStylusModesWidget.py b/src/FlowStylusModesWidget.py
deleted file mode 100644
index 78dc9fa..0000000
--- a/src/FlowStylusModesWidget.py
+++ /dev/null
@@ -1,132 +0,0 @@
-from PySide2.QtWidgets import QWidget, QPushButton, QHBoxLayout, QSlider, QColorDialog
-from PySide2.QtCore import Qt
-from PySide2.QtGui import QColor
-
-
-class FlowStylusModesWidget(QWidget):
-    def __init__(self, flow):
-        super(FlowStylusModesWidget, self).__init__()
-
-        # GENERAL ATTRIBUTES
-        self.flow = flow
-        self.pen_color = QColor(255, 255, 0)
-
-        # stylus button
-        self.stylus_button = QPushButton('stylus')  # show/hide
-        self.stylus_button.clicked.connect(self.on_stylus_button_clicked)
-
-        # mode
-        self.set_stylus_mode_comment_button = QPushButton('comment')
-        self.set_stylus_mode_comment_button.clicked.connect(self.on_comment_button_clicked)
-        self.set_stylus_mode_edit_button = QPushButton('edit')
-        self.set_stylus_mode_edit_button.clicked.connect(self.on_edit_button_clicked)
-
-        # pen style
-        self.pen_color_button = QPushButton('color')
-        self.pen_color_button.clicked.connect(self.on_choose_color_clicked)
-        self.pen_width_slider = QSlider(Qt.Horizontal)
-        self.pen_width_slider.setRange(1, 100)  # -> /10
-        self.pen_width_slider.setValue(20)
-
-        self.pen_style_widget = QWidget()
-        pen_style_layout = QHBoxLayout()
-        pen_style_layout.addWidget(self.pen_color_button)
-        pen_style_layout.addWidget(self.pen_width_slider)
-
-        self.pen_style_widget.setLayout(pen_style_layout)
-
-        # MERGE SETTINGS
-        self.settings_widget = QWidget()
-        settings_layout = QHBoxLayout()
-        settings_layout.addWidget(self.pen_style_widget)
-        settings_layout.addWidget(self.set_stylus_mode_comment_button)
-        settings_layout.addWidget(self.set_stylus_mode_edit_button)
-
-        self.settings_widget.setLayout(settings_layout)
-
-
-        # MAIN LAYOUT
-        main_horizontal_layout = QHBoxLayout()
-
-        main_horizontal_layout.addWidget(self.settings_widget)
-        main_horizontal_layout.addWidget(self.stylus_button)
-
-        self.setLayout(main_horizontal_layout)
-
-
-        self.setStyleSheet('''
-            background: transparent;
-            border: none;
-        ''')
-
-        self.push_button_style_sheet_start = '''
-            QPushButton:hover:pressed {
-                background-color: rgba(100, 100, 100, 150);
-            }
-            QPushButton {
-                border: 1px solid #aaaaaa;
-                border-radius: 8px;
-                height: 25px;
-                width: 50px;'''  # color may be dynamic
-
-        self.std_push_button_style_sheet = self.push_button_style_sheet_start + '''
-                background-color: rgba(10, 10, 10, 150);
-                color: #aaaaaa;
-            }
-        '''
-
-        self.set_stylus_mode_comment_button.setStyleSheet(self.std_push_button_style_sheet)
-        self.set_stylus_mode_edit_button.setStyleSheet(self.std_push_button_style_sheet)
-        self.stylus_button.setStyleSheet(self.std_push_button_style_sheet)
-        self.update_color_button_SS()
-
-        self.stylus_button.setFixedWidth(50)
-        self.set_stylus_mode_edit_button.setFixedWidth(60)
-        self.set_stylus_mode_comment_button.setFixedWidth(60)
-
-        self.pen_style_widget.hide()
-        self.settings_widget.hide()
-
-
-    def on_stylus_button_clicked(self):
-        if self.settings_widget.isHidden():
-            self.settings_widget.show()
-        elif not self.settings_widget.isHidden():
-            self.settings_widget.hide()
-        self.adjustSize()
-        self.flow.set_stylus_proxy_pos()
-
-    def on_edit_button_clicked(self):
-        self.flow.stylus_mode = 'edit'
-        self.pen_style_widget.hide()
-
-        # if I don't hide and show the settings_widget manually here, the stylus mode buttons take up the additional
-        # space when clicking on comment and then edit. self.adjustSize() does not seem to work properly here...
-        self.settings_widget.hide()
-        self.settings_widget.show()
-
-        self.adjustSize()
-        self.flow.set_stylus_proxy_pos()
-        # self.flow.setDragMode(QGraphicsView.RubberBandDrag)
-
-    def on_comment_button_clicked(self):
-        self.flow.stylus_mode = 'comment'
-        self.pen_style_widget.show()
-        self.adjustSize()
-        self.flow.set_stylus_proxy_pos()
-        # self.flow.setDragMode(QGraphicsView.NoDrag)
-
-    def on_choose_color_clicked(self):
-        self.pen_color = QColorDialog.getColor(self.pen_color, options=QColorDialog.ShowAlphaChannel,
-                                               title='Choose pen color')
-        self.update_color_button_SS()
-
-    def update_color_button_SS(self):
-        self.pen_color_button.setStyleSheet(self.push_button_style_sheet_start+'''
-                color: black;
-                background-color: '''+self.pen_color.name()+''';
-            }''')
-
-    def get_pen_settings(self):
-        return {'color': self.pen_color.name(),
-                'base stroke weight': self.pen_width_slider.value()/10}
\ No newline at end of file
diff --git a/src/FlowTheme.py b/src/FlowTheme.py
new file mode 100644
index 0000000..cc6be5b
--- /dev/null
+++ b/src/FlowTheme.py
@@ -0,0 +1,1266 @@
+from PySide2.QtCore import Qt, QPointF, QRectF
+from PySide2.QtGui import QColor, QPainter, QBrush, QRadialGradient, QLinearGradient, QPen, QPainterPath, QFont
+from PySide2.QtWidgets import QStyle, QStyleOption
+
+from .global_tools.math import pythagoras
+
+
+class FlowTheme:
+    
+    # STATIC ATTRIBUTES
+    name = ''
+    type_ = 'dark'
+    
+    exec_conn_color = QColor('#ffffff')
+    exec_conn_width = 1.5
+    exec_conn_pen_style = Qt.SolidLine
+    
+    data_conn_color = QColor('#ffffff')
+    data_conn_width = 1.5
+    data_conn_pen_style = Qt.DashLine
+    
+    flow_background_brush = QBrush(QColor('#333333'))
+    flow_background_grid = None
+    flow_highlight_pen_color = QColor('#245d75')
+
+    node_item_shadow_color = QColor('#2b2b2b')
+
+    EXPORT = []
+
+
+    def __init__(self):
+        pass
+
+    def load(self, data: dict):
+        if data and self.name in data.keys():
+            imported = {}
+            for k, v in data[self.name].items():
+                if v != 'default':
+                    imported[k] = v
+            self._load(imported)
+
+    def _load(self, imported: dict):
+        pass
+
+    def paint_NI_title_label(self, node, painter: QPainter, option: QStyleOption, hovering: bool, node_style: str, 
+                             node_title: str, node_color: QColor, node_item_bounding_rect):
+        pass
+
+    def paint_PI_label(self, node, painter: QPainter, option: QStyleOption, type_: str, connected: bool, label_str: str, 
+                       node_color: QColor, bounding_rect: QRectF):
+        pass
+
+    def paint_PI(self, node, painter: QPainter, option: QStyleOption, node_color: QColor, type_: str, connected: bool,
+                 padding, w, h):
+        pass
+
+    def paint_NI(self, node, node_style: str,
+                 painter: QPainter, option: QStyleOption,
+                 color: QColor, w, h, bounding_rect, title_rect):
+
+        painter.setRenderHint(QPainter.Antialiasing)
+
+        if node_style == 'extended':
+            self.draw_NI_extended(node, painter, color, w, h, bounding_rect, title_rect)
+        elif node_style == 'small':
+            self.draw_NI_small(node, painter, color, w, h, bounding_rect)
+
+    def draw_NI_extended(self, node, painter: QPainter, c: QColor, w, h, bounding_rect, title_rect):
+        pass
+
+    def draw_NI_small(self, node, painter: QPainter, c: QColor, w, h, bounding_rect, background_color=None):
+        pass
+
+
+
+
+
+    @staticmethod
+    def paint_NI_title_label_default(painter: QPainter, node_style: str, title: str, color: QColor, pen_w: float,
+                                     font: QFont, node_item_bounding_rect):
+        pen = QPen(color)
+        pen.setWidth(pen_w)
+
+        painter.setPen(pen)
+        painter.setFont(font)
+
+        text_rect = node_item_bounding_rect
+        text_rect.setTop(text_rect.top()-7)
+
+        if node_style == 'extended':
+            painter.drawText(text_rect, Qt.AlignLeft | Qt.AlignVCenter, title)
+        elif node_style == 'small':
+            painter.drawText(text_rect, Qt.AlignTop | Qt.AlignHCenter, title)
+
+    @staticmethod
+    def paint_PI_label_default(painter: QPainter, label_str: str, color: QColor, font: QFont, bounding_rect: QRectF):
+        painter.setBrush(Qt.NoBrush)
+        pen = QPen(color)
+        painter.setPen(pen)
+        painter.setFont(font)
+        painter.drawText(bounding_rect, Qt.AlignCenter, label_str)
+
+    @staticmethod
+    def get_header_rect(node_width, node_height, title_rect):
+        header_height = 1.4 * title_rect.height()  # 35 * (self.parent_node.title.count('\n')+1)
+
+        header_rect = QRectF()
+        header_rect.setTopLeft(QPointF(-node_width / 2, -node_height / 2))
+        header_rect.setWidth(node_width)
+        header_rect.setHeight(header_height)
+        return header_rect
+    
+    @staticmethod
+    def interpolate_color(c1, c2, val):
+        r1 = c1.red()
+        g1 = c1.green()
+        b1 = c2.blue()
+        a1 = c1.alpha()
+
+        r2 = c2.red()
+        g2 = c2.green()
+        b2 = c2.blue()
+        a2 = c2.alpha()
+
+        r = (r2 - r1) * val + r1
+        g = (g2 - g1) * val + g1
+        b = (b2 - b1) * val + b1
+        a = (a2 - a1) * val + a1
+
+        return QColor(r, g, b, a)
+
+    @staticmethod
+    def hex_to_col(hex_str: str) -> QColor:
+        """Converts a hex value in format '#xxxxxx[xx]' to QColor using alpha value if [xx] is used."""
+
+        h = hex_str.lstrip('#')
+
+        if len(h) == 6:
+            r, g, b = tuple(
+                int(h[i:i + 2], 16) for i in (0, 2, 4)
+            )
+            return QColor(r, g, b)
+        elif len(h) == 8:
+            r, g, b, a = tuple(
+                int(h[i:i + 2], 16) for i in (0, 2, 4, 6)
+            )
+            return QColor(r, g, b, a)
+
+        return None
+
+
+class FlowTheme_Toy(FlowTheme):
+
+
+    name = 'Toy'
+
+    exec_conn_color = QColor(188, 187, 242)
+    exec_conn_width = 5
+    exec_conn_pen_style = Qt.SolidLine
+
+    data_conn_color = QColor(188, 187, 242)
+    data_conn_width = 5
+    data_conn_pen_style = Qt.DashLine
+
+    flow_background_brush = QBrush(QColor('#333333'))
+
+
+    def paint_NI_title_label(self, node, painter, option, hovering, node_style, node_title, node_color, 
+                             node_item_bounding_rect):
+        
+        if node_style == 'extended':
+            self.paint_NI_title_label_default(
+                painter=painter, 
+                node_style=node_style, 
+                title=node_title, 
+                color=QColor(30, 43, 48) if not hovering else node_color.lighter(),
+                pen_w=2 if hovering else 1.5,
+                font=QFont('Poppins', 15),
+                node_item_bounding_rect=node_item_bounding_rect
+            )
+        else:
+            self.paint_NI_title_label_default(
+                painter=painter, 
+                node_style=node_style, 
+                title=node_title, 
+                color=QColor(30, 43, 48) if not hovering else node_color.lighter(),
+                pen_w=1.5,
+                font=QFont('K2D', 20, QFont.Bold, True),
+                node_item_bounding_rect=node_item_bounding_rect
+            )
+    
+    def paint_PI_label(self, node, painter, option, type_, connected, label_str, node_color, bounding_rect):
+        c = QColor('#FFFFFF')
+        self.paint_PI_label_default(painter, label_str, c, QFont("Source Code Pro", 10, QFont.Bold), bounding_rect)
+    
+    def paint_PI(self, node, painter, option, node_color, type_, connected, padding, w, h):
+
+        color = QColor('#2E688C') if type_ == 'data' else QColor('#3880ad')
+        if option.state & QStyle.State_MouseOver:
+            color = color.lighter()
+
+        brush = QBrush(QColor(color))
+        painter.setBrush(brush)
+        painter.setPen(Qt.NoPen)
+
+        painter.drawEllipse(QRectF(padding, padding, w, h))
+
+
+    def draw_NI_extended(self, node, painter: QPainter, c: QColor, w, h, bounding_rect, title_rect):
+
+        # main rect
+        body_gradient = QRadialGradient(bounding_rect.topLeft(), pythagoras(h, w))
+        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(bounding_rect, 12, 12)
+
+        header_gradient = QLinearGradient(FlowTheme_Toy.get_header_rect(w, h, title_rect).topRight(),
+                                          FlowTheme_Toy.get_header_rect(w, h, title_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(FlowTheme_Toy.get_header_rect(w, h, title_rect), 12, 12)
+
+
+    def draw_NI_small(self, node, painter: QPainter, c: QColor, w, h, bounding_rect, background_color=None):
+
+        path = QPainterPath()
+        path.moveTo(-w / 2, 0)
+
+        path.cubicTo(-w / 2, -h / 2,
+                     -w / 2, -h / 2,
+                     0, -h / 2)
+        path.cubicTo(+w / 2, -h / 2,
+                     +w / 2, -h / 2,
+                     +w / 2, 0)
+        path.cubicTo(+w / 2, +h / 2,
+                     +w / 2, +h / 2,
+                     0, +h / 2)
+        path.cubicTo(-w / 2, +h / 2,
+                     -w / 2, +h / 2,
+                     -w / 2, 0)
+        path.closeSubpath()
+
+        body_gradient = QLinearGradient(bounding_rect.bottomLeft(),
+                                        bounding_rect.topRight())
+        body_gradient.setColorAt(0, QColor(c.red(), c.green(), c.blue(), 150))
+        body_gradient.setColorAt(1, QColor(c.red(), c.green(), c.blue(), 80))
+
+        painter.setBrush(body_gradient)
+        painter.setPen(QPen(QColor(30, 43, 48)))
+
+        painter.drawPath(path)
+
+
+class FlowTheme_DarkTron(FlowTheme):
+
+
+    name = 'Tron'
+
+    exec_conn_color = QColor(0, 120, 180)
+    exec_conn_width = 4
+    exec_conn_pen_style = Qt.SolidLine
+
+    data_conn_color = QColor(0, 120, 180)
+    data_conn_width = 4
+    data_conn_pen_style = Qt.DashLine
+
+    flow_background_brush = QBrush(QColor('#333333'))
+
+    
+    def paint_NI_title_label(self, node, painter, option, hovering, node_style, node_title, node_color, node_item_bounding_rect):
+        if node_style == 'extended':
+            self.paint_NI_title_label_default(
+                painter=painter, 
+                node_style=node_style, 
+                title=node_title, 
+                color=node_color if not hovering else node_color.lighter(),
+                pen_w=2,
+                font=QFont('Poppins', 15),
+                node_item_bounding_rect=node_item_bounding_rect
+            )
+        else:
+            self.paint_NI_title_label_default(
+                painter=painter, 
+                node_style=node_style, 
+                title=node_title, 
+                color=node_color,
+                pen_w=2,
+                font=QFont('K2D', 20, QFont.Bold, True),
+                node_item_bounding_rect=node_item_bounding_rect
+            )
+
+
+    def paint_PI_label(self, node, painter, option, type_, connected, label_str, node_color, bounding_rect):
+        if type_ == 'exec':
+            c = QColor('#FFFFFF')
+        else:
+            c = node_color
+
+        self.paint_PI_label_default(painter, label_str, c, QFont("Source Code Pro", 10, QFont.Bold), bounding_rect)
+
+
+    def paint_PI(self, node, painter, option, node_color, type_, connected, padding, w, h):
+
+        color = QColor('#FFFFFF') if type_ == 'exec' else node_color
+        pen = QPen(color)
+        pen.setWidth(2)
+        painter.setPen(pen)
+        if connected or \
+                option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
+            r = node_color.red()
+            g = node_color.green()
+            b = node_color.blue()
+            brush = QBrush(QColor(r, g, b, 100))
+            painter.setBrush(brush)
+        else:
+            painter.setBrush(Qt.NoBrush)
+
+        painter.drawEllipse(QRectF(padding, padding, w, h))
+
+
+    def paint_NI(self, node, node_style,
+                 painter, option,
+                 color: QColor, w: int, h: int, bounding_rect, title_rect):
+
+        painter.setRenderHint(QPainter.Antialiasing)
+
+        if node_style == 'extended':
+            self.draw_NI_extended(node, painter, color, w, h, bounding_rect, title_rect)
+        elif node_style == 'small':
+            if option.state & QStyle.State_MouseOver:  # use special dark background color when mouse hovers
+                self.draw_NI_small(node, painter, color, w, h, bounding_rect, color.darker())
+            else:
+                self.draw_NI_small(node, painter, color, w, h, bounding_rect, QColor('#212429'))
+
+
+    def draw_NI_extended(self, node, painter, c: QColor, w: int, h: int, bounding_rect, title_rect):
+
+        background_color = QColor('#212224')
+        painter.setBrush(background_color)
+        pen = QPen(c)
+        pen.setWidth(2)
+        painter.setPen(pen)
+        body_path = self.get_extended_body_path(w, h)
+        painter.drawPath(body_path)
+
+        header_gradient = QLinearGradient(self.get_header_rect(w, h, title_rect).topRight(),
+                                          self.get_header_rect(w, h, title_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(w, h, title_rect)
+        painter.drawPath(header_path)
+
+
+    @staticmethod
+    def get_extended_body_path(w, h):
+        
+        c_s = 10  # corner size
+
+        path = QPainterPath()
+        path.moveTo(+w / 2, -h / 2 + c_s)
+        path.lineTo(+w / 2 - c_s, -h / 2)
+        path.lineTo(-w / 2 + c_s, -h / 2)
+        path.lineTo(-w / 2, -h / 2 + c_s)
+        path.lineTo(-w / 2, +h / 2 - c_s)
+        path.lineTo(-w / 2 + c_s, +h / 2)
+        path.lineTo(+w / 2 - c_s, +h / 2)
+        path.lineTo(+w / 2, +h / 2 - c_s)
+        path.closeSubpath()
+        return path
+
+    
+    def get_extended_header_path(self, w, h, title_rect):
+        
+        c_s = 10  # corner size
+
+        # header_height = 35 * (NIPainter_DarkTron.ni.parent_node.title.count('\n') + 1)
+        header_height = self.get_header_rect(w, h, title_rect).height()
+        header_bottom = -h / 2 + header_height
+        path = QPainterPath()
+        path.moveTo(+w / 2, -h / 2 + c_s)
+        path.lineTo(+w / 2 - c_s, -h / 2)
+        path.lineTo(-w / 2 + c_s, -h / 2)
+        path.lineTo(-w / 2, -h / 2 + c_s)
+        path.lineTo(-w / 2, header_bottom - c_s)
+        path.lineTo(-w / 2 + c_s, header_bottom)
+        path.lineTo(+w / 2 - c_s, header_bottom)
+        path.lineTo(+w / 2, header_bottom - c_s)
+        path.closeSubpath()
+        return path
+
+
+    def draw_NI_small(self, node, painter: QPainter, c: QColor, w, h, bounding_rect, background_color=None):
+
+        c_s = 10  # corner size
+
+        path = QPainterPath()
+        path.moveTo(-w / 2, 0)
+
+        path.lineTo(-w / 2 + c_s / 2, -h / 2 + c_s / 2)
+        path.lineTo(0, -h / 2)
+        path.lineTo(+w / 2 - c_s / 2, -h / 2 + c_s / 2)
+        path.lineTo(+w / 2, 0)
+        path.lineTo(+w / 2 - c_s / 2, +h / 2 - c_s / 2)
+        path.lineTo(0, +h / 2)
+        path.lineTo(-w / 2 + c_s / 2, +h / 2 - c_s / 2)
+        path.closeSubpath()
+
+        painter.setBrush(background_color)
+        pen = QPen(c)
+        pen.setWidth(2)
+        painter.setPen(pen)
+
+        painter.drawPath(path)
+
+
+class FlowTheme_Ghost(FlowTheme):
+
+
+    name = 'Ghost'
+    type_ = 'dark'
+
+    exec_conn_color = QColor(0, 17, 25)
+    exec_conn_width = 2
+    exec_conn_pen_style = Qt.SolidLine
+
+    data_conn_color = QColor(0, 17, 25)
+    data_conn_width = 2
+    data_conn_pen_style = Qt.DashLine
+
+    flow_background_color = QColor('#333333')
+    flow_background_brush = QBrush(flow_background_color)
+
+    node_color = QColor(28, 28, 28, 170)
+    node_small_color = QColor('#212429')
+
+    EXPORT = [
+        'nodes color',
+        'small nodes color',
+        'flow background color'
+    ]
+
+
+    def _load(self, imported: dict):
+        for k, v in imported.items():
+            if k == 'nodes color':
+                self.node_color = self.hex_to_col(v)
+            elif k == 'small nodes color':
+                self.node_small_color = self.hex_to_col(v)
+            elif k == 'flow background color':
+                self.flow_background_color = self.hex_to_col(v)
+                self.flow_background_brush = QBrush(self.flow_background_color)
+
+
+    def paint_NI_title_label(self, node, painter, option, hovering, node_style, node_title, node_color, 
+                             node_item_bounding_rect):
+        
+        if node_style == 'extended':
+            self.paint_NI_title_label_default(
+                painter=painter, 
+                node_style=node_style, 
+                title=node_title, 
+                color=node_color if not hovering else node_color.lighter(),
+                pen_w=2,
+                font=QFont('Poppins', 15),
+                node_item_bounding_rect=node_item_bounding_rect
+            )
+        else:
+            self.paint_NI_title_label_default(
+                painter=painter, 
+                node_style=node_style, 
+                title=node_title, 
+                color=node_color,
+                pen_w=2,
+                font=QFont('K2D', 20, QFont.Bold, True),
+                node_item_bounding_rect=node_item_bounding_rect
+            )
+    
+    def paint_PI_label(self, node, painter, option, type_, connected, label_str, node_color, bounding_rect):
+        if type_ == 'exec':
+            c = QColor('#FFFFFF')
+        else:
+            c = node_color
+        
+        self.paint_PI_label_default(painter, label_str, c, QFont("Source Code Pro", 10, QFont.Bold), bounding_rect)
+    
+    def paint_PI(self, node, painter, option, node_color, type_, connected, padding, w, h):
+
+        color = QColor('#FFFFFF') if type_ == 'exec' else node_color
+
+        if type_ == 'exec':
+            if connected or \
+                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
+                brush = QBrush(QColor(255, 255, 255, 100))
+                painter.setBrush(brush)
+            else:
+                painter.setBrush(Qt.NoBrush)
+        elif type_ == 'data':
+            if connected or \
+                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
+                r = node_color.red()
+                g = node_color.green()
+                b = node_color.blue()
+                brush = QBrush(QColor(r, g, b, 100))
+                painter.setBrush(brush)
+            else:
+                painter.setBrush(Qt.NoBrush)
+
+        pen = QPen(color)
+        pen.setWidth(1)
+        painter.setPen(pen)
+        painter.drawEllipse(QRectF(padding+w/4, padding+h/4, w/2, h/2))
+
+
+    def paint_NI(self, node, node_style,
+                 painter, option,
+                 color: QColor, w: int, h: int, bounding_rect, title_rect):
+
+        painter.setRenderHint(QPainter.Antialiasing)
+
+        if node_style == 'extended':
+            self.draw_NI_extended(node, painter, color, w, h, bounding_rect, title_rect)
+        elif node_style == 'small':
+            if option.state & QStyle.State_MouseOver:  # use special dark background color when mouse hovers
+                self.draw_NI_small(node, painter, color, w, h, bounding_rect, background_color=color.darker())
+            else:
+                self.draw_NI_small(node, painter, color, w, h, bounding_rect)
+
+
+    def draw_NI_extended(self, node, painter, c, w, h, bounding_rect, title_rect):
+
+        background_color = self.node_color
+
+        # body_gradient = QRadialGradient(QPointF(bounding_rect.topLeft().x() + w,
+        #                                         bounding_rect.topLeft().y() - h),
+        #                                 pythagoras(2 * h, 2 * w))
+        # body_gradient.setColorAt(0, QColor(c.red() / 10 + 100, c.green() / 10 + 100, c.blue() / 10 + 100, 100))
+        # body_gradient.setColorAt(0.7, background_color)
+        # body_gradient.setColorAt(1, background_color)
+        #
+        # painter.setBrush(body_gradient)
+
+        painter.setBrush(background_color)
+        pen = QPen(c.darker())
+        pen.setWidth(1)
+        painter.setPen(pen)
+        body_path = self.get_extended_body_path(5, w, h)
+        painter.drawPath(body_path)
+
+
+    @staticmethod
+    def get_extended_body_path(c_s, w, h):
+
+        path = QPainterPath()
+        path.moveTo(+w / 2, -h / 2 + c_s)
+        path.lineTo(+w / 2 - c_s, -h / 2)
+        path.lineTo(-w / 2 + c_s, -h / 2)
+        path.lineTo(-w / 2, -h / 2 + c_s)
+        path.lineTo(-w / 2, +h / 2 - c_s)
+        path.lineTo(-w / 2 + c_s, +h / 2)
+        path.lineTo(+w / 2 - c_s, +h / 2)
+        path.lineTo(+w / 2, +h / 2 - c_s)
+        path.closeSubpath()
+        return path
+
+
+    def draw_NI_small(self, node, painter, c, w, h, bounding_rect, background_color=None):
+
+        background_color = self.node_small_color
+        c_s = 10  # corner size
+        
+        path = self.get_extended_body_path(c_s, w, h)  # equals the small in this case
+
+        painter.setBrush(background_color)
+        # pen = QPen(QColor('#333333'))  # QPen(c)
+        # pen.setWidth(1)
+        # painter.setPen(pen)
+        painter.setPen(Qt.NoPen)
+
+        painter.drawPath(path)
+
+
+class FlowTheme_Blender(FlowTheme):
+
+
+    name = 'Blender'
+
+    exec_conn_color = QColor(0, 17, 25)
+    exec_conn_width = 2
+    exec_conn_pen_style = Qt.SolidLine
+
+    data_conn_color = QColor(0, 17, 25)
+    data_conn_width = 2
+    data_conn_pen_style = Qt.DashLine
+
+    flow_background_color = QColor('#333333')
+    flow_background_brush = QBrush(flow_background_color)
+
+    node_color = QColor(100, 100, 100, 150)
+
+    EXPORT = [
+        'nodes color',
+        'flow background color'
+    ]
+
+
+    def _load(self, imported: dict):
+        for k, v in imported.items():
+            if k == 'nodes color':
+                self.node_color = self.hex_to_col(v)
+            elif k == 'flow background color':
+                self.flow_background_color = self.hex_to_col(v)
+                self.flow_background_brush = QBrush(self.flow_background_color)
+
+
+    def paint_NI_title_label(self, node, painter, option, hovering, node_style, node_title, node_color, node_item_bounding_rect):
+        if node_style == 'extended':
+            self.paint_NI_title_label_default(
+                painter=painter, 
+                node_style=node_style, 
+                title=node_title, 
+                color=QColor('#FFFFFF'),
+                pen_w=2,
+                font=QFont('Poppins', 15),
+                node_item_bounding_rect=node_item_bounding_rect
+            )
+        else:
+            self.paint_NI_title_label_default(
+                painter=painter, 
+                node_style=node_style, 
+                title=node_title, 
+                color=node_color,
+                pen_w=2,
+                font=QFont('K2D', 20, QFont.Bold, True),
+                node_item_bounding_rect=node_item_bounding_rect
+            )
+
+
+    def paint_PI_label(self, node, painter, option, type_, connected, label_str, node_color, bounding_rect):
+        if type_ == 'exec':
+            c = QColor('#FFFFFF')
+        else:
+            c = node_color
+        
+        self.paint_PI_label_default(painter, label_str, c, QFont("Source Code Pro", 10, QFont.Bold), bounding_rect)
+
+
+    def paint_PI(self, node, painter, option, node_color, type_, connected, padding, w, h):
+
+        color = QColor('#FFFFFF') if type_ == 'exec' else node_color
+
+        if type_ == 'exec':
+            if connected or \
+                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
+                brush = QBrush(QColor(255, 255, 255, 100))
+                painter.setBrush(brush)
+            else:
+                painter.setBrush(Qt.NoBrush)
+        elif type_ == 'data':
+            if connected or \
+                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
+                r = node_color.red()
+                g = node_color.green()
+                b = node_color.blue()
+                brush = QBrush(QColor(r, g, b, 100))
+                painter.setBrush(brush)
+            else:
+                painter.setBrush(Qt.NoBrush)
+
+        pen = QPen(color)
+        pen.setWidth(1)
+        painter.setPen(pen)
+        painter.drawEllipse(QRectF(padding + w / 4, padding + h / 4, w / 2, h / 2))
+
+    
+    def draw_NI_extended(self, node, painter, c, w, h, bounding_rect, title_rect):
+
+        background_color = self.node_color
+        header_color = QColor(c.red(), c.green(), c.blue(), 180)
+
+        rel_header_height = self.get_header_rect(w, h, title_rect).height() / h
+        gradient = QLinearGradient(bounding_rect.topLeft(), bounding_rect.bottomLeft())
+        gradient.setColorAt(0, header_color)
+        gradient.setColorAt(rel_header_height, header_color)
+        gradient.setColorAt(rel_header_height+0.0001, background_color)
+        gradient.setColorAt(1, background_color)
+
+        painter.setBrush(gradient)
+        painter.setPen(QPen(c.darker()))
+        painter.drawRoundedRect(bounding_rect, 7, 7)
+
+
+    def draw_NI_small(self, node, painter, c, w, h, bounding_rect, background_color=None):
+
+        background_color = QColor('#212429')
+        c_s = 15
+        painter.setBrush(self.interpolate_color(c, background_color, 0.97))
+        painter.setPen(QPen(c))
+        painter.drawRoundedRect(bounding_rect, c_s, c_s)
+
+
+class FlowTheme_Simple(FlowTheme):
+
+
+    name = 'Simple'
+    type_ = 'dark'
+
+    exec_conn_color = QColor('#989c9f')
+    exec_conn_width = 2
+    exec_conn_pen_style = Qt.SolidLine
+
+    data_conn_color = QColor('#989c9f')
+    data_conn_width = 2
+    data_conn_pen_style = Qt.DashLine
+
+    flow_background_color = QColor('#3f4044')
+    flow_background_brush = QBrush(flow_background_color)
+
+    node_background_color = QColor('#212429')
+    node_small_background_color = QColor('#212429')
+
+    EXPORT = [
+        'nodes background color',
+        'small nodes background color',
+        'flow background color'
+    ]
+
+
+    def _load(self, imported: dict):
+        for k, v in imported.items():
+            if k == 'nodes background color':
+                self.node_background_color = self.hex_to_col(v)
+            elif k == 'small nodes background color':
+                self.node_small_background_color = self.hex_to_col(v)
+            elif k == 'flow background color':
+                self.flow_background_color = self.hex_to_col(v)
+                self.flow_background_brush = QBrush(self.flow_background_color)
+
+
+    def paint_NI_title_label(self, node, painter, option, hovering, node_style, node_title, node_color, node_item_bounding_rect):
+        if node_style == 'extended':
+            self.paint_NI_title_label_default(
+                painter=painter,
+                node_style=node_style,
+                title=node_title,
+                color=QColor('#312b29'),
+                pen_w=2,
+                font=QFont('ASAP', 13, QFont.Bold),
+                node_item_bounding_rect=node_item_bounding_rect
+            )
+        else:
+            self.paint_NI_title_label_default(
+                painter=painter,
+                node_style=node_style,
+                title=node_title,
+                color=node_color,
+                pen_w=2,
+                font=QFont('Poppins', 15, QFont.Thin),
+                node_item_bounding_rect=node_item_bounding_rect
+            )
+
+
+    def paint_PI_label(self, node, painter, option, type_, connected, label_str, node_color, bounding_rect):
+        c = None
+        if not connected:
+            c = QColor('#53585c')
+        else:
+            if type_ == 'exec':
+                c = QColor('#cccccc')
+            else:
+                c = node_color
+
+        self.paint_PI_label_default(painter, label_str, c, QFont("Courier New", 10, QFont.Bold), bounding_rect)
+
+
+    def paint_PI(self, node, painter, option, node_color, type_, connected, padding, w, h):
+
+        color = None
+        if not connected:
+            color = QColor('#53585c')
+        else:
+            if type_ == 'exec':
+                color = QColor('#dddddd')
+            else:
+                color = node_color
+
+        if type_ == 'exec':
+            if connected or \
+                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
+                brush = QBrush(QColor(255, 255, 255, 100))
+                painter.setBrush(brush)
+            else:
+                painter.setBrush(Qt.NoBrush)
+        elif type_ == 'data':
+            if connected or \
+                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
+                r = node_color.red()
+                g = node_color.green()
+                b = node_color.blue()
+                brush = QBrush(QColor(r, g, b, 100))
+                painter.setBrush(brush)
+            else:
+                painter.setBrush(Qt.NoBrush)
+
+        brush = QBrush(QColor(color))
+        painter.setBrush(brush)
+        painter.setPen(Qt.NoPen)
+
+        painter.drawEllipse(QRectF(padding+w/8, padding+h/8, 3*w/4, 3*h/4))
+
+
+    def draw_NI_extended(self, node, painter, c, w, h, bounding_rect, title_rect):
+
+        background_color = self.node_background_color
+        header_color = c
+
+        rel_header_height = self.get_header_rect(w, h, title_rect).height() / h
+        gradient = QLinearGradient(bounding_rect.topLeft(), bounding_rect.bottomLeft())
+        gradient.setColorAt(0, header_color)
+        gradient.setColorAt(rel_header_height, header_color)
+        gradient.setColorAt(rel_header_height+0.0001, background_color)
+        gradient.setColorAt(1, background_color)
+
+        painter.setBrush(gradient)
+        painter.setPen(Qt.NoPen)  # QPen(c.darker()))
+        painter.drawRoundedRect(bounding_rect, 9, 9)
+
+
+    def draw_NI_small(self, node, painter, c, w, h, bounding_rect, background_color=None):
+
+        background_color = self.node_small_background_color
+        c_s = 10
+        painter.setBrush(self.interpolate_color(c, background_color, 0.97))
+        painter.setPen(Qt.NoPen)
+        painter.drawRoundedRect(bounding_rect, c_s, c_s)
+
+
+class FlowTheme_Ueli(FlowTheme):
+
+
+    name = 'Ueli'
+
+    exec_conn_color = QColor('#989c9f')
+    exec_conn_width = 2
+    exec_conn_pen_style = Qt.SolidLine
+
+    data_conn_color = QColor('#989c9f')
+    data_conn_width = 2
+    data_conn_pen_style = Qt.DashLine
+
+    flow_background_color = QColor('#3f4044')
+    flow_background_brush = QBrush(flow_background_color)
+
+    nodes_background_color = QColor('#212429')
+    small_nodes_background_color = nodes_background_color
+
+    EXPORT = [
+        'nodes background color',
+        'small nodes background color',
+        'flow background color'
+    ]
+
+
+    def _load(self, imported: dict):
+        for k, v in imported.items():
+            if k == 'nodes background color':
+                c = self.hex_to_col(v)
+                self.nodes_background_color = c
+            elif k == 'small nodes background color':
+                self.small_nodes_background_color = self.hex_to_col(v)
+            elif k == 'flow background color':
+                self.flow_background_color = self.hex_to_col(v)
+                self.flow_background_brush = QBrush(self.flow_background_color)
+
+
+    def paint_NI_title_label(self, node, painter, option, hovering, node_style, node_title, node_color, node_item_bounding_rect):
+        if node_style == 'extended':
+            painter.setPen(QPen(QColor(node_color.name())))
+            painter.setFont(QFont('Poppins', 13))
+            painter.drawText(node_item_bounding_rect, Qt.AlignLeft | Qt.AlignVCenter, node_title)
+        else:
+            self.paint_NI_title_label_default(
+                painter=painter, 
+                node_style=node_style, 
+                title=node_title, 
+                color=node_color,
+                pen_w=2,
+                font=QFont('Poppins', 15, QFont.Thin),
+                node_item_bounding_rect=node_item_bounding_rect
+            )
+
+
+    def paint_PI_label(self, node, painter, option, type_, connected, label_str, node_color, bounding_rect):
+        
+        c = None
+        if not connected:
+            c = QColor('#53585c')
+        else:
+            if type_ == 'exec':
+                c = QColor('#cccccc')
+            else:
+                c = node_color
+        
+        self.paint_PI_label_default(painter, label_str, c, QFont("Courier New", 10, QFont.Bold), bounding_rect)
+
+
+    def paint_PI(self, node, painter, option, node_color, type_, connected, padding, w, h):
+        
+        color = None
+        if not connected:
+            color = QColor('#53585c')
+        else:
+            if type_ == 'exec':
+                color = QColor('#dddddd')
+            else:
+                color = node_color
+
+        if type_ == 'exec':
+            if connected or \
+                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
+                brush = QBrush(QColor(255, 255, 255, 100))
+                painter.setBrush(brush)
+            else:
+                painter.setBrush(Qt.NoBrush)
+        elif type_ == 'data':
+            if connected or \
+                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
+                r = node_color.red()
+                g = node_color.green()
+                b = node_color.blue()
+                brush = QBrush(QColor(r, g, b, 100))
+                painter.setBrush(brush)
+            else:
+                painter.setBrush(Qt.NoBrush)
+
+        brush = QBrush(QColor(color))
+        painter.setBrush(brush)
+        painter.setPen(Qt.NoPen)
+
+        painter.drawEllipse(QRectF(padding+w/8, padding+h/8, 3*w/4, 3*h/4))
+
+    
+    def draw_NI_extended(self, node, painter, c, w, h, bounding_rect: QRectF, title_rect):
+
+        background_color = self.nodes_background_color
+        header_color = c
+
+        header_height = self.get_header_rect(w, h, title_rect).height()
+        rel_header_height = header_height/h
+        gradient = QLinearGradient(bounding_rect.topLeft(), bounding_rect.bottomLeft())
+        gradient.setColorAt(0, header_color)
+        gradient.setColorAt(rel_header_height, header_color)
+        gradient.setColorAt(rel_header_height+0.0001, background_color)
+        gradient.setColorAt(1, background_color)
+
+        painter.setBrush(QBrush(background_color))
+        painter.setPen(Qt.NoPen)  # QPen(c.darker()))
+        painter.drawRoundedRect(QRectF(
+            QPointF(bounding_rect.left(), bounding_rect.top()+header_height),
+            bounding_rect.bottomRight()
+        ), 6, 6)
+
+
+    def draw_NI_small(self, node, painter, c, w, h, bounding_rect, background_color=None):
+        background_color = self.small_nodes_background_color
+        c_s = 10  # corner size
+        painter.setBrush(self.interpolate_color(c, background_color, 0.97))
+        painter.setPen(Qt.NoPen)
+        painter.drawRoundedRect(bounding_rect, c_s, c_s)
+
+
+class FlowTheme_Samuel1(FlowTheme):
+
+
+    name = 'Samuel 1d'
+
+    exec_conn_color = QColor('#ffffff')
+    exec_conn_width = 1.5
+    exec_conn_pen_style = Qt.SolidLine
+
+    data_conn_color = QColor('#ffffff')
+    data_conn_width = 1.5
+    data_conn_pen_style = Qt.DashLine
+
+    flow_background_brush = QBrush(QColor('#1E242A'))
+    flow_background_grid = ('points', flow_background_brush.color().lighter(), 2, 50, 50)
+
+
+    node_ext_background_color = QColor('#0C1116')
+    node_small_background_color = QColor('#363c41')
+    node_title_color = QColor('#ffffff')
+    port_pin_pen_color = QColor('#ffffff')
+
+    EXPORT = [
+        'extended node background color',
+        'small node background color',
+        'node title color',
+        'port pin pen color'
+    ]
+
+
+    def _load(self, imported: dict):
+        for k, v in imported.items():
+            if k == 'extended node background color':
+                self.node_ext_background_color = self.hex_to_col(v)
+            elif k == 'small node background color':
+                self.node_small_background_color = self.hex_to_col(v)
+            elif k == 'node title color':
+                self.node_title_color = self.hex_to_col(v)
+            elif k == 'port pin pen color':
+                self.port_pin_pen_color = self.hex_to_col(v)
+
+
+    def paint_NI_title_label(self, node, painter, option, hovering, node_style, node_title, node_color, node_item_bounding_rect):
+
+        painter.setPen(QPen(self.node_title_color))
+
+        if node_style == 'extended':
+            painter.setFont(QFont('Segoe UI', 11))
+            align = Qt.AlignLeft | Qt.AlignVCenter
+        else:
+            painter.setFont(QFont('Segoe UI', 15))
+            align = Qt.AlignCenter
+
+        painter.drawText(node_item_bounding_rect, align, node_title)
+
+
+    def paint_PI_label(self, node, painter, option, type_, connected, label_str, node_color, bounding_rect):
+        c = None
+        if not connected:
+            c = QColor('#53585c')
+        else:
+            if type_ == 'exec':
+                c = QColor('#cccccc')
+            else:
+                c = node_color
+        
+        self.paint_PI_label_default(painter, label_str, c, QFont("Segoe UI", 10), bounding_rect)
+
+
+    def paint_PI(self, node, painter, option, node_color, type_, connected, padding, w, h):
+
+        if connected:
+            painter.setBrush(QColor('#508AD8'))
+            painter.setPen(Qt.NoPen)
+        else:
+            painter.setBrush(Qt.NoBrush)
+            p = QPen(self.port_pin_pen_color)
+            p.setWidthF(1.1)
+            painter.setPen(p)
+
+        painter.drawEllipse(QRectF(padding + w / 8, padding + h / 8, 3 * w / 4, 3 * h / 4))
+
+
+    def draw_NI_extended(self, node, painter, c, w, h, bounding_rect: QRectF, title_rect):
+
+        background_color = self.node_ext_background_color
+
+        header_height = self.get_header_rect(w, h, title_rect).height()
+
+        painter.setBrush(QBrush(background_color))
+        painter.setPen(Qt.NoPen)  # QPen(c.darker()))
+        painter.drawRoundedRect(QRectF(
+            QPointF(bounding_rect.left(), bounding_rect.top() + header_height),
+            bounding_rect.bottomRight()
+        ), 3, 3)
+
+        p = QPen(c)
+        p.setWidthF(2.3)
+        painter.setPen(p)
+        painter.drawLine(
+            QPointF(bounding_rect.left(), bounding_rect.top() + header_height),
+            QPointF(bounding_rect.right(), bounding_rect.top() + header_height)
+        )
+
+
+    def draw_NI_small(self, node, painter, c, w, h, bounding_rect, background_color=None):
+        
+        painter.setBrush(QBrush(self.node_small_background_color))
+        painter.setPen(Qt.NoPen)
+        painter.drawRoundedRect(bounding_rect, 4, 4)
+
+
+class FlowTheme_Samuel1_Light(FlowTheme_Samuel1):
+
+
+    name = 'Samuel 1l'
+    type_ = 'light'
+
+    exec_conn_color = QColor('#1f1f1f')
+
+    data_conn_color = QColor('#1f1f1f')
+
+    flow_background_brush = QBrush(QColor('#ffffff'))
+    flow_background_grid = ('points', QColor('#dddddd'), 2, 20, 20)
+
+
+    node_ext_background_color = QColor('#cdcfd1')
+    node_small_background_color = QColor('#bebfc1')
+    node_title_color = QColor('#1f1f1f')
+    port_pin_pen_color = QColor('#1f1f1f')
+
+
+    node_item_shadow_color = QColor('#bbbbbb')
+
+
+class FlowTheme_Samuel2(FlowTheme):
+
+
+    name = 'Samuel 2d'
+
+    exec_conn_color = QColor('#ffffff')
+    exec_conn_width = 1.5
+    exec_conn_pen_style = Qt.SolidLine
+
+    data_conn_color = QColor('#ffffff')
+    data_conn_width = 1.5
+    data_conn_pen_style = Qt.DashLine
+
+    flow_background_brush = QBrush(QColor('#1E242A'))
+    flow_background_grid = ('points', flow_background_brush.color().lighter(), 2, 50, 50)
+
+
+    node_ext_background_color = QColor('#0C1116')
+    node_small_background_color = QColor('#363c41')
+    node_title_color = QColor('#ffffff')
+    port_pin_pen_color = QColor('#ffffff')
+
+
+    EXPORT = [
+        'node title color',
+        'port pin pen color'
+    ]
+
+
+    def _load(self, imported: dict):
+        for k, v in imported.items():
+            if k == 'node title color':
+                self.node_title_color = self.hex_to_col(v)
+            elif k == 'port pin pen color':
+                self.port_pin_pen_color = self.hex_to_col(v)
+
+
+    def paint_NI_title_label(self, node, painter, option, hovering, node_style, node_title, node_color,
+                             node_item_bounding_rect):
+
+        painter.setPen(QPen(self.node_title_color))
+
+        if node_style == 'extended':
+            painter.setFont(QFont('Segoe UI', 11))
+            align = Qt.AlignLeft | Qt.AlignVCenter
+        else:
+            painter.setFont(QFont('Segoe UI', 15))
+            align = Qt.AlignCenter
+
+        painter.drawText(node_item_bounding_rect, align, node_title)
+
+
+    def paint_PI_label(self, node, painter, option, type_, connected, label_str, node_color, bounding_rect):
+        c = None
+        if not connected:
+            c = QColor('#dddddd')
+        else:
+            if type_ == 'exec':
+                c = QColor('#cccccc')
+            else:
+                c = node_color
+
+        self.paint_PI_label_default(painter, label_str, c, QFont("Segoe UI", 10), bounding_rect)
+
+
+    def paint_PI(self, node, painter, option, node_color, type_, connected, padding, w, h):
+
+        if connected:
+            painter.setBrush(QColor('#508AD8'))
+            painter.setPen(Qt.NoPen)
+        else:
+            painter.setBrush(Qt.NoBrush)
+            p = QPen(self.port_pin_pen_color)
+            p.setWidthF(1.1)
+            painter.setPen(p)
+
+        painter.drawEllipse(QRectF(padding + w / 8, padding + h / 8, 3 * w / 4, 3 * h / 4))
+
+
+    def draw_NI_extended(self, node, painter, c, w, h, bounding_rect: QRectF, title_rect):
+
+        background_color = QColor(c.red(), c.green(), c.blue(), 90)
+        header_color = QColor(c.red(), c.green(), c.blue(), 130).darker()
+
+        rel_header_height = self.get_header_rect(w, h, title_rect).height() / h
+        gradient = QLinearGradient(bounding_rect.topLeft(), bounding_rect.bottomLeft())
+        gradient.setColorAt(0, header_color)
+        gradient.setColorAt(rel_header_height, header_color)
+        gradient.setColorAt(rel_header_height+0.0001, background_color)
+        gradient.setColorAt(1, background_color)
+
+        painter.setBrush(gradient)
+        painter.setPen(Qt.NoPen)
+        painter.drawRoundedRect(bounding_rect, 7, 7)
+
+    def draw_NI_small(self, node, painter, c, w, h, bounding_rect, background_color=None):
+
+        painter.setBrush(QBrush(QColor(c.red(), c.green(), c.blue(), 150)))
+        painter.setPen(Qt.NoPen)
+        painter.drawRoundedRect(bounding_rect, 8, 8)
+
+
+class FlowTheme_Samuel2_Light(FlowTheme_Samuel2):
+
+
+    name = 'Samuel 2l'
+    type_ = 'light'
+
+    exec_conn_color = QColor('#1f1f1f')
+
+    data_conn_color = QColor('#1f1f1f')
+
+    flow_background_brush = QBrush(QColor('#ffffff'))
+    flow_background_grid = ('points', QColor('#dddddd'), 2, 20, 20)
+
+    node_title_color = QColor('#1f1f1f')
+    port_pin_pen_color = QColor('#1f1f1f')
+
+
+    def paint_PI_label(self, node, painter, option, type_, connected, label_str, node_color, bounding_rect):
+        c = None
+        if not connected:
+            c = QColor('#1f1f1f')
+        else:
+            if type_ == 'exec':
+                c = QColor('#cccccc')
+            else:
+                c = node_color
+
+        self.paint_PI_label_default(painter, label_str, c, QFont("Segoe UI", 10), bounding_rect)
+
+
+    def draw_NI_extended(self, node, painter, c, w, h, bounding_rect: QRectF, title_rect):
+
+        background_color = QColor(c.red(), c.green(), c.blue(), 90)
+        header_color = QColor(c.red(), c.green(), c.blue(), 130).darker()
+
+        rel_header_height = self.get_header_rect(w, h, title_rect).height() / h
+        gradient = QLinearGradient(bounding_rect.topLeft(), bounding_rect.bottomLeft())
+        gradient.setColorAt(0, header_color)
+        gradient.setColorAt(rel_header_height, header_color)
+        gradient.setColorAt(rel_header_height+0.0001, background_color)
+        gradient.setColorAt(1, background_color)
+
+        painter.setBrush(gradient)
+        painter.setPen(Qt.NoPen)
+        painter.drawRoundedRect(bounding_rect, 7, 7)
+
+
+    def draw_NI_small(self, node, painter, c, w, h, bounding_rect, background_color=None):
+
+        painter.setBrush(QBrush(QColor(c.red(), c.green(), c.blue(), 150)))
+        painter.setPen(Qt.NoPen)
+        painter.drawRoundedRect(bounding_rect, 8, 8)
diff --git a/src/FlowView.py b/src/FlowView.py
new file mode 100644
index 0000000..111ef1c
--- /dev/null
+++ b/src/FlowView.py
@@ -0,0 +1,1219 @@
+import time
+import json
+
+from PySide2.QtCore import Qt, QPointF, QPoint, QRectF, QSizeF, Signal, QTimer, QObject
+from PySide2.QtGui import QPainter, QPen, QColor, QKeySequence, QTabletEvent, QImage, QGuiApplication, QFont
+from PySide2.QtWidgets import QGraphicsView, QGraphicsScene, QShortcut, QMenu, QGraphicsItem, QUndoStack
+
+from .Flow import Flow
+from .FlowCommands import MoveComponents_Command, PlaceNode_Command, \
+    PlaceDrawing_Command, RemoveComponents_Command, ConnectPorts_Command, Paste_Command, FlowUndoCommand
+from .FlowViewProxyWidget import FlowViewProxyWidget
+from .FlowViewStylusModesWidget import FlowViewStylusModesWidget
+from .FlowSessionThreadInterface import FlowSessionThreadInterface
+from .FlowViewZoomWidget import FlowViewZoomWidget
+from .Node import Node
+from .NodeObjPort import NodeObjPort, NodeObjOutput, NodeObjInput
+from .node_choice_widget.PlaceNodeWidget import PlaceNodeWidget
+from .NodeItem import NodeItem
+from .PortItem import PortItem, PortItemPin
+from .Connection import Connection, DataConnection
+from .ConnectionItem import default_cubic_connection_path, ConnectionItem, DataConnectionItem, ExecConnectionItem
+from .DrawingObject import DrawingObject
+from .global_tools.Debugger import Debugger
+from .RC import PortObjPos, FlowAlg
+from .RC import FlowVPUpdateMode as VPUpdateMode
+
+
+class FlowView(QGraphicsView):
+    """Manages the GUI of flows"""
+
+    nodes_selection_changed = Signal(list)
+    node_placed = Signal(Node)
+
+    create_node_request = Signal(object, dict)
+    remove_node_request = Signal(Node)
+
+    check_connection_validity_request = Signal(NodeObjPort, NodeObjPort)
+    connect_request = Signal(NodeObjPort, NodeObjPort)
+
+    get_nodes_config_request = Signal(list)
+    get_connections_config_request = Signal(list)
+    get_flow_config_data_request = Signal()
+
+    viewport_update_mode_changed = Signal(str)
+
+
+    def __init__(self, session, script, flow, config=None, flow_size: list = None, parent=None):
+        super(FlowView, self).__init__(parent=parent)
+
+
+        # 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)
+
+        # SHORTCUTS
+        self._init_shortcuts()
+
+        # GENERAL ATTRIBUTES
+        self.session = session
+        self.script = script
+        self.flow: Flow = flow
+        self.node_items: dict = {}  # {Node: NodeItem}
+        self.node_items__cache: dict = {}
+        self.connection_items: dict = {}  # {Connection: ConnectionItem}
+        self.connection_items__cache: dict = {}
+
+        # PRIVATE FIELDS
+        self._temp_config_data = None
+        self._selected_pin: PortItemPin = None
+        self._dragging_connection = False
+        self._temp_connection_ports = None
+        self._ignore_mouse_event = False  # for stylus - see tablet event
+        self._showing_framerate = False
+        self._last_mouse_move_pos: QPointF = None
+        self._node_place_pos = QPointF()
+        self._left_mouse_pressed_in_flow = False
+        self._right_mouse_pressed_in_flow = False
+        self._mouse_press_pos: QPointF = None
+        self._auto_connection_pin = 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
+
+        # CONNECTIONS TO FLOW
+        self.create_node_request.connect(self.flow.create_node)
+        self.remove_node_request.connect(self.flow.remove_node)
+        self.node_placed.connect(self.flow.node_placed)
+        self.check_connection_validity_request.connect(self.flow.check_connection_validity)
+        self.get_nodes_config_request.connect(self.flow.generate_nodes_config)
+        self.get_connections_config_request.connect(self.flow.generate_connections_config)
+        self.get_flow_config_data_request.connect(self.flow.generate_config_data)
+
+        # CONNECTIONS FROM FLOW
+        self.flow.node_added.connect(self.add_node)
+        self.flow.node_removed.connect(self.remove_node)
+        self.flow.connection_added.connect(self.add_connection)
+        self.flow.connection_removed.connect(self.remove_connection)
+        self.flow.connection_request_valid.connect(self.connection_request_valid)
+        # self.flow.nodes_config_generated.connect(self._abstract_nodes_config_generated)
+        # self.flow.connections_config_generated.connect(self._abstract_connections_config_generated)
+
+        # SESSION THREAD
+        if self.session.threaded:
+            self.thread_interface = FlowSessionThreadInterface()
+            self.thread_interface.moveToThread(self.session.thread())
+
+        # SETTINGS
+        self.vp_update_mode: VPUpdateMode = VPUpdateMode.SYNC
+
+        # CREATE UI
+        scene = QGraphicsScene(self)
+        scene.setItemIndexMethod(QGraphicsScene.NoIndex)
+        if flow_size is None:
+            scene.setSceneRect(0, 0, 10 * self.width(), 10 * self.height())
+        else:
+            scene.setSceneRect(0, 0, flow_size[0], flow_size[1])
+
+        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._scene_selection_changed)
+        self.setAcceptDrops(True)
+
+        self.centerOn(QPointF(self.viewport().width() / 2, self.viewport().height() / 2))
+
+        # PLACE NODE WIDGET
+        self._place_node_widget_proxy = FlowViewProxyWidget(self)
+        self._place_node_widget_proxy.setZValue(1000)
+        self._place_node_widget = PlaceNodeWidget(self, self.session.nodes)
+        self._place_node_widget_proxy.setWidget(self._place_node_widget)
+        self.scene().addItem(self._place_node_widget_proxy)
+        self.hide_place_node_widget()
+
+        # ZOOM WIDGET
+        self._zoom_proxy = FlowViewProxyWidget(self)
+        self._zoom_proxy.setFlag(QGraphicsItem.ItemIgnoresTransformations, True)
+        self._zoom_proxy.setZValue(1001)
+        self._zoom_widget = FlowViewZoomWidget(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 = FlowViewProxyWidget(self)
+        self._stylus_modes_proxy.setFlag(QGraphicsItem.ItemIgnoresTransformations, True)
+        self._stylus_modes_proxy.setZValue(1001)
+        self._stylus_modes_widget = FlowViewStylusModesWidget(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)
+
+        # # TOUCH GESTURES
+        # recognizer = PanGestureRecognizer()
+        # pan_gesture_id = QGestureRecognizer.registerRecognizer(recognizer) <--- CRASH HERE
+        # self.grabGesture(pan_gesture_id)
+
+        # DESIGN
+        self.session.design.flow_theme_changed.connect(self._theme_changed)
+
+        # FRAMERATE TRACKING
+        self.num_frames = 0
+        self.framerate = 0
+        self.framerate_timer = QTimer(self)
+        self.framerate_timer.timeout.connect(self._on_framerate_timer_timeout)
+
+        # self.show_framerate(m_sec_interval=100)  # for testing
+
+        # CONFIG
+        if config is not None:
+            # viewport update mode
+            vpum = config['viewport update mode']
+            if vpum == 'sync':  # vpum == VPUpdateMode.SYNC
+                # self.vp_update_mode = VPUpdateMode.SYNC
+                self.set_viewport_update_mode('sync')
+            elif vpum == 'async':  # vpum == VPUpdateMode.ASYNC
+                # self.vp_update_mode = VPUpdateMode.ASYNC
+                self.set_viewport_update_mode('async')
+
+            if 'drawings' in config:  # not all (old) project files have drawings arr
+                self.place_drawings_from_config(config['drawings'])
+
+            if 'view size' in config:
+                self.setSceneRect(0, 0, config['view size'][0], config['view size'][1])
+
+            self._undo_stack.clear()
+
+
+    def show_framerate(self, show: bool = True, m_sec_interval: int = 1000):
+        self._showing_framerate = show
+        self.framerate_timer.setInterval(m_sec_interval)
+        self.framerate_timer.start()
+
+    def _on_framerate_timer_timeout(self):
+        self.framerate = self.num_frames
+        self.num_frames = 0
+
+    def _init_shortcuts(self):
+        place_new_node_shortcut = QShortcut(QKeySequence('Shift+P'), self)
+        place_new_node_shortcut.activated.connect(self._place_new_node_by_shortcut)
+        move_selected_components_left_shortcut = QShortcut(QKeySequence('Shift+Left'), self)
+        move_selected_components_left_shortcut.activated.connect(self._move_selected_comps_left)
+        move_selected_components_up_shortcut = QShortcut(QKeySequence('Shift+Up'), self)
+        move_selected_components_up_shortcut.activated.connect(self._move_selected_comps_up)
+        move_selected_components_right_shortcut = QShortcut(QKeySequence('Shift+Right'), self)
+        move_selected_components_right_shortcut.activated.connect(self._move_selected_comps_right)
+        move_selected_components_down_shortcut = QShortcut(QKeySequence('Shift+Down'), self)
+        move_selected_components_down_shortcut.activated.connect(self._move_selected_comps_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_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)
+
+    def _theme_changed(self, t):
+        # TODO: repaint background. how?
+        self.viewport().update()
+
+    def _scene_selection_changed(self):
+        self.nodes_selection_changed.emit(self.selected_nodes())
+
+    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 isinstance(i, NodeItem):
+                ni: NodeItem = i
+                menu: QMenu = ni.get_context_menu()
+                menu.exec_(event.globalPos())
+                event.accept()
+
+    def _push_undo(self, cmd: FlowUndoCommand):
+        self._undo_stack.push(cmd)
+        cmd.activate()
+
+    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.write('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._place_node_widget_proxy.isVisible():
+                self.hide_place_node_widget()
+            else:
+                if isinstance(self.itemAt(event.pos()), PortItemPin):
+                    self._selected_pin = self.itemAt(event.pos())
+                    self._dragging_connection = True
+
+            self._left_mouse_pressed_in_flow = True
+
+        elif event.button() == Qt.RightButton:
+            self._right_mouse_pressed_in_flow = True
+            event.accept()
+
+        # elif event.button() == Qt.MidButton:
+        # elif event.button() == Qt.RightButton:
+        #     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)
+
+        # print('qwer')
+        # print(event.button())
+
+        if self._right_mouse_pressed_in_flow:    # PAN
+
+            if not self._panning:
+                self._panning = True
+                self._pan_last_x = event.x()
+                self._pan_last_y = event.y()
+
+            self.pan(event.pos())
+            event.accept()
+
+        # 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)
+
+        node_item_at_event_pos = None
+        for item in self.items(event.pos()):
+            if isinstance(item, NodeItem):
+                node_item_at_event_pos = item
+
+        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 self._panning:
+        # elif event.button() == Qt.MidButton:
+            self._panning = False
+
+        elif event.button() == Qt.RightButton:
+            self._right_mouse_pressed_in_flow = False
+            if self._mouse_press_pos == self._last_mouse_move_pos and \
+                    len(self.items(event.pos())) == 0:
+
+                self._place_node_widget.reset_list()
+                self.show_place_node_widget(event.pos())
+                return
+
+
+        if self._dragging_connection:
+
+            # connection dropped over specific pin
+            if self.itemAt(event.pos()) and isinstance(self.itemAt(event.pos()), PortItemPin):
+
+                # the validity of the connection is checked in connect_node_ports__cmd
+                self.connect_node_ports__cmd(
+                    self._selected_pin.port,
+                    self.itemAt(event.pos()).port
+                )
+
+            # connection dropped above NodeItem -> auto connect
+            elif node_item_at_event_pos:
+                # find node item
+                ni_under_drop = None
+                for item in self.items(event.pos()):
+                    if isinstance(item, NodeItem):
+                        ni_under_drop = item
+                        self.auto_connect(self._selected_pin.port, ni_under_drop.node)
+                        break
+
+            # connection dropped somewhere else -> show node choice widget
+            else:
+                self._auto_connection_pin = self._selected_pin
+                self.show_place_node_widget(event.pos())
+
+            self._dragging_connection = False
+            self._selected_pin = None
+
+        if event.button() == Qt.LeftButton:
+            self._left_mouse_pressed_in_flow = False
+        elif event.button() == Qt.RightButton:
+            self._right_mouse_pressed_in_flow = False
+
+        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__cmd()
+
+    def wheelEvent(self, event):
+
+        # ZOOM
+        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
+
+        scaled_event_pos: QPointF = event.posF()/self._current_scale
+
+        if event.type() == QTabletEvent.TabletPress:
+            self._ignore_mouse_event = True
+
+            if event.button() == Qt.LeftButton:
+                if self.stylus_mode == 'comment':
+                    view_pos = self.mapToScene(self.viewport().pos())
+                    new_drawing = self._create_and_place_drawing__cmd(
+                        view_pos + scaled_event_pos,
+                        config={**self._stylus_modes_widget.get_pen_settings(), 'viewport pos': view_pos}
+                    )
+                    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 isinstance(i, DrawingObject):
+                            self.remove_drawing(i)
+                            break
+            elif self.stylus_mode == 'comment' and self._drawing:
+                if self._current_drawing.append_point(scaled_event_pos):
+                    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.write('drawing obj finished')
+                self._current_drawing.finish()
+                self._current_drawing = None
+                self._drawing = False
+
+    # https://forum.qt.io/topic/121473/qgesturerecognizer-registerrecognizer-crashes-using-pyside2
+    #
+    # def event(self, event) -> bool:
+    #     # if event.type() == QEvent.Gesture:
+    #     #     if event.gesture(PanGesture) is not None:
+    #     #         return self.pan_gesture(event)
+    #
+    #     return QGraphicsView.event(self, event)
+    #
+    # def pan_gesture(self, event: QGestureEvent) -> bool:
+    #     pan: PanGesture = event.gesture(PanGesture)
+    #     print(pan)
+    #     return True
+
+    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.write('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.session.nodes if find_type_in_object(n, GetVar_Node) or
+    #                                       find_type_in_object(n, SetVar_Node)])
+
+    # PAINTING
+    def drawBackground(self, painter, rect):
+
+        painter.setBrush(self.session.design.flow_theme.flow_background_brush)
+        painter.drawRect(rect.intersected(self.sceneRect()))
+        painter.setPen(Qt.NoPen)
+        painter.drawRect(self.sceneRect())
+
+        if self.session.design.performance_mode == 'pretty':
+            theme = self.session.design.flow_theme
+            if theme.flow_background_grid:
+                if theme.flow_background_grid[0] == 'points':
+                    color = theme.flow_background_grid[1]
+                    pen_width = theme.flow_background_grid[2]
+                    diff_x = theme.flow_background_grid[3]
+                    diff_y = theme.flow_background_grid[4]
+
+                    pen = QPen(color)
+                    pen.setWidthF(pen_width)
+                    painter.setPen(pen)
+
+                    for x in range(diff_x, self.sceneRect().toRect().width(), diff_x):
+                        for y in range(diff_y, self.sceneRect().toRect().height(), diff_y):
+                            painter.drawPoint(x, y)
+
+
+        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):
+
+        if self._showing_framerate:
+            self.num_frames += 1
+            pen = QPen(QColor('#A9D5EF'))
+            pen.setWidthF(2)
+            painter.setPen(pen)
+
+            pos = self.mapToScene(10, 23)
+            painter.setFont(QFont('Poppins', round(11 * self._total_scale_div)))
+            painter.drawText(pos, "{:.2f}".format(self.framerate))
+
+
+        # DRAW CURRENTLY DRAGGED CONNECTION
+        if self._dragging_connection:
+            pen = QPen('#101520')
+            pen.setWidth(3)
+            pen.setStyle(Qt.DotLine)
+            painter.setPen(pen)
+
+            pin_pos = self._selected_pin.get_scene_center_pos()
+            spp = self._selected_pin.port
+            cursor_pos = self._last_mouse_move_pos
+
+            pos1 = pin_pos if spp.io_pos == PortObjPos.OUTPUT else cursor_pos
+            pos2 = pin_pos if spp.io_pos == PortObjPos.INPUT else cursor_pos
+
+            painter.drawPath(
+                default_cubic_connection_path(pos1, pos2)
+            )
+
+
+        # DRAW SELECTED NIs BORDER
+        for ni in self.selected_node_items():
+            pen = QPen(self.session.design.flow_theme.flow_highlight_pen_color)
+            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) -> QImage:
+        """Returns a clear image of the viewport"""
+
+        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) -> QImage:
+        """Returns an image of the whole scene, scaled accordingly to current scale factor.
+        Due to a bug this only works from the viewport position down and right, so the user has to scroll to
+        the top left corner in order to get the full scene"""
+
+        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()
+
+    # PLACE NODE WIDGET
+    def show_place_node_widget(self, pos, nodes=None):
+        """Opens the place node 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._place_node_widget.width() / self._total_scale_div > self.viewport().width():
+            dialog_pos.setX(dialog_pos.x() - (
+                    dialog_pos.x() + self._place_node_widget.width() / self._total_scale_div - self.viewport().width()))
+        if dialog_pos.y() + self._place_node_widget.height() / self._total_scale_div > self.viewport().height():
+            dialog_pos.setY(dialog_pos.y() - (
+                    dialog_pos.y() + self._place_node_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._place_node_widget.update_list(nodes if nodes is not None else self.session.nodes)
+        self._place_node_widget.update_view()
+        self._place_node_widget_proxy.setPos(dialog_pos)
+        self._place_node_widget_proxy.show()
+        self._place_node_widget.refocus()
+
+    def hide_place_node_widget(self):
+        self._place_node_widget_proxy.hide()
+        self._place_node_widget.clearFocus()
+        self._auto_connection_pin = None
+
+    def _place_new_node_by_shortcut(self):  # Shift+P
+        point_in_viewport = None
+        selected_NIs = self.selected_node_items()
+        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._place_node_widget.reset_list()
+        self.show_place_node_widget(point_in_viewport)
+
+    # 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)
+
+
+
+
+    # NODES
+    def create_node__cmd(self, node_class):
+        self._push_undo(
+            PlaceNode_Command(self, node_class, self._node_place_pos)
+        )
+
+    def add_node(self, node):
+
+        # create item
+        item: NodeItem = None
+
+        if node in self.node_items__cache.keys():  # load from cache
+            # print('using a cached item')
+            item = self.node_items__cache[node]
+            self._add_node_item(item)
+
+        else:  # create new item
+            item_config = node.init_config
+            item = NodeItem(node, params=(self, self.session.design, item_config))
+            node.item = item
+            item.initialize()
+            self.node_placed.emit(node)
+
+            pos = None
+            if item_config is not None:
+                if 'pos x' in item_config:
+                    pos = QPointF(item_config['pos x'], item_config['pos y'])
+                elif 'position x' in item_config:  # backwards compatibility
+                    pos = QPointF(item_config['position x'], item_config['position y'])
+            else:
+                pos = self._node_place_pos
+
+            self._add_node_item(item, pos)
+
+        # auto connect
+        if self._auto_connection_pin:
+            self.auto_connect(self._auto_connection_pin.port,
+                              node)
+
+    def _add_node_item(self, item: NodeItem, pos=None):
+        self.node_items[item.node] = item
+
+        self.scene().addItem(item)
+        if pos:
+            item.setPos(pos)
+
+        # select new item
+        self.clear_selection()
+        item.setSelected(True)
+
+    def remove_node(self, node):
+        item = self.node_items[node]
+        self._remove_node_item(item)
+        del self.node_items[node]
+
+    def _remove_node_item(self, item: NodeItem):
+        # store item in case the remove action gets undone later
+        self.node_items__cache[item.node] = item
+        self.scene().removeItem(item)
+
+    # CONNECTIONS
+    def connect_node_ports__cmd(self, p1: NodeObjPort, p2: NodeObjPort):
+        self._temp_connection_ports = (p1, p2)
+        self.check_connection_validity_request.emit(p1, p2)
+
+    def connection_request_valid(self, valid: bool):
+        """
+        Triggered after the abstract flow evaluated validity of pending connect request.
+        This can also lead to a disconnect!
+        """
+
+        if valid:
+            out, inp = self._temp_connection_ports
+            if out.io_pos == PortObjPos.INPUT:
+                out, inp = inp, out
+
+            # remove forbidden connections
+            if inp.type_ == 'data':
+                for c in inp.connections:
+
+                    if c.out == out:
+                        # if the exact connection exists, we want to remove it by command
+                        continue
+
+                    self._push_undo(
+                        ConnectPorts_Command(self, out=c.out, inp=inp)
+                    )
+
+            self._push_undo(
+                ConnectPorts_Command(self, out=out, inp=inp)
+            )
+
+    def add_connection(self, c: Connection):
+
+        item: ConnectionItem = None
+        if c in self.connection_items__cache.keys():
+            item = self.connection_items__cache[c]
+
+        else:
+            if isinstance(c, DataConnection):
+                item = DataConnectionItem(c, self.session.design)
+            else:
+                item = ExecConnectionItem(c, self.session.design)
+
+
+
+        self._add_connection_item(item)
+
+    def _add_connection_item(self, item: ConnectionItem):
+        self.connection_items[item.connection] = item
+        self.scene().addItem(item)
+        item.setZValue(10)
+        # self.viewport().repaint()
+
+    def remove_connection(self, c: Connection):
+        item = self.connection_items[c]
+        self._remove_connection_item(item)
+        del self.connection_items[c]
+
+    def _remove_connection_item(self, item: ConnectionItem):
+        self.connection_items__cache[item.connection] = item
+        self.scene().removeItem(item)
+
+    def auto_connect(self, p: NodeObjPort, n: Node):
+        if p.io_pos == PortObjPos.OUTPUT:
+            for inp in n.inputs:
+                if p.type_ == inp.type_:
+                    # connect exactly once
+                    self.connect_node_ports__cmd(p, inp)
+                    return
+        elif p.io_pos == PortObjPos.INPUT:
+            for out in n.outputs:
+                if p.type_ == out.type_:
+                    # connect exactly once
+                    self.connect_node_ports__cmd(p, out)
+                    return
+
+    # DRAWINGS
+    def create_drawing(self, config=None) -> DrawingObject:
+        """Creates and returns a new DrawingObject."""
+
+        new_drawing = DrawingObject(self, config)
+        return new_drawing
+
+    def add_drawing(self, drawing_obj, posF=None):
+        """Adds a DrawingObject to the scene."""
+
+        self.scene().addItem(drawing_obj)
+        if posF:
+            drawing_obj.setPos(posF)
+        self.drawings.append(drawing_obj)
+
+    def add_drawings(self, drawings):
+        """Adds a list of DrawingObjects to the scene."""
+
+        for d in drawings:
+            self.add_drawing(d)
+
+    def remove_drawing(self, drawing):
+        """Removes a drawing from the scene."""
+
+        self.scene().removeItem(drawing)
+        self.drawings.remove(drawing)
+
+    def place_drawings_from_config(self, drawings_config: list, offset_pos=QPoint(0, 0)):
+        """Creates and places drawings from drawings. The same list is returned by the config_data() method
+        at 'drawings'."""
+
+        new_drawings = []
+        for d_config in drawings_config:
+            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, posF, config=None):
+        new_drawing_obj = self.create_drawing(config)
+        place_command = PlaceDrawing_Command(self, posF, new_drawing_obj)
+        self._push_undo(place_command)
+        return new_drawing_obj
+
+    # ADDING/REMOVING COMPONENTS
+    def add_component(self, e: QGraphicsItem):
+        if isinstance(e, NodeItem):
+            self.add_node(e.node)
+            self.add_node_item(e)
+        elif isinstance(e, DrawingObject):
+            self.add_drawing(e)
+
+    def remove_components(self, comps: [QGraphicsItem]):
+        for c in comps:
+            self.remove_component(c)
+
+    def remove_component(self, e: QGraphicsItem):
+        if isinstance(e, NodeItem):
+            self.remove_node(e.node)
+            self.remove_node_item(e)
+        elif isinstance(e, DrawingObject):
+            self.remove_drawing(e)
+
+    def remove_selected_components__cmd(self):
+        self._push_undo(
+            RemoveComponents_Command(self, self.scene().selectedItems())
+        )
+
+        self.viewport().update()
+
+    # MOVING COMPONENTS
+    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
+            w = i.boundingRect().width()
+            h = i.boundingRect().height()
+            if new_pos.x() - w / 2 < 0 or \
+                    new_pos.x() + w / 2 > self.scene().width() or \
+                    new_pos.y() - h / 2 < 0 or \
+                    new_pos.y() + h / 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._push_undo(
+                MoveComponents_Command(self, self.scene().selectedItems(), p_from=-new_rel_pos, p_to=QPointF(0, 0))
+            )
+
+        self.viewport().repaint()
+
+    def _move_selected_comps_left(self):
+        self._move_selected_copmonents__cmd(-40, 0)
+
+    def _move_selected_comps_up(self):
+        self._move_selected_copmonents__cmd(0, -40)
+
+    def _move_selected_comps_right(self):
+        self._move_selected_copmonents__cmd(+40, 0)
+
+    def _move_selected_comps_down(self):
+        self._move_selected_copmonents__cmd(0, +40)
+
+    # SELECTION
+    def selected_components_moved(self, pos_diff):
+        items_list = self.scene().selectedItems()
+
+        self._push_undo(
+            MoveComponents_Command(self, items_list, p_from=-pos_diff, p_to=QPointF(0, 0))
+        )
+
+    def selected_node_items(self) -> [NodeItem]:
+        """Returns a list of the currently selected NodeItems."""
+
+        selected_NIs = []
+        for i in self.scene().selectedItems():
+            if isinstance(i, NodeItem):
+                selected_NIs.append(i)
+        return selected_NIs
+
+    def selected_nodes(self) -> [Node]:
+        return [item.node for item in self.selected_node_items()]
+
+    def selected_drawings(self) -> [DrawingObject]:
+        """Returns a list of the currently selected drawings."""
+
+        selected_drawings = []
+        for i in self.scene().selectedItems():
+            if isinstance(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 clear_selection(self):
+        self.scene().clearSelection()
+
+    def select_components(self, comps):
+        self.scene().clearSelection()
+        for c in comps:
+            c.setSelected(True)
+
+    # ACTIONS
+    def _copy(self):  # ctrl+c
+        data = {'nodes': self._get_nodes_config_data(self.selected_nodes()),
+                'connections': self._get_connections_config_data(self.selected_nodes()),
+                'drawings': self._get_drawings_config_data(self.selected_drawings())}
+        QGuiApplication.clipboard().setText(json.dumps(data))
+
+    def _cut(self):  # ctrl+x
+        data = {'nodes': self._get_nodes_config_data(self.selected_nodes()),
+                'connections': self._get_connections_config_data(self.selected_nodes()),
+                'drawings': self._get_drawings_config_data(self.selected_drawings())}
+        QGuiApplication.clipboard().setText(json.dumps(data))
+        self.remove_selected_components__cmd()
+
+    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['pos x'],
+                              'y': n['pos 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._push_undo(
+            Paste_Command(self, data, offset_for_middle_pos)
+        )
+
+    # VIEWPORT UPDATE MODE
+    def viewport_update_mode(self) -> str:
+        """Returns the current viewport update mode as string (sync or async) of the flow"""
+        return VPUpdateMode.stringify(self.vp_update_mode)
+
+    def set_viewport_update_mode(self, mode: str):
+        """
+        Sets the viewport update mode of the flow
+        :mode: 'sync' or 'async'
+        """
+        if mode == 'sync':
+            self.vp_update_mode = VPUpdateMode.SYNC
+        elif mode == 'async':
+            self.vp_update_mode = VPUpdateMode.ASYNC
+
+        self.viewport_update_mode_changed.emit(self.viewport_update_mode())
+
+    # CONFIG
+    def generate_config_data(self, abstract_flow_data):
+
+        # # wait for abstract flow
+        # self.flow._temp_config_data = None
+        # self.get_flow_config_data_request.emit()
+        # while self.flow._temp_config_data is None:
+        #     time.sleep(0.001)
+        # flow_config, nodes_cfg, connections_cfg = self.flow._temp_config_data
+
+        flow_config, nodes_cfg, connections_cfg = abstract_flow_data
+
+        final_flow_config = {
+            **flow_config,
+            'nodes': self.complete_nodes_config_data(nodes_cfg),
+            'connections': self.complete_connections_config_data(connections_cfg),
+        }
+        self_config = {
+            'viewport update mode': VPUpdateMode.stringify(self.vp_update_mode),
+            'drawings': self._get_drawings_config_data(self.drawings),
+            'view size': [self.sceneRect().size().width(), self.sceneRect().size().height()]
+        }
+
+        # self.config_generated.emit(data)
+        self._temp_config_data = (final_flow_config, self_config)
+        return final_flow_config, self_config
+
+    def complete_nodes_config_data(self, nodes_config_dict):
+        """
+        Adds the item config (scene pos etc.) to the config of the nodes.
+        """
+
+        nodes_config_list = []
+        for n in list(nodes_config_dict.keys()):
+            cfg = nodes_config_dict[n]
+
+            item = self.node_items[n]
+            nodes_config_list.append(item.complete_config(cfg))
+
+
+        self._temp_config_data = nodes_config_list
+        return nodes_config_list
+
+    def complete_connections_config_data(self, conn_config_dict):
+        """
+        Adds the item config to the config of the connections
+        """
+
+        # TODO: add custom config when implementing custom connection items later
+
+        conns_config_list = []
+        for c in list(conn_config_dict.keys()):
+            conns_config_list.append(conn_config_dict[c])
+
+        self._temp_config_data = conns_config_list
+        return conns_config_list
+
+    def _get_nodes_config_data(self, nodes):
+
+        # wait for abstract flow
+        self.flow._temp_config_data = None
+        self.get_nodes_config_request.emit(nodes)
+        while self.flow._temp_config_data is None:
+            time.sleep(0.001)
+
+        return self.complete_nodes_config_data(self.flow._temp_config_data)
+
+    def _get_connections_config_data(self, nodes):
+
+        # wait for abstract flow
+        self.flow._temp_config_data = None
+        self.get_connections_config_request.emit(nodes)
+        while self.flow._temp_config_data is None:
+            time.sleep(0.001)
+
+        return self.complete_connections_config_data(self.flow._temp_config_data)
+
+    def _get_drawings_config_data(self, drawings):
+        drawings_list = []
+        for drawing in drawings:
+            drawing_dict = drawing.config_data()
+
+            drawings_list.append(drawing_dict)
+
+        return drawings_list
diff --git a/src/FlowProxyWidget.py b/src/FlowViewProxyWidget.py
similarity index 59%
rename from src/FlowProxyWidget.py
rename to src/FlowViewProxyWidget.py
index 6f4e2dd..4edc8c0 100644
--- a/src/FlowProxyWidget.py
+++ b/src/FlowViewProxyWidget.py
@@ -1,20 +1,20 @@
 from PySide2.QtWidgets import QGraphicsProxyWidget
 
 
-class FlowProxyWidget(QGraphicsProxyWidget):
-    def __init__(self, flow, parent=None):
-        super(FlowProxyWidget, self).__init__(parent)
+class FlowViewProxyWidget(QGraphicsProxyWidget):
+    def __init__(self, flow_view, parent=None):
+        super(FlowViewProxyWidget, self).__init__(parent)
 
-        self.flow = flow
+        self.flow_view = flow_view
 
 
     def mousePressEvent(self, arg__1):
         QGraphicsProxyWidget.mousePressEvent(self, arg__1)
         if arg__1.isAccepted():
-            self.flow.ignore_mouse_event = True
+            self.flow_view._ignore_mouse_event = True
 
     def mouseReleaseEvent(self, arg__1):
-        self.flow.ignore_mouse_event = True
+        self.flow_view._ignore_mouse_event = True
         QGraphicsProxyWidget.mouseReleaseEvent(self, arg__1)
 
     def wheelEvent(self, event):
@@ -23,4 +23,4 @@ def wheelEvent(self, event):
     def keyPressEvent(self, arg__1):
         QGraphicsProxyWidget.keyPressEvent(self, arg__1)
         if arg__1.isAccepted():
-            self.flow.ignore_key_event = True
\ No newline at end of file
+            self.flow_view.ignore_key_event = True
\ No newline at end of file
diff --git a/src/FlowViewStylusModesWidget.py b/src/FlowViewStylusModesWidget.py
new file mode 100644
index 0000000..a2d9365
--- /dev/null
+++ b/src/FlowViewStylusModesWidget.py
@@ -0,0 +1,125 @@
+from PySide2.QtWidgets import QWidget, QPushButton, QHBoxLayout, QSlider, QColorDialog
+from PySide2.QtCore import Qt
+from PySide2.QtGui import QColor
+
+
+class FlowViewStylusModesWidget(QWidget):
+    def __init__(self, flow_view):
+        super(FlowViewStylusModesWidget, self).__init__()
+
+        self.setObjectName('FlowViewStylusModesWidget')
+
+        # GENERAL ATTRIBUTES
+        self.flow_view = flow_view
+        self.pen_color = QColor(255, 255, 0)
+        self.stylus_buttons_visible = True
+
+        # stylus button
+        self.stylus_button = QPushButton('stylus')  # show/hide
+        self.stylus_button.clicked.connect(self.on_stylus_button_clicked)
+
+        # mode
+        self.set_stylus_mode_comment_button = QPushButton('comment')
+        self.set_stylus_mode_comment_button.clicked.connect(self.on_comment_button_clicked)
+        self.set_stylus_mode_edit_button = QPushButton('edit')
+        self.set_stylus_mode_edit_button.clicked.connect(self.on_edit_button_clicked)
+
+        # pen style
+        self.pen_color_button = QPushButton('color')
+        self.pen_color_button.clicked.connect(self.on_choose_color_clicked)
+        self.pen_width_slider = QSlider(Qt.Horizontal)
+        self.pen_width_slider.setRange(1, 100)
+        self.pen_width_slider.setValue(20)
+
+
+        # MAIN LAYOUT
+        main_horizontal_layout = QHBoxLayout()
+
+        main_horizontal_layout.addWidget(self.pen_color_button)
+        main_horizontal_layout.addWidget(self.pen_width_slider)
+        main_horizontal_layout.addWidget(self.set_stylus_mode_comment_button)
+        main_horizontal_layout.addWidget(self.set_stylus_mode_edit_button)
+        main_horizontal_layout.addWidget(self.stylus_button)
+
+        self.setLayout(main_horizontal_layout)
+
+        self.setStyleSheet('''
+        QWidget#FlowViewStylusModesWidget {
+            background: transparent; 
+        }
+                ''')
+
+        self.hide_stylus_buttons()
+        self.hide_pen_style_widgets()
+
+
+    def hide_stylus_buttons(self):
+        self.set_stylus_mode_edit_button.hide()
+        self.set_stylus_mode_comment_button.hide()
+        self.stylus_buttons_visible = False
+
+    def show_stylus_buttons(self):
+        self.set_stylus_mode_edit_button.show()
+        self.set_stylus_mode_comment_button.show()
+        self.stylus_buttons_visible = True
+
+    def hide_pen_style_widgets(self):
+        self.pen_color_button.hide()
+        self.pen_width_slider.hide()
+
+    def show_pen_style_widgets(self):
+        self.pen_color_button.show()
+        self.pen_width_slider.show()
+
+    def on_stylus_button_clicked(self):
+        if self.stylus_buttons_visible:
+            self.hide_pen_style_widgets()
+            self.hide_stylus_buttons()
+        else:
+            self.show_stylus_buttons()
+
+        self.adjustSize()
+        self.flow_view.set_stylus_proxy_pos()
+
+    def on_edit_button_clicked(self):
+        self.flow_view.stylus_mode = 'edit'
+        # self.pen_style_widget.hide()
+        self.hide_pen_style_widgets()
+
+        # if I don't hide and show the settings_widget manually here, the stylus mode buttons take up the additional
+        # space when clicking on comment and then edit. self.adjustSize() does not seem to work properly here...
+        self.hide_stylus_buttons()
+        self.show_stylus_buttons()
+        # self.settings_widget.hide()
+        # self.settings_widget.show()
+
+        self.adjustSize()
+        self.flow_view.set_stylus_proxy_pos()
+        # self.flow.setDragMode(QGraphicsView.RubberBandDrag)
+
+    def on_comment_button_clicked(self):
+        self.flow_view.stylus_mode = 'comment'
+        # self.pen_style_widget.show()
+        self.show_pen_style_widgets()
+        self.adjustSize()
+        self.flow_view.set_stylus_proxy_pos()
+        # self.flow.setDragMode(QGraphicsView.NoDrag)
+
+    def on_choose_color_clicked(self):
+        self.pen_color = QColorDialog.getColor(self.pen_color, options=QColorDialog.ShowAlphaChannel,
+                                               title='Choose pen color')
+        self.update_color_button_SS()
+
+
+    def update_color_button_SS(self):
+
+        self.pen_color_button.setStyleSheet(
+            '''
+QPushButton {
+    background-color: '''+self.pen_color.name()+''';
+}'''
+        )
+
+    def get_pen_settings(self):
+        return {'color': self.pen_color.name(),
+                'base stroke weight': self.pen_width_slider.value()/10}
\ No newline at end of file
diff --git a/src/FlowViewZoomWidget.py b/src/FlowViewZoomWidget.py
new file mode 100644
index 0000000..9f8bd2a
--- /dev/null
+++ b/src/FlowViewZoomWidget.py
@@ -0,0 +1,34 @@
+from PySide2.QtWidgets import QWidget, QPushButton, QHBoxLayout
+
+
+class FlowViewZoomWidget(QWidget):
+    def __init__(self, flow_view):
+        super(FlowViewZoomWidget, self).__init__()
+
+        self.flow_view = flow_view
+
+        self.setObjectName('FlowViewZoomWidget')
+
+        self.zoom_in_button = QPushButton('+')
+        self.zoom_in_button.clicked.connect(self.on_zoom_in_button_clicked)
+        self.zoom_out_button = QPushButton('-')
+        self.zoom_out_button.clicked.connect(self.on_zoom_out_button_clicked)
+
+        main_horizontal_layout = QHBoxLayout()
+
+        main_horizontal_layout.addWidget(self.zoom_out_button)
+        main_horizontal_layout.addWidget(self.zoom_in_button)
+        self.setLayout(main_horizontal_layout)
+
+        self.setStyleSheet('''
+QWidget#FlowViewZoomWidget {
+    background: transparent; 
+}
+        ''')
+
+
+    def on_zoom_in_button_clicked(self):
+        self.flow_view.zoom_in(250)
+
+    def on_zoom_out_button_clicked(self):
+        self.flow_view.zoom_out(250)
\ No newline at end of file
diff --git a/src/FlowWorkerThread.py b/src/FlowWorkerThread.py
deleted file mode 100644
index 8e8dae2..0000000
--- a/src/FlowWorkerThread.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from PySide2.QtCore import QThread, QObject
-
-
-class FlowWorkerThread(QThread):
-    def __init__(self, parent=None):
-        super().__init__(parent=parent)
-
-        self.interface = FlowWorkerThreadInterface()
-        self.interface.moveToThread(self)
-
-
-class FlowWorkerThreadInterface(QObject):
-
-    def trigger_port_connected(self, port):
-        port.connected()
-
-    def trigger_port_disconnected(self, port):
-        port.disconnected()
-
-    def trigger_node_update(self, node, input_called=-1):
-        node.update(input_called)
-
-    def trigger_node_action(self, method, data=None):
-        if data:
-            method(data)
-        else:
-            method()
diff --git a/src/FlowZoomWidget.py b/src/FlowZoomWidget.py
deleted file mode 100644
index c68a180..0000000
--- a/src/FlowZoomWidget.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from PySide2.QtWidgets import QWidget, QPushButton, QHBoxLayout
-
-
-class FlowZoomWidget(QWidget):
-    def __init__(self, flow):
-        super(FlowZoomWidget, self).__init__()
-
-        self.flow = flow
-
-        self.zoom_in_button = QPushButton('+')
-        self.zoom_in_button.clicked.connect(self.on_zoom_in_button_clicked)
-        self.zoom_out_button = QPushButton('-')
-        self.zoom_out_button.clicked.connect(self.on_zoom_out_button_clicked)
-
-        main_horizontal_layout = QHBoxLayout()
-
-        main_horizontal_layout.addWidget(self.zoom_out_button)
-        main_horizontal_layout.addWidget(self.zoom_in_button)
-        self.setLayout(main_horizontal_layout)
-
-
-        self.setStyleSheet('''
-            background: transparent;
-            border: none;
-        ''')
-
-        self.zoom_in_button.setStyleSheet('''
-            QPushButton {
-                background-color: rgba(10, 10, 10, 150);
-                border: 1px solid #aaaaaa;
-                border-radius: 8px;
-                color: #aaaaaa;
-                height: 35px;
-                width: 35px;
-            }
-            QPushButton:hover:pressed {
-                background-color: rgba(100, 100, 100, 150);
-            }
-        ''')
-
-        self.zoom_out_button.setStyleSheet('''
-            QPushButton {
-                background-color: rgba(10, 10, 10, 150);
-                border: 1px solid #aaaaaa;
-                border-radius: 8px;
-                color: #aaaaaa;
-                height: 35px;
-                width: 35px;
-            }
-            QPushButton:hover:pressed {
-                background-color: rgba(100, 100, 100, 150);
-            }
-        ''')
-
-
-    def on_zoom_in_button_clicked(self):
-        self.flow.zoom_in(250)
-
-    def on_zoom_out_button_clicked(self):
-        self.flow.zoom_out(250)
\ No newline at end of file
diff --git a/src/Node.py b/src/Node.py
index fa76875..a134c3c 100644
--- a/src/Node.py
+++ b/src/Node.py
@@ -39,15 +39,14 @@ def __init__(self, params):
         self.session = self.script.session
         self.inputs: [NodeInput] = []
         self.outputs: [NodeOutput] = []
-        self.default_actions = default_node_actions
+        self.default_actions = self.init_default_actions()
         self.special_actions = {}
         self.logs = []
 
         self.init_config = config
+        self.initialized = False
 
-        # CAUTION !!!
-        # The item will live in a different thread! No direct method calls after initialization!
-        self.item = NodeItem(self, params)
+        self.item = None  # set by the flow widget
 
     def finish_initialization(self):
 
@@ -56,19 +55,27 @@ def finish_initialization(self):
 
             self.special_actions = self.set_special_actions_data(self.init_config['special actions'])
 
+        else:
+            self.setup_ports()
+
+        self.enable_logs()
+
+        self._initialized()
+
+        self.initialized = True
+
+        # self.update()
+
+    def load_config_data(self):
+        if self.init_config:
             try:
                 self.set_data(self.init_config['state data'])
             except Exception as e:
                 print('Exception while setting data in', self.title, 'Node:', e,
                       ' (was this intended?)')
-        else:
-            self.setup_ports()
-
-        self.item.initialized()
-
-        self.initialized()
 
-        self.update()
+    def init_default_actions(self) -> dict:
+        return {'update shape': {'method': self.update_shape}}
 
     def setup_ports(self, inputs_config=None, outputs_config=None):
 
@@ -84,7 +91,7 @@ def setup_ports(self, inputs_config=None, outputs_config=None):
                 self.create_output(out.type_, out.label)
         else:  # when loading saved NIs, the init_inputs and init_outputs are irrelevant
             for inp in inputs_config:
-                has_widget = inp['has widget']
+                has_widget = inp['has widget'] if inp['type'] == 'data' else False
 
                 self.create_input(type_=inp['type'], label=inp['label'],
                                   widget_name=inp['widget name'] if has_widget else None,
@@ -148,16 +155,12 @@ def set_output_val(self, index, val):
         """Sets the value of a data output.
         self.data_outputs_updated() has to be called manually after all values are set."""
 
-        if not self.session.threading_enabled:
-            if self.flow.vp_update_mode == FlowVPUpdateMode.ASYNC and not self.item.initializing:  # asynchronous viewport updates
-                vp = self.flow.viewport()
-                vp.repaint(self.flow.mapFromScene(self.item.sceneBoundingRect()))
-
         self.outputs[index].set_val(val)
 
-    def remove_event(self):
-        """Method to stop all threads in hold of the NI itself."""
+    def place_event(self):
+        pass
 
+    def remove_event(self):
         pass
 
     #                                 _
@@ -169,7 +172,8 @@ def remove_event(self):
     #
     # all algorithm-unrelated api methods:
 
-    def initialized(self):
+    def _initialized(self):
+        """Called after the node has been initialized and the item has been created."""
         pass
 
     #   LOGGING
@@ -234,8 +238,8 @@ def create_input(self, type_: str = 'data', label: str = '', widget_name=None,
         # self.item.add_new_input(inp, pos)
         self.input_added.emit(inp, pos)
 
-        if self.session.threading_enabled:
-            inp.moveToThread(self.flow.worker_thread)
+        # if self.session.threaded:
+        #     inp.moveToThread(self.flow.worker_thread)
 
 
     def delete_input(self, i):
@@ -248,7 +252,7 @@ def delete_input(self, i):
 
         # break all connections
         for c in inp.connections:
-            self.flow.connect_ports(c.out, inp)
+            self.flow.connect_nodes(c.out, inp)
 
         self.inputs.remove(inp)
         # self.item.remove_input(inp)
@@ -275,8 +279,8 @@ def create_output(self, type_: str = 'data', label: str = '', pos=-1):
         # self.item.add_new_output(out, pos)
         self.output_added.emit(out, pos)
 
-        if self.session.threading_enabled:
-            out.moveToThread(self.flow.worker_thread)
+        # if self.session.threaded:
+        #     out.moveToThread(self.flow.worker_thread)
 
     def delete_output(self, o):
         """Disconnects and removes output. Handy for subclasses."""
@@ -288,7 +292,7 @@ def delete_output(self, o):
 
         # break all connections
         for c in out.connections:
-            self.flow.connect_ports(out, c.inp)
+            self.flow.connect_nodes(out, c.inp)
 
         self.outputs.remove(out)
         # self.item.remove_output(out)
@@ -316,7 +320,7 @@ class in get_data(), see above.
         pass
 
     def session_stylesheet(self) -> str:
-        return self.flow.session.design.global_stylesheet
+        return self.session.design.global_stylesheet
 
     # VARIABLES
 
@@ -386,7 +390,7 @@ def action_exec_input(self, data):
     # def action_remove(self):
     #     self.flow.remove_node_item(self.item)
 
-    def about_to_remove_from_scene(self):
+    def prepare_removal(self):
         """Called from Flow when the NI gets removed from the scene
         to stop all running threads and disable personal logs."""
 
@@ -411,19 +415,16 @@ def has_main_widget(self):
         return self.main_widget() is not None
 
 
-    def config_data(self, item_config):
+    def config_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_dict = {'identifier': self.__class__.__name__,
-                     'position x': item_config['pos x'],
-                     'position y': item_config['pos y']}
-        if self.main_widget():
-            node_dict['main widget data'] = self.main_widget().get_data()
-
-        node_dict['state data'] = self.get_data()
-        node_dict['special actions'] = self.get_special_actions_data(self.special_actions)
+        node_dict = {
+            'identifier': self.__class__.__name__,
+            'state data': self.get_data(),
+            'special actions': self.get_special_actions_data(self.special_actions)
+        }
 
         # inputs
         inputs = []
@@ -440,11 +441,3 @@ def config_data(self, item_config):
         node_dict['outputs'] = outputs
 
         return node_dict
-
-
-
-
-
-
-default_node_actions = {  # 'remove': {'method': Node.action_remove},
-                   'update shape': {'method': Node.update_shape}}
diff --git a/src/NodeInstance.py b/src/NodeInstance.py
deleted file mode 100644
index f0edf9e..0000000
--- a/src/NodeInstance.py
+++ /dev/null
@@ -1,644 +0,0 @@
-from PySide2.QtWidgets import QGraphicsItem, QMenu, QGraphicsDropShadowEffect
-from PySide2.QtCore import Qt, QRectF
-from PySide2.QtGui import QColor
-
-from .NodeInstanceAction import NodeInstanceAction
-from .NodeInstanceAnimator import NodeInstanceAnimator
-from .NodeInstanceWidget import NodeInstanceWidget
-from .RC import FlowVPUpdateMode
-from .global_tools.Debugger import Debugger
-from .global_tools.MovementEnum import MovementEnum
-from .logging.Log import Log
-
-from .PortInstance import InputPortInstance, OutputPortInstance
-from .retain import M
-
-
-class NodeInstance(QGraphicsItem):
-    def __init__(self, params):
-        super(NodeInstance, self).__init__()
-
-
-        # GENERAL ATTRIBUTES
-
-        # the constructor parameters are stored in a tuple to make the source code of custom NIs cleaner
-        parent_node, flow, design, config = params
-
-        self.parent_node = parent_node
-        self.flow = flow
-        self.session_design = design
-        self.script = flow.script
-        self.movement_state = None
-        self.movement_pos_from = None
-        self.painted_once = False
-        self.inputs = []
-        self.outputs = []
-        self.color = QColor(self.parent_node.color)  # manipulated by self.animator
-
-        self.default_actions = {'remove': {'method': self.action_remove},
-                                'update shape': {'method': self.update_shape}}
-                                # 'console ref': {'method': self.set_console_scope}}  # for context menus
-        self.special_actions = {}  # only gets written in custom NodeInstance-subclasses
-        self.personal_logs = []
-
-        # 'initializing' will be set to False below. It's needed for the ports setup, to prevent shape updating stuff
-        self.initializing = True
-
-        self.temp_state_data = None
-        self.init_config = config
-
-
-        # FLAGS
-        self.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable |
-                      QGraphicsItem.ItemSendsScenePositionChanges)
-        self.setAcceptHoverEvents(True)
-        self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
-
-
-
-        # UI
-        self.shadow_effect = None
-        self.main_widget = None
-        if self.parent_node.main_widget_class is not None:
-            self.main_widget = self.parent_node.main_widget_class(self)
-        self.widget = NodeInstanceWidget(self)  # QGraphicsWidget(self)
-
-        self.animator = NodeInstanceAnimator(self)  # needs self.title_label
-
-        # TOOLTIP
-        if self.parent_node.description != '':
-            self.setToolTip('<html><head/><body><p>'+self.parent_node.description+'</p></body></html>')
-        self.setCursor(Qt.SizeAllCursor)
-
-        # DESIGN THEME
-        self.session_design.flow_theme_changed.connect(self.update_design)
-
-
-
-    def initialized(self):
-        """All ports and the main widget get finally created here."""
-
-        # LOADING CONFIG
-        if self.init_config is not None:
-            # self.setPos(config['position x'], config['position y'])
-            self.setup_ports(self.init_config['inputs'], self.init_config['outputs'])
-            if self.main_widget:
-                try:
-                    self.main_widget.set_data(self.init_config['main widget data'])
-                except Exception as e:
-                    print('Exception while setting data in', self.parent_node.title, 'NodeInstance\'s main widget:', e,
-                          ' (was this intended?)')
-
-            self.special_actions = self.set_special_actions_data(self.init_config['special actions'])
-            self.temp_state_data = self.init_config['state data']
-        else:
-            self.setup_ports()
-
-        # LOADING DATA
-        if self.temp_state_data is not None:
-            try:
-                self.set_data(self.temp_state_data)
-            except Exception as e:
-                print('Exception while setting data in', self.parent_node.title, 'NodeInstance:', e,
-                      ' (was this intended?)')
-
-        self.initializing = False
-
-        # No self.update_shape() here because for some reason, the bounding rect hasn't been initialized yet, so
-        # self.update_shape() gets called when the item is being drawn the first time (see paint event in NI painter)
-        # TODO: change that ^ once there is a solution for this: https://forum.qt.io/topic/117179/force-qgraphicsitem-to-update-immediately-wait-for-update-event
-
-        self.update_design()  # load current design, update QGraphicsItem
-
-        self.update()  # and finally update the NodeInstance once
-
-    #                        __                             _    __     __
-    #              ____ _   / /  ____ _   ____     _____   (_)  / /_   / /_     ____ ___
-    #             / __ `/  / /  / __ `/  / __ \   / ___/  / /  / __/  / __ \   / __ `__ \
-    #            / /_/ /  / /  / /_/ /  / /_/ /  / /     / /  / /_   / / / /  / / / / / /
-    #            \__,_/  /_/   \__, /   \____/  /_/     /_/   \__/  /_/ /_/  /_/ /_/ /_/
-    #                         /____/
-
-    def update(self, input_called=-1, output_called=-1):
-        """This is the method used to activate a NodeInstance. Note that this signature shadows the update() method from
-        QGraphicsItem used to graphically update a QGraphicsItem which can be accessed via
-        QGraphicsItem.update(self)."""
-
-        if self.session_design.animations_enabled:
-            self.animator.start()
-
-        Debugger.write('update in', self.parent_node.title, 'on input', input_called)
-        try:
-            self.update_event(input_called)
-        except Exception as e:
-            Debugger.write_err('EXCEPTION IN', self.parent_node.title, 'NI:', e)
-
-    def update_event(self, input_called=-1):
-        """Gets called when an input received a signal. This is where the magic begins in subclasses."""
-
-        pass
-
-    def input(self, index: int):
-        """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.write('input called in', self.parent_node.title, 'NI:', index)
-        return self.inputs[index].get_val()
-
-    def exec_output(self, index: int):
-        """Executes an execution output, sending a signal to all connected execution inputs causing the connected
-        NIs to update."""
-        self.outputs[index].exec()
-
-    def set_output_val(self, index, val):
-        """Sets the value of a data output.
-        self.data_outputs_updated() has to be called manually after all values are set."""
-
-        if self.flow.vp_update_mode == FlowVPUpdateMode.ASYNC:  # asynchronous viewport updates
-            vp = self.flow.viewport()
-            vp.repaint(self.flow.mapFromScene(self.sceneBoundingRect()))
-
-        self.outputs[index].set_val(val)
-
-    def remove_event(self):
-        """Method to stop all threads in hold of the NI itself."""
-
-        pass
-
-    #                                 _
-    #              ____ _   ____     (_)
-    #             / __ `/  / __ \   / /
-    #            / /_/ /  / /_/ /  / /
-    #            \__,_/  / .___/  /_/
-    #                   /_/
-    #
-    # all algorithm-unrelated api methods:
-
-    #   LOGGING
-    def new_log(self, title) -> Log:
-        """Requesting a new personal Log."""
-        # new_log = self.script.logger.new_log(self, title)
-        new_log = self.script.logger.new_log(title)
-        self.personal_logs.append(new_log)
-        return new_log
-
-    def disable_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_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, msg: str, target: str):
-        """Writes a string to a default log with title target"""
-
-        self.script.logger.log_message(msg, target)
-
-    # SHAPE
-    def update_shape(self):
-        """Causes recompilation of the whole shape."""
-        self.widget.update_shape()
-        self.flow.viewport().update()
-
-    # PORTS
-    def create_new_input(self, type_: str = 'data', label: str = '', widget_name=None,
-                         widget_pos='besides', pos=-1, config=None):
-        """
-        Creates and adds a new input.
-        :widget_pos: 'besides' or 'below'
-        """
-        Debugger.write('create_new_input called')
-
-        # backwards compatibility
-        widget_pos = widget_pos if widget_pos != 'under' else 'below'
-
-        pi = InputPortInstance(self, type_, label,
-                               config_data=config,
-                               widget_name=widget_name,
-                               widget_pos=widget_pos)
-        if pos < -1:
-            pos += len(self.inputs)
-        if pos == -1:
-            self.inputs.append(pi)
-            self.widget.add_input_to_layout(pi)
-        else:
-            self.inputs.insert(pos, pi)
-            self.widget.insert_input_into_layout(pos, pi)
-
-        if not self.initializing:
-            self.update_shape()
-            self.update()
-
-    def delete_input(self, i):
-        """Disconnects and removes input."""
-        inp: InputPortInstance = None
-        if type(i) == int:
-            inp = self.inputs[i]
-        elif type(i) == InputPortInstance:
-            inp = i
-
-        for c in inp.connections:
-            self.flow.connect_pins(c.out, inp)
-
-        # for some reason, I have to remove all widget items manually from the scene too. setting the items to
-        # ownedByLayout(True) does not work, I don't know why.
-        self.scene().removeItem(inp.pin)
-        self.scene().removeItem(inp.label)
-        if inp.proxy is not None:
-            self.scene().removeItem(inp.proxy)
-
-        self.inputs.remove(inp)
-        self.widget.remove_input_from_layout(inp)
-
-        if not self.initializing:
-            self.update_shape()
-            self.update()
-
-
-    def create_new_output(self, type_: str = 'data', label: str = '', pos=-1):
-        """Creates and adds a new output."""
-
-        pi = OutputPortInstance(self, type_, label)
-        if pos < -1:
-            pos += len(self.outputs)
-        if pos == -1:
-            self.outputs.append(pi)
-            self.widget.add_output_to_layout(pi)
-        else:
-            self.outputs.insert(pos, pi)
-            self.widget.insert_output_into_layout(pos, pi)
-
-        if not self.initializing:
-            self.update_shape()
-            self.update()
-
-    def delete_output(self, o):
-        """Disconnects and removes output. Handy for subclasses."""
-        out: OutputPortInstance = None
-        if type(o) == int:
-            out = self.outputs[o]
-        elif type(o) == OutputPortInstance:
-            out = o
-
-        for cpi in out.connections:
-            self.flow.connect_pins(out, cpi)
-
-        # see delete_input() for info!
-        self.scene().removeItem(out.pin)
-        self.scene().removeItem(out.label)
-
-        self.outputs.remove(out)
-        self.widget.remove_output_from_layout(out)
-
-        if not self.initializing:
-            self.update_shape()
-            self.update()
-
-    # GET, SET DATA
-    def get_data(self):
-        """
-        This method gets subclassed and specified. If the NI has states (so, the behavior depends on certain values),
-        all these values must be stored in JSON-able format in a dict here. This dictionary will be used to reload the
-        node's state when loading a project or pasting copied/cut nodes in the Flow (the states get copied too), see
-        self.set_data(self, data) below.
-        Unfortunately, I can't use pickle or something like that due to PySide2 which runs on C++, not Python.
-        :return: Dictionary representing all values necessary to determine the NI's current state
-        """
-        return {}
-
-    def set_data(self, data):
-        """
-        If the NI has states, it's state should get reloaded here according to what was previously provided by the same
-        class in get_data(), see above.
-        :param data: Dictionary representing all values necessary to determine the NI's current state
-        """
-        pass
-
-    def session_stylesheet(self) -> str:
-        return self.session_design.global_stylesheet
-
-    # VARIABLES
-
-    def get_vars_manager(self):
-        return self.script.vars_manager
-
-    def get_var_val(self, name: str):
-        return self.get_vars_manager().get_var_val(name)
-
-    def set_var_val(self, name: str, val):
-        return self.get_vars_manager().set_var(name, val)
-
-    def register_var_receiver(self, name: str, method):
-        self.get_vars_manager().register_receiver(self, name, method)
-
-    def unregister_var_receiver(self, name: str):
-        self.get_vars_manager().unregister_receiver(self, name)
-
-    # --------------------------------------------------------------------------------------
-    # UI STUFF ----------------------------------------
-
-    # def set_console_scope(self):
-    #     # extensive_dict = {}  # unlike self.__dict__, it also includes methods to call! :)
-    #     # for att in dir(self):
-    #     #     extensive_dict[att] = getattr(self, att)
-    #     MainConsole.main_console.add_obj_context(self)
-
-    def update_design(self):
-        """Loads the shadow effect option and causes redraw with active theme."""
-
-        if self.session_design.node_inst_shadows_enabled:
-            self.shadow_effect = QGraphicsDropShadowEffect()
-            self.shadow_effect.setXOffset(12)
-            self.shadow_effect.setYOffset(12)
-            self.shadow_effect.setBlurRadius(20)
-            self.shadow_effect.setColor(QColor('#2b2b2b'))
-            self.setGraphicsEffect(self.shadow_effect)
-        else:
-            self.setGraphicsEffect(None)
-
-        self.widget.update()
-        self.animator.reload_values()
-
-        QGraphicsItem.update(self)
-
-    def boundingRect(self):
-        # remember: (0, 0) shall be the NI's center!
-        rect = QRectF()
-        w = self.widget.layout().geometry().width()
-        h = self.widget.layout().geometry().height()
-        rect.setLeft(-w/2)
-        rect.setTop(-h/2)
-        rect.setWidth(w)
-        rect.setHeight(h)
-        return rect
-
-    #   PAINTING
-    def paint(self, painter, option, widget=None):
-        """All painting is done by NodeInstancePainter"""
-
-        # in order to access a meaningful geometry of GraphicsWidget contents in update_shape(), the paint event
-        # has to be called once. See here:
-        # https://forum.qt.io/topic/117179/force-qgraphicsitem-to-update-immediately-wait-for-update-event/4
-        if not self.painted_once:
-
-            # ok, quick notice. Since I am using a NodeInstanceWidget, calling self.update_design() here (again)
-            # leads to a QT crash without error, which is really strange. Calling update_design multiple times
-            # principally isn't a problem, but, for some reason, here it leads to a crash in QT. It's not necessary
-            # anymore, so I just removed it.
-            # self.update_design()
-
-            self.update_shape()
-            self.update_conn_pos()
-
-        self.session_design.flow_theme.node_inst_painter.paint_NI(
-            design_style=self.parent_node.design_style,
-            painter=painter,
-            option=option,
-            c=self.color,
-            w=self.boundingRect().width(),
-            h=self.boundingRect().height(),
-            bounding_rect=self.boundingRect(),
-            title_rect=self.widget.title_label.boundingRect()
-        )
-
-        self.painted_once = True
-
-    def get_context_menu(self):
-        menu = QMenu(self.flow)
-
-        for a in self.get_actions(self.get_extended_default_actions(), menu):  # menu needed for 'parent'
-            if type(a) == NodeInstanceAction:
-                menu.addAction(a)
-            elif type(a) == QMenu:
-                menu.addMenu(a)
-
-        menu.addSeparator()
-
-        actions = self.get_actions(self.special_actions, menu)
-        for a in actions:  # menu needed for 'parent'
-            if type(a) == NodeInstanceAction:
-                menu.addAction(a)
-            elif type(a) == QMenu:
-                menu.addMenu(a)
-
-        return menu
-
-    def itemChange(self, change, value):
-        """This method ensures that all connections, selection borders etc. that get drawn in the Flow are constantly
-        redrawn during a NI drag. Also updates the positions of connections"""
-
-        if change == QGraphicsItem.ItemPositionChange:
-            if self.session_design.performance_mode == 'pretty':
-                self.flow.viewport().update()
-            if self.movement_state == MovementEnum.mouse_clicked:
-                self.movement_state = MovementEnum.position_changed
-
-        self.update_conn_pos()
-
-        return QGraphicsItem.itemChange(self, change, value)
-
-    def update_conn_pos(self):
-        """Updates the global positions of connections at outputs"""
-        for o in self.outputs:
-            for c in o.connections:
-                c.recompute()
-        for i in self.inputs:
-            for c in i.connections:
-                c.recompute()
-
-    def hoverEnterEvent(self, event):
-        self.widget.title_label.set_NI_hover_state(hovering=True)
-        QGraphicsItem.hoverEnterEvent(self, event)
-
-    def hoverLeaveEvent(self, event):
-        self.widget.title_label.set_NI_hover_state(hovering=False)
-        QGraphicsItem.hoverLeaveEvent(self, event)
-
-    def mousePressEvent(self, event):
-        """Used for Moving-Commands in Flow - may be replaced later with a nicer determination of a moving action."""
-        self.movement_state = MovementEnum.mouse_clicked
-        self.movement_pos_from = self.pos()
-        return QGraphicsItem.mousePressEvent(self, event)
-
-    def mouseReleaseEvent(self, event):
-        """Used for Moving-Commands in Flow - may be replaced later with a nicer determination of a moving action."""
-        if self.movement_state == MovementEnum.position_changed:
-            self.flow.selected_components_moved(self.pos() - self.movement_pos_from)
-        self.movement_state = None
-        return QGraphicsItem.mouseReleaseEvent(self, event)
-
-    # ACTIONS
-    def get_extended_default_actions(self):
-        actions_dict = self.default_actions.copy()
-        for index in range(len(self.inputs)):
-            inp = self.inputs[index]
-            if inp.type_ == 'exec':
-                actions_dict['exec input '+str(index)] = {'method': self.action_exec_input,
-                                                          'data': {'input index': index}}
-        return actions_dict
-
-    def action_exec_input(self, data):
-        self.update(data['input index'])
-
-    def get_actions(self, actions_dict, menu):
-        actions = []
-
-        for k in actions_dict:
-            v_dict = actions_dict[k]
-            try:
-                method = v_dict['method']
-                data = None
-                try:
-                    data = v_dict['data']
-                except KeyError:
-                    pass
-                action = NodeInstanceAction(k, menu, data)
-                action.triggered_with_data.connect(method)  # see NodeInstanceAction for explanation
-                action.triggered_without_data.connect(method)  # see NodeInstanceAction for explanation
-                actions.append(action)
-            except KeyError:
-                action_menu = QMenu(k, menu)
-                sub_actions = self.get_actions(v_dict, action_menu)
-                for a in sub_actions:
-                    action_menu.addAction(a)
-                actions.append(action_menu)
-
-        return actions
-
-    def action_remove(self):
-        self.flow.remove_node_instance_triggered(self)
-
-    def get_special_actions_data(self, actions):
-        cleaned_actions = actions.copy()
-        for key in cleaned_actions:
-            v = cleaned_actions[key]
-            if type(v) == M:  # callable(v):
-                cleaned_actions[key] = v.method_name
-            elif callable(v):
-                cleaned_actions[key] = v.__name__
-            elif type(v) == dict:
-                cleaned_actions[key] = self.get_special_actions_data(v)
-            else:
-                cleaned_actions[key] = v
-        return cleaned_actions
-
-    def set_special_actions_data(self, actions_data):
-        actions = {}
-        for key in actions_data:
-            if type(actions_data[key]) != dict:
-                if key == 'method':
-                    try:
-                        actions['method'] = M(getattr(self, actions_data[key]))
-                    except AttributeError:  # outdated method referenced
-                        pass
-                elif key == 'data':
-                    actions['data'] = actions_data[key]
-            else:
-                actions[key] = self.set_special_actions_data(actions_data[key])
-        return actions
-
-    # PORTS
-    def setup_ports(self, inputs_config=None, outputs_config=None):
-        if not inputs_config and not outputs_config:
-            for i in range(len(self.parent_node.inputs)):
-                inp = self.parent_node.inputs[i]
-                self.create_new_input(inp.type_, inp.label,
-                                      widget_name=self.parent_node.inputs[i].widget_name,
-                                      widget_pos =self.parent_node.inputs[i].widget_pos)
-
-            for o in range(len(self.parent_node.outputs)):
-                out = self.parent_node.outputs[o]
-                self.create_new_output(out.type_, out.label)
-        else:  # when loading saved NIs, the port instances might not be synchronised to the parent's ports anymore
-            for inp in inputs_config:
-                has_widget = inp['has widget']
-
-                self.create_new_input(inp['type'], inp['label'],
-                                      widget_name=inp['widget name'] if has_widget else None,
-                                      widget_pos =inp['widget position'] if has_widget else None,
-                                      config=inp['widget data'] if has_widget else None)
-
-            for out in outputs_config:
-                self.create_new_output(out['type'], out['label'])
-
-    def add_input_to_scene(self, i):
-        self.flow.scene().addItem(i.pin)
-        self.flow.scene().addItem(i.label)
-        if i.widget:
-            self.flow.scene().addItem(i.proxy)
-
-    def add_output_to_scene(self, o):
-        self.flow.scene().addItem(o.pin)
-        self.flow.scene().addItem(o.label)
-
-    # GENERAL
-    def about_to_remove_from_scene(self):
-        """Called from Flow when the NI gets removed from the scene
-        to stop all running threads and disable personal logs."""
-
-        if self.main_widget:
-            self.main_widget.remove_event()
-        self.remove_event()
-
-        self.disable_logs()
-
-    def is_active(self):
-        for i in self.inputs:
-            if i.type_ == 'exec':
-                return True
-        for o in self.outputs:
-            if o.type_ == 'exec':
-                return True
-        return False
-
-    def has_main_widget(self):
-        """Might be used later in CodePreview_Widget to enable not only showing the NI's class but also it's
-        main_widget's class."""
-        return self.main_widget is not None
-
-    def get_input_widgets(self):
-        """Might be used later in CodePreview_Widget to enable not only showing the NI's class but its input widgets'
-        classes."""
-        input_widgets = []
-        for i in range(len(self.inputs)):
-            inp = self.inputs[i]
-            if inp.widget is not None:
-                input_widgets.append({i: inp.widget})
-        return input_widgets
-
-    def config_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.config_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.config_data()
-            node_instance_outputs_list.append(output_dict)
-        node_instance_dict['outputs'] = node_instance_outputs_list
-
-        return node_instance_dict
\ No newline at end of file
diff --git a/src/NodeInstanceAction.py b/src/NodeInstanceAction.py
deleted file mode 100644
index 7ea60f4..0000000
--- a/src/NodeInstanceAction.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from PySide2.QtCore import Signal
-from PySide2.QtWidgets import QAction
-
-
-class NodeInstanceAction(QAction):
-    """A custom implementation of QAction that additionally stores transmitted 'data' which can be intuitively used
-    in subclasses f.ex. to determine the exact source of the action triggered. For more info see GitHub docs.
-    It shall not be a must to use the data parameter though. For that reason, there are two different signals,
-    one that triggers with transmitted data, one without.
-    So, if a special action does not have 'data', the connected method does not need to have a data parameter.
-    Both signals get connected to the target method but only if data isn't None, the signal with the data parameter
-    is used."""
-
-    triggered_with_data = Signal(object)
-    triggered_without_data = Signal()
-
-    def __init__(self, text, menu, data=None):
-        super(NodeInstanceAction, self).__init__(text=text, parent=menu)
-
-        self.data = data
-        self.triggered.connect(self.triggered_)  # yeah, I think that's ugly but I didn't find a nicer way; it works
-
-    def triggered_(self):
-        if self.data is not None:
-            self.triggered_with_data.emit(self.data)
-        else:
-            self.triggered_without_data.emit()
\ No newline at end of file
diff --git a/src/NodeInstanceAnimator.py b/src/NodeInstanceAnimator.py
deleted file mode 100644
index 714344d..0000000
--- a/src/NodeInstanceAnimator.py
+++ /dev/null
@@ -1,67 +0,0 @@
-from PySide2.QtCore import QObject, QPropertyAnimation, Property
-from PySide2.QtGui import QColor
-from PySide2.QtWidgets import QGraphicsItem
-
-
-class NodeInstanceAnimator(QObject):
-    def __init__(self, node_instance):
-        super(NodeInstanceAnimator, self).__init__()
-
-        self.node_instance = node_instance
-        self.animation_running = False
-
-        self.title_activation_animation = QPropertyAnimation(self, b"p_title_color")
-        self.title_activation_animation.setDuration(700)
-        self.title_activation_animation.finished.connect(self.finished)
-        self.body_activation_animation = QPropertyAnimation(self, b"p_body_color")
-        self.body_activation_animation.setDuration(700)
-
-    def start(self):
-        self.animation_running = True
-        self.title_activation_animation.start()
-        self.body_activation_animation.start()
-
-    def stop(self):
-        # reset color values. it would just freeze without
-        self.title_activation_animation.setCurrentTime(self.title_activation_animation.duration())
-        self.body_activation_animation.setCurrentTime(self.body_activation_animation.duration())
-
-        self.title_activation_animation.stop()
-        self.body_activation_animation.stop()
-
-    def finished(self):
-        self.animation_running = False
-
-    def running(self):
-        return self.animation_running
-
-    def reload_values(self):
-        self.stop()
-
-        # self.node_instance.title_label.update_design()
-        self.title_activation_animation.setKeyValueAt(0, self.get_title_color())
-        self.title_activation_animation.setKeyValueAt(0.3, self.get_body_color().lighter().lighter())
-        self.title_activation_animation.setKeyValueAt(1, self.get_title_color())
-
-        self.body_activation_animation.setKeyValueAt(0, self.get_body_color())
-        self.body_activation_animation.setKeyValueAt(0.3, self.get_body_color().lighter())
-        self.body_activation_animation.setKeyValueAt(1, self.get_body_color())
-
-    def get_body_color(self):
-        return self.node_instance.color
-
-    def set_body_color(self, val):
-        self.node_instance.color = val
-        QGraphicsItem.update(self.node_instance)
-
-    p_body_color = Property(QColor, get_body_color, set_body_color)
-
-
-    def get_title_color(self):
-        return self.node_instance.widget.title_label.color
-
-    def set_title_color(self, val):
-        self.node_instance.widget.title_label.color = val
-        # QGraphicsItem.update(self.node_instance)
-
-    p_title_color = Property(QColor, get_title_color, set_title_color)
\ No newline at end of file
diff --git a/src/NodeInstancePainter.py b/src/NodeInstancePainter.py
deleted file mode 100644
index 657de41..0000000
--- a/src/NodeInstancePainter.py
+++ /dev/null
@@ -1,921 +0,0 @@
-from PySide2.QtCore import Qt, QPointF, QRectF
-from PySide2.QtGui import QColor, QPainter, QBrush, QRadialGradient, QLinearGradient, QPen, QPainterPath, QFont
-from PySide2.QtWidgets import QStyle
-
-from .global_tools.math import pythagoras
-
-
-class NIPainter:
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        pass
-
-    def paint_NI_title_label_default(painter, design_style, title_str, c, pen_w, font, bounding_rect):
-        pen = QPen(c)
-        pen.setWidth(pen_w)
-
-        painter.setPen(pen)
-        painter.setFont(font)
-
-        text_rect = bounding_rect
-        text_rect.setTop(text_rect.top()-7)
-
-        if design_style == 'extended':
-            painter.drawText(text_rect, Qt.AlignLeft | Qt.AlignVCenter, title_str)
-        elif design_style == 'small':
-            painter.drawText(text_rect, Qt.AlignTop | Qt.AlignHCenter, title_str)
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        pass
-
-    def paint_PI_label_default(painter, label_str, c, font, bounding_rect):
-        painter.setBrush(Qt.NoBrush)
-        pen = QPen(c)
-        painter.setPen(pen)
-        painter.setFont(font)
-        painter.drawText(bounding_rect, Qt.AlignCenter, label_str)
-
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-        pass
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-        pass
-
-    def get_header_rect(w, h, title_rect):
-        """
-        :param w: width
-        :param h: height
-        """
-
-        header_height = 1.4 * title_rect.height()  # 35 * (self.parent_node.title.count('\n')+1)
-
-        header_rect = QRectF()
-        header_rect.setTopLeft(QPointF(-w / 2, -h / 2))
-        header_rect.setWidth(w)
-        header_rect.setHeight(header_height)
-        return header_rect
-
-    def interpolate_color(c1, c2, val):
-        r1 = c1.red()
-        g1 = c1.green()
-        b1 = c2.blue()
-        a1 = c1.alpha()
-
-        r2 = c2.red()
-        g2 = c2.green()
-        b2 = c2.blue()
-        a2 = c2.alpha()
-
-        r = (r2 - r1) * val + r1
-        g = (g2 - g1) * val + g1
-        b = (b2 - b1) * val + b1
-        a = (a2 - a1) * val + a1
-
-        return QColor(r, g, b, a)
-
-
-class NIPainter_DarkStd(NIPainter):
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        if design_style == 'extended':
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, QColor(30, 43, 48) if not hovering else node_color.lighter(),
-                2 if hovering else 1.5,
-                QFont('Poppins', 15),
-                bounding_rect
-            )
-        else:
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, QColor(30, 43, 48) if not hovering else node_color.lighter(),
-                1.5,
-                QFont('K2D', 20, QFont.Bold, True),
-                bounding_rect
-            )
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        c = QColor('#FFFFFF')
-        NIPainter.paint_PI_label_default(painter, label_str, c, QFont("Source Code Pro", 10, QFont.Bold), bounding_rect)
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-
-        color = QColor('#2E688C') if exec_type == 'data' else QColor('#3880ad')
-        if option.state & QStyle.State_MouseOver:
-            color = color.lighter()
-
-        brush = QBrush(QColor(color))
-        painter.setBrush(brush)
-        painter.setPen(Qt.NoPen)
-
-        painter.drawEllipse(QRectF(padding, padding, w, h))
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-
-        painter.setRenderHint(QPainter.Antialiasing)
-
-        if design_style == 'extended':
-            NIPainter_DarkStd.draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect)
-        elif design_style == 'small':
-            NIPainter_DarkStd.draw_NI_minimalistic(painter, c, w, h, bounding_rect)
-
-    def draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect):
-        """
-        :param painter: painter from paint event
-        :param c: NodeInstance's theme color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeInstance's bounding rect
-        :param title_rect: NI's title label's bounding rect
-        """
-
-        # main rect
-        body_gradient = QRadialGradient(bounding_rect.topLeft(), pythagoras(h, w))
-        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(bounding_rect, 12, 12)
-
-        header_gradient = QLinearGradient(NIPainter_DarkStd.get_header_rect(w, h, title_rect).topRight(),
-                                          NIPainter_DarkStd.get_header_rect(w, h, title_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(NIPainter_DarkStd.get_header_rect(w, h, title_rect), 12, 12)
-
-    def draw_NI_minimalistic(painter, c, w, h, bounding_rect):
-        """
-        :param painter: painter from paint event
-        :param c: color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeInstance's bounding rect
-        """
-
-        path = QPainterPath()
-        path.moveTo(-w / 2, 0)
-
-        path.cubicTo(-w / 2, -h / 2,
-                     -w / 2, -h / 2,
-                     0, -h / 2)
-        path.cubicTo(+w / 2, -h / 2,
-                     +w / 2, -h / 2,
-                     +w / 2, 0)
-        path.cubicTo(+w / 2, +h / 2,
-                     +w / 2, +h / 2,
-                     0, +h / 2)
-        path.cubicTo(-w / 2, +h / 2,
-                     -w / 2, +h / 2,
-                     -w / 2, 0)
-        path.closeSubpath()
-
-        body_gradient = QLinearGradient(bounding_rect.bottomLeft(),
-                                        bounding_rect.topRight())
-        body_gradient.setColorAt(0, QColor(c.red(), c.green(), c.blue(), 150))
-        body_gradient.setColorAt(1, QColor(c.red(), c.green(), c.blue(), 80))
-
-        painter.setBrush(body_gradient)
-        painter.setPen(QPen(QColor(30, 43, 48)))
-
-        painter.drawPath(path)
-
-
-class NIPainter_DarkTron(NIPainter):
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        if design_style == 'extended':
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color if not hovering else node_color.lighter(),
-                2,
-                QFont('Poppins', 15),
-                bounding_rect
-            )
-        else:
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color,
-                2,
-                QFont('K2D', 20, QFont.Bold, True),
-                bounding_rect
-            )
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        if exec_type == 'exec':
-            c = QColor('#FFFFFF')
-        else:
-            c = node_color
-        NIPainter.paint_PI_label_default(painter, label_str, c, QFont("Source Code Pro", 10, QFont.Bold), bounding_rect)
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-
-        color = QColor('#FFFFFF') if exec_type == 'exec' else node_color
-        pen = QPen(color)
-        pen.setWidth(2)
-        painter.setPen(pen)
-        if connected or \
-                option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-            r = node_color.red()
-            g = node_color.green()
-            b = node_color.blue()
-            brush = QBrush(QColor(r, g, b, 100))
-            painter.setBrush(brush)
-        else:
-            painter.setBrush(Qt.NoBrush)
-
-        painter.drawEllipse(QRectF(padding, padding, w, h))
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-
-        painter.setRenderHint(QPainter.Antialiasing)
-
-        if design_style == 'extended':
-            NIPainter_DarkTron.draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect)
-        elif design_style == 'small':
-            if option.state & QStyle.State_MouseOver:  # use special dark background color when mouse hovers
-                NIPainter_DarkTron.draw_NI_minimalistic(painter, c, w, h, background_color=c.darker())
-            else:
-                NIPainter_DarkTron.draw_NI_minimalistic(painter, c, w, h)
-
-    def draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect):
-        """
-        :param painter: painter from paint event
-        :param c: NodeInstance's theme color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeInstance's bounding rect
-        :param title_rect: NI's title label's bounding rect
-        """
-
-        background_color = QColor('#212224')
-        painter.setBrush(background_color)
-        pen = QPen(c)
-        pen.setWidth(2)
-        painter.setPen(pen)
-        body_path = NIPainter_DarkTron.get_extended_body_path(w, h)
-        painter.drawPath(body_path)
-
-        header_gradient = QLinearGradient(NIPainter_DarkTron.get_header_rect(w, h, title_rect).topRight(),
-                                          NIPainter_DarkTron.get_header_rect(w, h, title_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 = NIPainter_DarkTron.get_extended_header_path(w, h, title_rect)
-        painter.drawPath(header_path)
-
-    def get_extended_body_path(w, h):
-        """
-        :param c_s: corner size/corner radius
-        :param w: width
-        :param h: height
-        """
-        c_s = 10
-
-        path = QPainterPath()
-        path.moveTo(+w / 2, -h / 2 + c_s)
-        path.lineTo(+w / 2 - c_s, -h / 2)
-        path.lineTo(-w / 2 + c_s, -h / 2)
-        path.lineTo(-w / 2, -h / 2 + c_s)
-        path.lineTo(-w / 2, +h / 2 - c_s)
-        path.lineTo(-w / 2 + c_s, +h / 2)
-        path.lineTo(+w / 2 - c_s, +h / 2)
-        path.lineTo(+w / 2, +h / 2 - c_s)
-        path.closeSubpath()
-        return path
-
-    def get_extended_header_path(w, h, title_rect):
-        """
-        :param w: width
-        :param h: height
-        :param title_rect: NI's title label's bounding rect
-        """
-        c_s = 10
-
-        # header_height = 35 * (NIPainter_DarkTron.ni.parent_node.title.count('\n') + 1)
-        header_height = NIPainter_DarkTron.get_header_rect(w, h, title_rect).height()
-        header_bottom = -h / 2 + header_height
-        path = QPainterPath()
-        path.moveTo(+w / 2, -h / 2 + c_s)
-        path.lineTo(+w / 2 - c_s, -h / 2)
-        path.lineTo(-w / 2 + c_s, -h / 2)
-        path.lineTo(-w / 2, -h / 2 + c_s)
-        path.lineTo(-w / 2, header_bottom - c_s)
-        path.lineTo(-w / 2 + c_s, header_bottom)
-        path.lineTo(+w / 2 - c_s, header_bottom)
-        path.lineTo(+w / 2, header_bottom - c_s)
-        path.closeSubpath()
-        return path
-
-    def draw_NI_minimalistic(painter, c, w, h, background_color=QColor('#36383B')):
-        """
-        :param painter: painter from paint event
-        :param c_s: corner size/corner radius
-        :param c: color
-        :param w: width
-        :param h: height
-        :param background_color: (default) background color
-        """
-
-        c_s = 10
-
-        path = QPainterPath()
-        path.moveTo(-w / 2, 0)
-
-        path.lineTo(-w / 2 + c_s / 2, -h / 2 + c_s / 2)
-        path.lineTo(0, -h / 2)
-        path.lineTo(+w / 2 - c_s / 2, -h / 2 + c_s / 2)
-        path.lineTo(+w / 2, 0)
-        path.lineTo(+w / 2 - c_s / 2, +h / 2 - c_s / 2)
-        path.lineTo(0, +h / 2)
-        path.lineTo(-w / 2 + c_s / 2, +h / 2 - c_s / 2)
-        path.closeSubpath()
-
-        painter.setBrush(background_color)
-        pen = QPen(c)
-        pen.setWidth(2)
-        painter.setPen(pen)
-
-        painter.drawPath(path)
-
-
-class NIPainter_Ghostly(NIPainter):
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        if design_style == 'extended':
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color if not hovering else node_color.lighter(),
-                2,
-                QFont('Poppins', 15),
-                bounding_rect
-            )
-        else:
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color,
-                2,
-                QFont('K2D', 20, QFont.Bold, True),
-                bounding_rect
-            )
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        if exec_type == 'exec':
-            c = QColor('#FFFFFF')
-        else:
-            c = node_color
-        NIPainter.paint_PI_label_default(painter, label_str, c, QFont("Source Code Pro", 10, QFont.Bold), bounding_rect)
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-
-        color = QColor('#FFFFFF') if exec_type == 'exec' else node_color
-
-        if exec_type == 'exec':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                brush = QBrush(QColor(255, 255, 255, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-        elif exec_type == 'data':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                r = node_color.red()
-                g = node_color.green()
-                b = node_color.blue()
-                brush = QBrush(QColor(r, g, b, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-
-        pen = QPen(color)
-        pen.setWidth(1)
-        painter.setPen(pen)
-        painter.drawEllipse(QRectF(padding+w/4, padding+h/4, w/2, h/2))
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-
-        painter.setRenderHint(QPainter.Antialiasing)
-
-        if design_style == 'extended':
-            NIPainter_Ghostly.draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect)
-        elif design_style == 'small':
-            if option.state & QStyle.State_MouseOver:  # use special dark background color when mouse hovers
-                NIPainter_Ghostly.draw_NI_minimalistic(painter, c, w, h, background_color=c.darker())
-            else:
-                NIPainter_Ghostly.draw_NI_minimalistic(painter, c, w, h)
-
-    def draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect):
-        """
-        :param painter: painter from paint event
-        :param c: NodeInstance's theme color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeInstance's bounding rect
-        :param title_rect: NI's title label's bounding rect
-        """
-
-        background_color = QColor(31, 31, 36, 150)
-
-        body_gradient = QRadialGradient(QPointF(bounding_rect.topLeft().x() + w,
-                                                bounding_rect.topLeft().y() - h),
-                                        pythagoras(2 * h, 2 * w))
-        body_gradient.setColorAt(0, QColor(c.red() / 10 + 100, c.green() / 10 + 100, c.blue() / 10 + 100, 100))
-        body_gradient.setColorAt(0.7, background_color)
-        body_gradient.setColorAt(1, background_color)
-
-        painter.setBrush(body_gradient)
-        painter.setBrush(QColor(28, 28, 28, 170))
-        pen = QPen(c.darker())
-        pen.setWidth(1)
-        painter.setPen(pen)
-        body_path = NIPainter_Ghostly.get_extended_body_path(5, w, h)
-        painter.drawPath(body_path)
-
-    def get_extended_body_path(c_s, w, h):
-        """
-        Very similar to 'extended tron'
-        :param c_s: corner size/corner radius
-        :param w: width
-        :param h: height
-        """
-
-        path = QPainterPath()
-        path.moveTo(+w / 2, -h / 2 + c_s)
-        path.lineTo(+w / 2 - c_s, -h / 2)
-        path.lineTo(-w / 2 + c_s, -h / 2)
-        path.lineTo(-w / 2, -h / 2 + c_s)
-        path.lineTo(-w / 2, +h / 2 - c_s)
-        path.lineTo(-w / 2 + c_s, +h / 2)
-        path.lineTo(+w / 2 - c_s, +h / 2)
-        path.lineTo(+w / 2, +h / 2 - c_s)
-        path.closeSubpath()
-        return path
-
-    def draw_NI_minimalistic(painter, c, w, h, background_color=QColor(30, 30, 30, 170)):
-        """
-        :param painter: painter from paint event
-        :param c_s: corner size/corner radius
-        :param c: color
-        :param w: width
-        :param h: height
-        :param background_color: std background color
-        """
-        c_s = 10
-        path = NIPainter_Ghostly.get_extended_body_path(c_s, w, h)  # equals the minimalistic in this case
-
-        painter.setBrush(background_color)
-        # pen = QPen(QColor('#333333'))  # QPen(c)
-        # pen.setWidth(1)
-        # painter.setPen(pen)
-        painter.setPen(Qt.NoPen)
-
-        painter.drawPath(path)
-
-
-class NIPainter_Blender(NIPainter):
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        if design_style == 'extended':
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, QColor('#FFFFFF'),
-                2,
-                QFont('Poppins', 15),
-                bounding_rect
-            )
-        else:
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color,
-                2,
-                QFont('K2D', 20, QFont.Bold, True),
-                bounding_rect
-            )
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        if exec_type == 'exec':
-            c = QColor('#FFFFFF')
-        else:
-            c = node_color
-        NIPainter.paint_PI_label_default(painter, label_str, c, QFont("Source Code Pro", 10, QFont.Bold), bounding_rect)
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-
-        color = QColor('#FFFFFF') if exec_type == 'exec' else node_color
-
-        if exec_type == 'exec':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                brush = QBrush(QColor(255, 255, 255, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-        elif exec_type == 'data':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                r = node_color.red()
-                g = node_color.green()
-                b = node_color.blue()
-                brush = QBrush(QColor(r, g, b, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-
-        pen = QPen(color)
-        pen.setWidth(1)
-        painter.setPen(pen)
-        painter.drawEllipse(QRectF(padding + w / 4, padding + h / 4, w / 2, h / 2))
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-
-        painter.setRenderHint(QPainter.Antialiasing)
-
-        if design_style == 'extended':
-            NIPainter_Blender.draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect)
-        elif design_style == 'small':
-            NIPainter_Blender.draw_NI_minimalistic(painter, c, w, h, bounding_rect)
-
-    def draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect):
-        """
-        :param painter: painter from paint event
-        :param c: NodeInstance's theme color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeInstance's bounding rect
-        :param title_rect: NI's title label's bounding rect
-        """
-
-        background_color = QColor(100, 100, 100, 150)
-        header_color = QColor(c.red(), c.green(), c.blue(), 180)
-
-        rel_header_height = NIPainter.get_header_rect(w, h, title_rect).height()/h
-        gradient = QLinearGradient(bounding_rect.topLeft(), bounding_rect.bottomLeft())
-        gradient.setColorAt(0, header_color)
-        gradient.setColorAt(rel_header_height, header_color)
-        gradient.setColorAt(rel_header_height+0.0001, background_color)
-        gradient.setColorAt(1, background_color)
-
-        painter.setBrush(gradient)
-        painter.setPen(QPen(c.darker()))
-        painter.drawRoundedRect(bounding_rect, 7, 7)
-
-    def draw_NI_minimalistic(painter, c, w, h, bounding_rect, background_color=QColor(30, 30, 30, 150)):
-        """
-        :param painter: painter from paint event
-        :param c_s: corner size/corner radius
-        :param c: color
-        :param w: width
-        :param h: height
-        :param background_color: std background color
-        """
-        c_s = 15
-        painter.setBrush(NIPainter.interpolate_color(c, background_color, 0.97))
-        painter.setPen(QPen(c))
-        painter.drawRoundedRect(bounding_rect, c_s, c_s)
-
-
-class NIPainter_Easy(NIPainter):
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        if design_style == 'extended':
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, QColor('#312b29'),
-                2,
-                QFont('ASAP', 13, QFont.Bold),
-                bounding_rect
-            )
-        else:
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color,
-                2,
-                QFont('Poppins', 15, QFont.Thin),
-                bounding_rect
-            )
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        c = None
-        if not connected:
-            c = QColor('#53585c')
-        else:
-            if exec_type == 'exec':
-                c = QColor('#cccccc')
-            else:
-                c = node_color
-        NIPainter.paint_PI_label_default(painter, label_str, c, QFont("Courier New", 10, QFont.Bold), bounding_rect)
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-        color = None
-        if not connected:
-            color = QColor('#53585c')
-        else:
-            if exec_type == 'exec':
-                color = QColor('#dddddd')
-            else:
-                color = node_color
-
-        if exec_type == 'exec':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                brush = QBrush(QColor(255, 255, 255, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-        elif exec_type == 'data':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                r = node_color.red()
-                g = node_color.green()
-                b = node_color.blue()
-                brush = QBrush(QColor(r, g, b, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-
-        brush = QBrush(QColor(color))
-        painter.setBrush(brush)
-        painter.setPen(Qt.NoPen)
-
-        painter.drawEllipse(QRectF(padding+w/8, padding+h/8, 3*w/4, 3*h/4))
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-
-        painter.setRenderHint(QPainter.Antialiasing)
-
-        if design_style == 'extended':
-            NIPainter_Easy.draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect)
-        elif design_style == 'small':
-            NIPainter_Easy.draw_NI_minimalistic(painter, c, w, h, bounding_rect)
-
-    def draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect):
-        """
-        :param painter: painter from paint event
-        :param c: NodeInstance's theme color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeInstance's bounding rect
-        :param title_rect: NI's title label's bounding rect
-        """
-
-        background_color = QColor('#2b2e33')
-        header_color = c
-
-        rel_header_height = NIPainter.get_header_rect(w, h, title_rect).height()/h
-        gradient = QLinearGradient(bounding_rect.topLeft(), bounding_rect.bottomLeft())
-        gradient.setColorAt(0, header_color)
-        gradient.setColorAt(rel_header_height, header_color)
-        gradient.setColorAt(rel_header_height+0.0001, background_color)
-        gradient.setColorAt(1, background_color)
-
-        painter.setBrush(gradient)
-        painter.setPen(Qt.NoPen) #QPen(c.darker()))
-        painter.drawRoundedRect(bounding_rect, 9, 9)
-
-    def draw_NI_minimalistic(painter, c, w, h, bounding_rect, background_color=QColor('#2b2e33')):
-        """
-        :param painter: painter from paint event
-        :param c_s: corner size/corner radius
-        :param c: color
-        :param w: width
-        :param h: height
-        :param background_color: std background color
-        """
-        c_s = 10
-        painter.setBrush(NIPainter.interpolate_color(c, background_color, 0.97))
-        painter.setPen(Qt.NoPen)
-        painter.drawRoundedRect(bounding_rect, c_s, c_s)
-
-
-class NIPainter_Peasy(NIPainter):
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        if design_style == 'extended':
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, QColor('#312b29'),
-                2,
-                QFont('ASAP', 13, QFont.Bold),
-                bounding_rect
-            )
-        else:
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color,
-                2,
-                QFont('Poppins', 15, QFont.Thin),
-                bounding_rect
-            )
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        c = None
-        if not connected:
-            c = QColor('#53585c')
-        else:
-            if exec_type == 'exec':
-                c = QColor('#cccccc')
-            else:
-                c = node_color
-        NIPainter.paint_PI_label_default(painter, label_str, c, QFont("Courier New", 10, QFont.Bold), bounding_rect)
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-        color = None
-        if not connected:
-            color = QColor('#53585c')
-        else:
-            if exec_type == 'exec':
-                color = QColor('#dddddd')
-            else:
-                color = node_color
-
-        if exec_type == 'exec':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                brush = QBrush(QColor(255, 255, 255, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-        elif exec_type == 'data':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                r = node_color.red()
-                g = node_color.green()
-                b = node_color.blue()
-                brush = QBrush(QColor(r, g, b, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-
-        brush = QBrush(QColor(color))
-        painter.setBrush(brush)
-        painter.setPen(Qt.NoPen)
-
-        painter.drawEllipse(QRectF(padding+w/8, padding+h/8, 3*w/4, 3*h/4))
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-
-        painter.setRenderHint(QPainter.Antialiasing)
-
-        if design_style == 'extended':
-            NIPainter_Peasy.draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect)
-        elif design_style == 'small':
-            NIPainter_Peasy.draw_NI_minimalistic(painter, c, w, h, bounding_rect)
-
-    def draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect):
-        """
-        :param painter: painter from paint event
-        :param c: NodeInstance's theme color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeInstance's bounding rect
-        :param title_rect: NI's title label's bounding rect
-        """
-
-        background_color = QColor('#212429')
-        header_color = c
-
-        rel_header_height = NIPainter.get_header_rect(w, h, title_rect).height()/h
-        gradient = QLinearGradient(bounding_rect.topLeft(), bounding_rect.bottomLeft())
-        gradient.setColorAt(0, header_color)
-        gradient.setColorAt(rel_header_height, header_color)
-        gradient.setColorAt(rel_header_height+0.0001, background_color)
-        gradient.setColorAt(1, background_color)
-
-        painter.setBrush(gradient)
-        painter.setPen(Qt.NoPen) #QPen(c.darker()))
-        painter.drawRoundedRect(bounding_rect, 9, 9)
-
-    def draw_NI_minimalistic(painter, c, w, h, bounding_rect, background_color=QColor('#212429')):
-        """
-        :param painter: painter from paint event
-        :param c_s: corner size/corner radius
-        :param c: color
-        :param w: width
-        :param h: height
-        :param background_color: std background color
-        """
-        c_s = 10
-        painter.setBrush(NIPainter.interpolate_color(c, background_color, 0.97))
-        painter.setPen(Qt.NoPen)
-        painter.drawRoundedRect(bounding_rect, c_s, c_s)
-
-
-class NIPainter_Ueli(NIPainter):
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        if design_style == 'extended':
-            painter.setPen(QPen(QColor(node_color.name())))
-            painter.setFont(QFont('Poppins', 13))
-            painter.drawText(bounding_rect, Qt.AlignLeft | Qt.AlignVCenter, title_str)
-        else:
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color,
-                2,
-                QFont('Poppins', 15, QFont.Thin),
-                bounding_rect
-            )
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        c = None
-        if not connected:
-            c = QColor('#53585c')
-        else:
-            if exec_type == 'exec':
-                c = QColor('#cccccc')
-            else:
-                c = node_color
-        NIPainter.paint_PI_label_default(painter, label_str, c, QFont("Courier New", 10, QFont.Bold), bounding_rect)
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-        color = None
-        if not connected:
-            color = QColor('#53585c')
-        else:
-            if exec_type == 'exec':
-                color = QColor('#dddddd')
-            else:
-                color = node_color
-
-        if exec_type == 'exec':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                brush = QBrush(QColor(255, 255, 255, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-        elif exec_type == 'data':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                r = node_color.red()
-                g = node_color.green()
-                b = node_color.blue()
-                brush = QBrush(QColor(r, g, b, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-
-        brush = QBrush(QColor(color))
-        painter.setBrush(brush)
-        painter.setPen(Qt.NoPen)
-
-        painter.drawEllipse(QRectF(padding+w/8, padding+h/8, 3*w/4, 3*h/4))
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-
-        painter.setRenderHint(QPainter.Antialiasing)
-
-        if design_style == 'extended':
-            NIPainter_Ueli.draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect)
-        elif design_style == 'small':
-            NIPainter_Ueli.draw_NI_minimalistic(painter, c, w, h, bounding_rect)
-
-    def draw_NI_extended_background(painter, c, w, h, bounding_rect: QRectF, title_rect):
-        """
-        :param painter: painter from paint event
-        :param c: NodeInstance's theme color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeInstance's bounding rect
-        :param title_rect: NI's title label's bounding rect
-        """
-
-        background_color = QColor('#212429')
-        header_color = c
-
-        header_height = NIPainter.get_header_rect(w, h, title_rect).height()
-        rel_header_height = header_height/h
-        gradient = QLinearGradient(bounding_rect.topLeft(), bounding_rect.bottomLeft())
-        gradient.setColorAt(0, header_color)
-        gradient.setColorAt(rel_header_height, header_color)
-        gradient.setColorAt(rel_header_height+0.0001, background_color)
-        gradient.setColorAt(1, background_color)
-
-        painter.setBrush(QBrush(background_color))
-        painter.setPen(Qt.NoPen)  # QPen(c.darker()))
-        painter.drawRoundedRect(QRectF(
-            QPointF(bounding_rect.left(), bounding_rect.top()+header_height),
-            bounding_rect.bottomRight()
-        ), 6, 6)
-
-    def draw_NI_minimalistic(painter, c, w, h, bounding_rect, background_color=QColor('#212429')):
-        """
-        :param painter: painter from paint event
-        :param c_s: corner size/corner radius
-        :param c: color
-        :param w: width
-        :param h: height
-        :param background_color: std background color
-        """
-        c_s = 10
-        painter.setBrush(NIPainter.interpolate_color(c, background_color, 0.97))
-        painter.setPen(Qt.NoPen)
-        painter.drawRoundedRect(bounding_rect, c_s, c_s)
diff --git a/src/NodeInstanceWidget.py b/src/NodeInstanceWidget.py
deleted file mode 100644
index 36a6b64..0000000
--- a/src/NodeInstanceWidget.py
+++ /dev/null
@@ -1,163 +0,0 @@
-from PySide2.QtCore import QPointF, QRectF, Qt
-from PySide2.QtWidgets import QGraphicsWidget, QGraphicsLinearLayout
-
-from .FlowProxyWidget import FlowProxyWidget
-from .NodeInstance_TitleLabel import TitleLabel
-from .PortInstance import InputPortInstance, OutputPortInstance
-
-
-class NodeInstanceWidget(QGraphicsWidget):
-    def __init__(self, node_instance):
-        super().__init__(parent=node_instance)
-
-        self.node_inst = node_instance
-        self.flow = self.node_inst.flow
-
-        self.title_label = TitleLabel(node_instance, parent=node_instance)
-        self.body_layout: QGraphicsLinearLayout = None
-        self.inputs_layout: QGraphicsLinearLayout = None
-        self.outputs_layout: QGraphicsLinearLayout = None
-        self.main_widget_proxy: FlowProxyWidget = None
-        self.setLayout(self.setup_layout())
-
-    def setup_layout(self) -> QGraphicsLinearLayout:
-
-        #   main layout
-        layout = QGraphicsLinearLayout(Qt.Vertical)
-        layout.setSpacing(10)
-
-        if self.node_inst.parent_node.design_style == 'extended':
-            layout.addItem(self.title_label)
-            layout.setAlignment(self.title_label, Qt.AlignTop)
-        else:
-            self.setZValue(self.title_label.zValue()+1)
-
-        #   inputs
-        self.inputs_layout = QGraphicsLinearLayout(Qt.Vertical)
-        self.inputs_layout.setSpacing(2)
-
-        #   outputs
-        self.outputs_layout = QGraphicsLinearLayout(Qt.Vertical)
-        self.outputs_layout.setSpacing(2)
-
-        #   body
-        self.body_layout = QGraphicsLinearLayout(Qt.Horizontal)
-
-        self.body_layout.setSpacing(4)
-        self.body_layout.addItem(self.inputs_layout)
-        self.body_layout.setAlignment(self.inputs_layout, Qt.AlignVCenter | Qt.AlignLeft)
-        self.body_layout.addStretch()
-        self.body_layout.addItem(self.outputs_layout)
-        self.body_layout.setAlignment(self.outputs_layout, Qt.AlignVCenter | Qt.AlignRight)
-
-        if self.node_inst.main_widget is not None:
-            self.main_widget_proxy = FlowProxyWidget(self.flow)
-            self.main_widget_proxy.setWidget(self.node_inst.main_widget)
-
-            if self.node_inst.parent_node.main_widget_pos == 'between ports':
-                self.body_layout.insertItem(1, self.main_widget_proxy)
-                self.body_layout.insertStretch(2)
-                layout.addItem(self.body_layout)
-
-            elif self.node_inst.parent_node.main_widget_pos == 'below ports':
-                layout.addItem(self.body_layout)
-                layout.addItem(self.main_widget_proxy)
-                layout.setAlignment(self.main_widget_proxy, Qt.AlignHCenter)
-        else:
-            layout.addItem(self.body_layout)
-
-        return layout
-
-    def rebuild_ui(self):
-        """Due to some really strange and annoying behaviour of these QGraphicsWidgets, they don't want to shrink
-        automatically when content is removed, they just stay large, even with a Minimum SizePolicy. I didn't find a
-        way around that yet, so for now I have to recreate the whole layout and make sure the widget uses the smallest
-        size possible."""
-
-        # if I don't manually remove the ports from the layouts,
-        # they get deleted when setting the widget's layout to None below
-        for i in range(len(self.node_inst.inputs)):
-            self.inputs_layout.removeAt(0)
-        for i in range(len(self.node_inst.outputs)):
-            self.outputs_layout.removeAt(0)
-
-        self.setLayout(None)
-        self.resize(self.minimumSize())
-        self.setLayout(self.setup_layout())
-
-        for inp in self.node_inst.inputs:
-            self.add_input_to_layout(inp)
-        for out in self.node_inst.outputs:
-            self.add_output_to_layout(out)
-
-    def update_shape(self):
-
-        # if not self.initializing:   # just to make sure
-        #     self.rebuild_ui()       # (hopefully) temporary fix -> see rebuild_ui() docstring
-        mw = self.node_inst.main_widget
-        if mw is not None:  # maybe the main_widget got resized
-            self.main_widget_proxy.setMaximumSize(mw.size())
-            self.adjustSize()
-            self.adjustSize()
-
-        self.body_layout.invalidate()
-        self.layout().invalidate()
-        self.layout().activate()
-        # very essential; repositions everything in case content has changed (inputs/outputs/widget)
-
-        if self.node_inst.parent_node.design_style == 'small':
-
-            # making it recompute its true minimumWidth here
-            self.adjustSize()
-
-            if self.layout().minimumWidth() < self.title_label.width + 15:
-                self.layout().setMinimumWidth(self.title_label.width + 15)
-                self.layout().activate()
-
-        w = self.boundingRect().width()
-        h = self.boundingRect().height()
-        rect = QRectF(QPointF(-w / 2, -h / 2),
-                      QPointF(w / 2, h / 2))
-        self.setPos(rect.left(), rect.top())
-
-        if not self.node_inst.parent_node.design_style == 'extended':
-            self.title_label.setPos(QPointF(-self.title_label.boundingRect().width() / 2,
-                                            -self.title_label.boundingRect().height() / 2))
-
-    def add_input_to_layout(self, inp: InputPortInstance):
-        if self.inputs_layout.count() > 0:
-            self.inputs_layout.addStretch()
-        self.inputs_layout.addItem(inp)
-        self.inputs_layout.setAlignment(inp, Qt.AlignLeft)
-
-    def insert_input_into_layout(self, index: int, inp: InputPortInstance):
-        self.inputs_layout.insertItem(index*2+1, inp)   # *2 bcs of the stretches
-        self.inputs_layout.setAlignment(inp, Qt.AlignLeft)
-        if len(self.node_inst.inputs) > 1:
-            self.inputs_layout.insertStretch(index*2+1)  # *2+1 because of the stretches, too
-
-    def remove_input_from_layout(self, inp: InputPortInstance):
-        self.inputs_layout.removeItem(inp)
-
-        # just a temporary workaround for the issues discussed here:
-        # https://forum.qt.io/topic/116268/qgraphicslayout-not-properly-resizing-to-change-of-content
-        self.rebuild_ui()
-
-    def add_output_to_layout(self, out: OutputPortInstance):
-        if self.outputs_layout.count() > 0:
-            self.outputs_layout.addStretch()
-        self.outputs_layout.addItem(out)
-        self.outputs_layout.setAlignment(out, Qt.AlignRight)
-
-    def insert_output_into_layout(self, index: int, out: OutputPortInstance):
-        self.outputs_layout.insertItem(index*2+1, out)  # *2 because of the stretches
-        self.outputs_layout.setAlignment(out, Qt.AlignRight)
-        if len(self.node_inst.outputs) > 1:
-            self.outputs_layout.insertStretch(index*2+1)  # *2+1 because of the stretches, too
-
-    def remove_output_from_layout(self, out: OutputPortInstance):
-        self.outputs_layout.removeItem(out)
-
-        # just a temporary workaround for the issues discussed here:
-        # https://forum.qt.io/topic/116268/qgraphicslayout-not-properly-resizing-to-change-of-content
-        self.rebuild_ui()
diff --git a/src/NodeInstance_TitleLabel.py b/src/NodeInstance_TitleLabel.py
deleted file mode 100644
index bf21041..0000000
--- a/src/NodeInstance_TitleLabel.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from PySide2.QtCore import QRectF, QPointF, QSizeF, Property
-from PySide2.QtGui import QFont, QFontMetricsF, QColor
-from PySide2.QtWidgets import QGraphicsWidget, QGraphicsLayoutItem, QGraphicsItem
-
-from .global_tools.strings import get_longest_line
-
-
-class TitleLabel(QGraphicsWidget):
-    def __init__(self, parent_node_instance, parent):
-        super(TitleLabel, self).__init__(parent)
-
-        self.setGraphicsItem(self)
-
-        self.parent_node_instance = parent_node_instance
-        self.title_str = self.parent_node_instance.parent_node.title
-        font = QFont('Poppins', 15) if self.parent_node_instance.parent_node.design_style == 'extended' else \
-            QFont('K2D', 20, QFont.Bold, True)  # should be quite similar to every specific font chosen by the painter
-        fm = QFontMetricsF(font)
-
-        # approximately!
-        self.width = fm.width(get_longest_line(self.title_str)+'___')
-        self.height = fm.height() * 0.7 * (self.title_str.count('\n') + 1)
-
-        self.color = QColor(30, 43, 48)
-        self.pen_width = 1.5
-        self.hovering = False  # whether the mouse is hovering over the parent NI (!)
-
-        # # Design.flow_theme_changed.connect(self.theme_changed)
-        # self.update_design()
-
-    def boundingRect(self):
-        return QRectF(QPointF(0, 0), self.geometry().size())
-
-    def setGeometry(self, rect):
-        self.prepareGeometryChange()
-        QGraphicsLayoutItem.setGeometry(self, rect)
-        self.setPos(rect.topLeft())
-
-    def sizeHint(self, which, constraint=...):
-        return QSizeF(self.width, self.height)
-
-    def paint(self, painter, option, widget=None):
-
-        self.parent_node_instance.session_design.flow_theme.node_inst_painter.paint_NI_title_label(
-            painter, option, self.hovering,
-            self.design_style(), self.title_str, self.parent_node_instance.color,
-            self.boundingRect()
-        )
-
-    def design_style(self):
-        return self.parent_node_instance.parent_node.design_style
-
-    def set_NI_hover_state(self, hovering: bool):
-        self.hovering = hovering
-        # self.update_design()
-        self.update()
-
-    # def theme_changed(self, new_theme):
-    #     """Gets called from the parent node instance because the order of the different updates matters.""" # not working yet
-    #     self.update_design()
-    #
-    # def update_design(self):
-    #
-    #     self.update()
-
-
-    # ANIMATION STUFF
-    def get_color(self):
-        return self.color
-
-    def set_color(self, val):
-        self.color = val
-        QGraphicsItem.update(self)
-
-    p_color = Property(QColor, get_color, set_color)
\ No newline at end of file
diff --git a/src/NodeItem.py b/src/NodeItem.py
index 7117a3f..f4e20e2 100644
--- a/src/NodeItem.py
+++ b/src/NodeItem.py
@@ -6,37 +6,19 @@
 from .NodeItemAction import NodeItemAction
 from .NodeItemAnimator import NodeItemAnimator
 from .NodeItemWidget import NodeItemWidget
-from .RC import FlowVPUpdateMode
-from .global_tools.Debugger import Debugger
-from .global_tools.MovementEnum import MovementEnum
-from .logging.Log import Log
-
 from .PortItem import InputPortItem, OutputPortItem
-from .retain import M
+from .global_tools.MovementEnum import MovementEnum
 
 
 class NodeItem(QGraphicsItem, QObject):
 
-    # # FIELDS
-    # init_inputs = []
-    # init_outputs = []
-    # title = ''
-    # type_ = ''
-    # description = ''
-    # main_widget_class = None
-    # main_widget_pos = 'below ports'
-    # input_widget_classes = {}
-    # style = 'extended'
-    # color = '#c69a15'
-
     def __init__(self, node, params):
-        # super(NodeItem, self).__init__()
         QGraphicsItem.__init__(self)
         QObject.__init__(self)
 
         self.node = node
-        flow, design, config = params
-        self.flow = flow
+        flow_view, design, config = params
+        self.flow_view = flow_view
         self.session_design = design
         self.movement_state = None
         self.movement_pos_from = None
@@ -45,10 +27,6 @@ def __init__(self, node, params):
         self.outputs = []
         self.color = QColor(self.node.color)  # manipulated by self.animator
 
-        # self.default_actions = {'remove': {'method': self.action_remove},
-        #                         'update shape': {'method': self.update_shape}}
-                                # 'console ref': {'method': self.set_console_scope}}  # for context menus
-        # self.special_actions = {}  # only gets written in custom NodeInstance-subclasses
         self.personal_logs = []
 
         # 'initializing' will be set to False below. It's needed for the ports setup, to prevent shape updating stuff
@@ -94,13 +72,11 @@ def __init__(self, node, params):
 
 
 
-    def initialized(self):
+    def initialize(self):
         """All ports and the main widget get finally created here."""
 
         # LOADING CONFIG
         if self.init_config is not None:
-            # self.setPos(config['position x'], config['position y'])
-            # self.setup_ports(self.init_config['inputs'], self.init_config['outputs'])
             if self.main_widget:
                 try:
                     self.main_widget.set_data(self.init_config['main widget data'])
@@ -108,17 +84,19 @@ def initialized(self):
                     print('Exception while setting data in', self.title, 'Node\'s main widget:', e,
                           ' (was this intended?)')
 
-            # self.special_actions = self.set_special_actions_data(self.init_config['special actions'])
-            # self.temp_state_data = self.init_config['state data']
-        # else:
-        #     self.setup_ports()
+        # catch up on ports
+        for i in self.node.inputs:
+            self.add_new_input(i, -1)
+
+        for o in self.node.outputs:
+            self.add_new_output(o, -1)
 
 
         self.initializing = False
 
         # No self.update_shape() here because for some reason, the bounding rect hasn't been initialized yet, so
         # self.update_shape() gets called when the item is being drawn the first time (see paint event in NI painter)
-        # TODO: change that ^ once there is a solution for this: https://forum.qt.io/topic/117179/force-qgraphicsitem-to-update-immediately-wait-for-update-event
+        # https://forum.qt.io/topic/117179/force-qgraphicsitem-to-update-immediately-wait-for-update-event
 
         self.update_design()  # load current design, update QGraphicsItem
 
@@ -129,11 +107,6 @@ def initialized(self):
     # --------------------------------------------------------------------------------------
     # UI STUFF ----------------------------------------
 
-    # def set_console_scope(self):
-    #     # extensive_dict = {}  # unlike self.__dict__, it also includes methods to call! :)
-    #     # for att in dir(self):
-    #     #     extensive_dict[att] = getattr(self, att)
-    #     MainConsole.main_console.add_obj_context(self)
 
     def node_updated(self):
         if self.session_design.animations_enabled:
@@ -141,6 +114,10 @@ def node_updated(self):
 
 
     def add_new_input(self, inp: NodeObjInput, pos: int):
+
+        # create item
+        inp.item = InputPortItem(inp.node, inp)
+
         if pos == -1:
             self.inputs.append(inp.item)
             self.widget.add_input_to_layout(inp.item)
@@ -170,6 +147,10 @@ def remove_input(self, inp: NodeObjInput):
             self.update()
 
     def add_new_output(self, out: NodeObjOutput, pos: int):
+
+        # create item
+        out.item = OutputPortItem(out.node, out)
+
         if pos == -1:
             self.outputs.append(out.item)
             self.widget.add_output_to_layout(out.item)
@@ -199,7 +180,7 @@ def remove_output(self, out: NodeObjOutput):
     def update_shape(self):
         self.widget.update_shape()
         self.update_conn_pos()
-        self.flow.viewport().update()
+        self.flow_view.viewport().update()
 
     def update_design(self):
         """Loads the shadow effect option and causes redraw with active theme."""
@@ -209,7 +190,7 @@ def update_design(self):
             self.shadow_effect.setXOffset(12)
             self.shadow_effect.setYOffset(12)
             self.shadow_effect.setBlurRadius(20)
-            self.shadow_effect.setColor(QColor('#2b2b2b'))
+            self.shadow_effect.setColor(self.session_design.flow_theme.node_item_shadow_color)
             self.setGraphicsEffect(self.shadow_effect)
         else:
             self.setGraphicsEffect(None)
@@ -248,11 +229,12 @@ def paint(self, painter, option, widget=None):
             self.update_shape()
             self.update_conn_pos()
 
-        self.session_design.flow_theme.node_item_painter.paint_NI(
-            design_style=self.node.style,
+        self.session_design.flow_theme.paint_NI(
+            node=self.node,
+            node_style=self.node.style,
             painter=painter,
             option=option,
-            c=self.color,
+            color=self.color,
             w=self.boundingRect().width(),
             h=self.boundingRect().height(),
             bounding_rect=self.boundingRect(),
@@ -262,7 +244,7 @@ def paint(self, painter, option, widget=None):
         self.painted_once = True
 
     def get_context_menu(self):
-        menu = QMenu(self.flow)
+        menu = QMenu(self.flow_view)
 
         for a in self.get_actions(self.node.get_extended_default_actions(), menu):  # menu needed for 'parent'
             if type(a) == NodeItemAction:
@@ -287,7 +269,7 @@ def itemChange(self, change, value):
 
         if change == QGraphicsItem.ItemPositionChange:
             if self.session_design.performance_mode == 'pretty':
-                self.flow.viewport().update()
+                self.flow_view.viewport().update()
             if self.movement_state == MovementEnum.mouse_clicked:
                 self.movement_state = MovementEnum.position_changed
 
@@ -299,10 +281,26 @@ def update_conn_pos(self):
         """Updates the global positions of connections at outputs"""
         for o in self.node.outputs:
             for c in o.connections:
-                c.item.recompute()
+                # c.item.recompute()
+
+                if c not in self.flow_view.connection_items:
+                    # it can happen that the connection item hasn't been
+                    # created yet
+                    continue
+
+                item = self.flow_view.connection_items[c]
+                item.recompute()
         for i in self.node.inputs:
             for c in i.connections:
-                c.item.recompute()
+                # c.item.recompute()
+
+                if c not in self.flow_view.connection_items:
+                    # it can happen that the connection item hasn't been
+                    # created yet
+                    continue
+
+                item = self.flow_view.connection_items[c]
+                item.recompute()
 
     def hoverEnterEvent(self, event):
         self.widget.title_label.set_NI_hover_state(hovering=True)
@@ -322,7 +320,7 @@ def 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.flow_view.selected_components_moved(self.pos() - self.movement_pos_from)
         self.movement_state = None
         return QGraphicsItem.mouseReleaseEvent(self, event)
 
@@ -340,9 +338,9 @@ def get_actions(self, actions_dict, menu):
                 except KeyError:
                     pass
                 action = NodeItemAction(text=k, method=method, menu=menu, data=data)
-                if self.flow.session.threading_enabled:
-                    action.triggered_with_data__thread.connect(self.flow.worker_thread.interface.trigger_node_action)
-                    action.triggered_without_data__thread.connect(self.flow.worker_thread.interface.trigger_node_action)
+                if self.flow_view.session.threaded:
+                    action.triggered_with_data__thread.connect(self.flow_view.thread_interface.trigger_node_action)
+                    action.triggered_without_data__thread.connect(self.flow_view.thread_interface.trigger_node_action)
                 else:
                     action.triggered_with_data.connect(method)  # see NodeItemAction for explanation
                     action.triggered_without_data.connect(method)  # see NodeItemAction for explanation
@@ -357,136 +355,26 @@ def get_actions(self, actions_dict, menu):
 
         return actions
 
-    def config_data(self):
-        return {'pos x': self.pos().x(), 'pos y': self.pos().y()}
-
-    # def get_special_actions_data(self, actions):
-    #     cleaned_actions = actions.copy()
-    #     for key in cleaned_actions:
-    #         v = cleaned_actions[key]
-    #         if type(v) == M:  # callable(v):
-    #             cleaned_actions[key] = v.method_name
-    #         elif callable(v):
-    #             cleaned_actions[key] = v.__name__
-    #         elif type(v) == dict:
-    #             cleaned_actions[key] = self.get_special_actions_data(v)
-    #         else:
-    #             cleaned_actions[key] = v
-    #     return cleaned_actions
-
-    # def set_special_actions_data(self, actions_data):
-    #     actions = {}
-    #     for key in actions_data:
-    #         if type(actions_data[key]) != dict:
-    #             if key == 'method':
-    #                 try:
-    #                     actions['method'] = M(getattr(self, actions_data[key]))
-    #                 except AttributeError:  # outdated method referenced
-    #                     pass
-    #             elif key == 'data':
-    #                 actions['data'] = actions_data[key]
-    #         else:
-    #             actions[key] = self.set_special_actions_data(actions_data[key])
-    #     return actions
-
-    # PORTS
-    # def setup_ports(self, inputs_config=None, outputs_config=None):
-    #     if not inputs_config and not outputs_config:
-    #         for i in range(len(self.init_inputs)):
-    #             inp = self.init_inputs[i]
-    #             self.create_new_input(inp.type_, inp.label,
-    #                                   widget_name=self.init_inputs[i].widget_name,
-    #                                   widget_pos =self.init_inputs[i].widget_pos)
-    #
-    #         for o in range(len(self.init_outputs)):
-    #             out = self.init_outputs[o]
-    #             self.create_new_output(out.type_, out.label)
-    #     else:  # when loading saved NIs, the port instances might not be synchronised to the parent's ports anymore
-    #         for inp in inputs_config:
-    #             has_widget = inp['has widget']
-    #
-    #             self.create_new_input(inp['type'], inp['label'],
-    #                                   widget_name=inp['widget name'] if has_widget else None,
-    #                                   widget_pos =inp['widget position'] if has_widget else None,
-    #                                   config=inp['widget data'] if has_widget else None)
-    #
-    #         for out in outputs_config:
-    #             self.create_new_output(out['type'], out['label'])
-
-    # def add_input_to_scene(self, i):
-    #     self.flow.scene().addItem(i.pin)
-    #     self.flow.scene().addItem(i.label)
-    #     if i.widget:
-    #         self.flow.scene().addItem(i.proxy)
-    #
-    # def add_output_to_scene(self, o):
-    #     self.flow.scene().addItem(o.pin)
-    #     self.flow.scene().addItem(o.label)
-
-    # # GENERAL
-    # def about_to_remove_from_scene(self):
-    #     """Called from Flow when the NI gets removed from the scene
-    #     to stop all running threads and disable personal logs."""
-    #
-    #     if self.main_widget:
-    #         self.main_widget.remove_event()
-    #     self.remove_event()
-    #
-    #     self.disable_logs()
-    #
-    # def is_active(self):
-    #     for i in self.inputs:
-    #         if i.type_ == 'exec':
-    #             return True
-    #     for o in self.outputs:
-    #         if o.type_ == 'exec':
-    #             return True
-    #     return False
-    #
-    # def has_main_widget(self):
-    #     """Might be used later in CodePreview_Widget to enable not only showing the NI's class but also it's
-    #     main_widget's class."""
-    #     return self.main_widget is not None
-
-    # def get_input_widgets(self):
-    #     """Might be used later in CodePreview_Widget to enable not only showing the NI's class but its input widgets'
-    #     classes."""
-    #     input_widgets = []
-    #     for i in range(len(self.inputs)):
-    #         inp = self.inputs[i]
-    #         if inp.widget is not None:
-    #             input_widgets.append({i: inp.widget})
-    #     return input_widgets
-
-    # def config_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.title,
-    #                           'parent node type': self.type_,
-    #                           # 'parent node package': self.parent_node.package,
-    #                           'parent node description': self.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.config_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.config_data()
-    #         node_instance_outputs_list.append(output_dict)
-    #     node_instance_dict['outputs'] = node_instance_outputs_list
-    #
-    #     return node_instance_dict
\ No newline at end of file
+    def complete_config(self, node_config):
+        # add input widgets config
+        for i in range(len(node_config['inputs'])):
+            input_cfg = node_config['inputs'][i]
+            inp_item = self.inputs[i]
+
+            if inp_item.port.type_ == 'data':
+                if inp_item.widget:
+                    input_cfg['has widget'] = True
+                    input_cfg['widget name'] = inp_item.port.widget_name
+                    input_cfg['widget data'] = inp_item.widget.get_data()
+                    input_cfg['widget position'] = inp_item.port.widget_pos
+                else:
+                    input_cfg['has widget'] = False
+                node_config['inputs'][i] = input_cfg
+
+        # add item properties
+        node_config['pos x'] = self.pos().x()
+        node_config['pos y'] = self.pos().y()
+        if self.main_widget:
+            node_config['main widget data'] = self.main_widget.get_data()
+
+        return node_config
diff --git a/src/NodeItemPainter.py b/src/NodeItemPainter.py
deleted file mode 100644
index 2841033..0000000
--- a/src/NodeItemPainter.py
+++ /dev/null
@@ -1,921 +0,0 @@
-from PySide2.QtCore import Qt, QPointF, QRectF
-from PySide2.QtGui import QColor, QPainter, QBrush, QRadialGradient, QLinearGradient, QPen, QPainterPath, QFont
-from PySide2.QtWidgets import QStyle
-
-from .global_tools.math import pythagoras
-
-
-class NIPainter:
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        pass
-
-    def paint_NI_title_label_default(painter, design_style, title_str, c, pen_w, font, bounding_rect):
-        pen = QPen(c)
-        pen.setWidth(pen_w)
-
-        painter.setPen(pen)
-        painter.setFont(font)
-
-        text_rect = bounding_rect
-        text_rect.setTop(text_rect.top()-7)
-
-        if design_style == 'extended':
-            painter.drawText(text_rect, Qt.AlignLeft | Qt.AlignVCenter, title_str)
-        elif design_style == 'small':
-            painter.drawText(text_rect, Qt.AlignTop | Qt.AlignHCenter, title_str)
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        pass
-
-    def paint_PI_label_default(painter, label_str, c, font, bounding_rect):
-        painter.setBrush(Qt.NoBrush)
-        pen = QPen(c)
-        painter.setPen(pen)
-        painter.setFont(font)
-        painter.drawText(bounding_rect, Qt.AlignCenter, label_str)
-
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-        pass
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-        pass
-
-    def get_header_rect(w, h, title_rect):
-        """
-        :param w: width
-        :param h: height
-        """
-
-        header_height = 1.4 * title_rect.height()  # 35 * (self.parent_node.title.count('\n')+1)
-
-        header_rect = QRectF()
-        header_rect.setTopLeft(QPointF(-w / 2, -h / 2))
-        header_rect.setWidth(w)
-        header_rect.setHeight(header_height)
-        return header_rect
-
-    def interpolate_color(c1, c2, val):
-        r1 = c1.red()
-        g1 = c1.green()
-        b1 = c2.blue()
-        a1 = c1.alpha()
-
-        r2 = c2.red()
-        g2 = c2.green()
-        b2 = c2.blue()
-        a2 = c2.alpha()
-
-        r = (r2 - r1) * val + r1
-        g = (g2 - g1) * val + g1
-        b = (b2 - b1) * val + b1
-        a = (a2 - a1) * val + a1
-
-        return QColor(r, g, b, a)
-
-
-class NIPainter_DarkStd(NIPainter):
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        if design_style == 'extended':
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, QColor(30, 43, 48) if not hovering else node_color.lighter(),
-                2 if hovering else 1.5,
-                QFont('Poppins', 15),
-                bounding_rect
-            )
-        else:
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, QColor(30, 43, 48) if not hovering else node_color.lighter(),
-                1.5,
-                QFont('K2D', 20, QFont.Bold, True),
-                bounding_rect
-            )
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        c = QColor('#FFFFFF')
-        NIPainter.paint_PI_label_default(painter, label_str, c, QFont("Source Code Pro", 10, QFont.Bold), bounding_rect)
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-
-        color = QColor('#2E688C') if exec_type == 'data' else QColor('#3880ad')
-        if option.state & QStyle.State_MouseOver:
-            color = color.lighter()
-
-        brush = QBrush(QColor(color))
-        painter.setBrush(brush)
-        painter.setPen(Qt.NoPen)
-
-        painter.drawEllipse(QRectF(padding, padding, w, h))
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-
-        painter.setRenderHint(QPainter.Antialiasing)
-
-        if design_style == 'extended':
-            NIPainter_DarkStd.draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect)
-        elif design_style == 'small':
-            NIPainter_DarkStd.draw_NI_minimalistic(painter, c, w, h, bounding_rect)
-
-    def draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect):
-        """
-        :param painter: painter from paint event
-        :param c: NodeItem's theme color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeItem's bounding rect
-        :param title_rect: NI's title label's bounding rect
-        """
-
-        # main rect
-        body_gradient = QRadialGradient(bounding_rect.topLeft(), pythagoras(h, w))
-        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(bounding_rect, 12, 12)
-
-        header_gradient = QLinearGradient(NIPainter_DarkStd.get_header_rect(w, h, title_rect).topRight(),
-                                          NIPainter_DarkStd.get_header_rect(w, h, title_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(NIPainter_DarkStd.get_header_rect(w, h, title_rect), 12, 12)
-
-    def draw_NI_minimalistic(painter, c, w, h, bounding_rect):
-        """
-        :param painter: painter from paint event
-        :param c: color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeItem's bounding rect
-        """
-
-        path = QPainterPath()
-        path.moveTo(-w / 2, 0)
-
-        path.cubicTo(-w / 2, -h / 2,
-                     -w / 2, -h / 2,
-                     0, -h / 2)
-        path.cubicTo(+w / 2, -h / 2,
-                     +w / 2, -h / 2,
-                     +w / 2, 0)
-        path.cubicTo(+w / 2, +h / 2,
-                     +w / 2, +h / 2,
-                     0, +h / 2)
-        path.cubicTo(-w / 2, +h / 2,
-                     -w / 2, +h / 2,
-                     -w / 2, 0)
-        path.closeSubpath()
-
-        body_gradient = QLinearGradient(bounding_rect.bottomLeft(),
-                                        bounding_rect.topRight())
-        body_gradient.setColorAt(0, QColor(c.red(), c.green(), c.blue(), 150))
-        body_gradient.setColorAt(1, QColor(c.red(), c.green(), c.blue(), 80))
-
-        painter.setBrush(body_gradient)
-        painter.setPen(QPen(QColor(30, 43, 48)))
-
-        painter.drawPath(path)
-
-
-class NIPainter_DarkTron(NIPainter):
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        if design_style == 'extended':
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color if not hovering else node_color.lighter(),
-                2,
-                QFont('Poppins', 15),
-                bounding_rect
-            )
-        else:
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color,
-                2,
-                QFont('K2D', 20, QFont.Bold, True),
-                bounding_rect
-            )
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        if exec_type == 'exec':
-            c = QColor('#FFFFFF')
-        else:
-            c = node_color
-        NIPainter.paint_PI_label_default(painter, label_str, c, QFont("Source Code Pro", 10, QFont.Bold), bounding_rect)
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-
-        color = QColor('#FFFFFF') if exec_type == 'exec' else node_color
-        pen = QPen(color)
-        pen.setWidth(2)
-        painter.setPen(pen)
-        if connected or \
-                option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-            r = node_color.red()
-            g = node_color.green()
-            b = node_color.blue()
-            brush = QBrush(QColor(r, g, b, 100))
-            painter.setBrush(brush)
-        else:
-            painter.setBrush(Qt.NoBrush)
-
-        painter.drawEllipse(QRectF(padding, padding, w, h))
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-
-        painter.setRenderHint(QPainter.Antialiasing)
-
-        if design_style == 'extended':
-            NIPainter_DarkTron.draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect)
-        elif design_style == 'small':
-            if option.state & QStyle.State_MouseOver:  # use special dark background color when mouse hovers
-                NIPainter_DarkTron.draw_NI_minimalistic(painter, c, w, h, background_color=c.darker())
-            else:
-                NIPainter_DarkTron.draw_NI_minimalistic(painter, c, w, h)
-
-    def draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect):
-        """
-        :param painter: painter from paint event
-        :param c: NodeItem's theme color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeItem's bounding rect
-        :param title_rect: NI's title label's bounding rect
-        """
-
-        background_color = QColor('#212224')
-        painter.setBrush(background_color)
-        pen = QPen(c)
-        pen.setWidth(2)
-        painter.setPen(pen)
-        body_path = NIPainter_DarkTron.get_extended_body_path(w, h)
-        painter.drawPath(body_path)
-
-        header_gradient = QLinearGradient(NIPainter_DarkTron.get_header_rect(w, h, title_rect).topRight(),
-                                          NIPainter_DarkTron.get_header_rect(w, h, title_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 = NIPainter_DarkTron.get_extended_header_path(w, h, title_rect)
-        painter.drawPath(header_path)
-
-    def get_extended_body_path(w, h):
-        """
-        :param c_s: corner size/corner radius
-        :param w: width
-        :param h: height
-        """
-        c_s = 10
-
-        path = QPainterPath()
-        path.moveTo(+w / 2, -h / 2 + c_s)
-        path.lineTo(+w / 2 - c_s, -h / 2)
-        path.lineTo(-w / 2 + c_s, -h / 2)
-        path.lineTo(-w / 2, -h / 2 + c_s)
-        path.lineTo(-w / 2, +h / 2 - c_s)
-        path.lineTo(-w / 2 + c_s, +h / 2)
-        path.lineTo(+w / 2 - c_s, +h / 2)
-        path.lineTo(+w / 2, +h / 2 - c_s)
-        path.closeSubpath()
-        return path
-
-    def get_extended_header_path(w, h, title_rect):
-        """
-        :param w: width
-        :param h: height
-        :param title_rect: NI's title label's bounding rect
-        """
-        c_s = 10
-
-        # header_height = 35 * (NIPainter_DarkTron.ni.parent_node.title.count('\n') + 1)
-        header_height = NIPainter_DarkTron.get_header_rect(w, h, title_rect).height()
-        header_bottom = -h / 2 + header_height
-        path = QPainterPath()
-        path.moveTo(+w / 2, -h / 2 + c_s)
-        path.lineTo(+w / 2 - c_s, -h / 2)
-        path.lineTo(-w / 2 + c_s, -h / 2)
-        path.lineTo(-w / 2, -h / 2 + c_s)
-        path.lineTo(-w / 2, header_bottom - c_s)
-        path.lineTo(-w / 2 + c_s, header_bottom)
-        path.lineTo(+w / 2 - c_s, header_bottom)
-        path.lineTo(+w / 2, header_bottom - c_s)
-        path.closeSubpath()
-        return path
-
-    def draw_NI_minimalistic(painter, c, w, h, background_color=QColor('#36383B')):
-        """
-        :param painter: painter from paint event
-        :param c_s: corner size/corner radius
-        :param c: color
-        :param w: width
-        :param h: height
-        :param background_color: (default) background color
-        """
-
-        c_s = 10
-
-        path = QPainterPath()
-        path.moveTo(-w / 2, 0)
-
-        path.lineTo(-w / 2 + c_s / 2, -h / 2 + c_s / 2)
-        path.lineTo(0, -h / 2)
-        path.lineTo(+w / 2 - c_s / 2, -h / 2 + c_s / 2)
-        path.lineTo(+w / 2, 0)
-        path.lineTo(+w / 2 - c_s / 2, +h / 2 - c_s / 2)
-        path.lineTo(0, +h / 2)
-        path.lineTo(-w / 2 + c_s / 2, +h / 2 - c_s / 2)
-        path.closeSubpath()
-
-        painter.setBrush(background_color)
-        pen = QPen(c)
-        pen.setWidth(2)
-        painter.setPen(pen)
-
-        painter.drawPath(path)
-
-
-class NIPainter_Ghostly(NIPainter):
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        if design_style == 'extended':
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color if not hovering else node_color.lighter(),
-                2,
-                QFont('Poppins', 15),
-                bounding_rect
-            )
-        else:
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color,
-                2,
-                QFont('K2D', 20, QFont.Bold, True),
-                bounding_rect
-            )
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        if exec_type == 'exec':
-            c = QColor('#FFFFFF')
-        else:
-            c = node_color
-        NIPainter.paint_PI_label_default(painter, label_str, c, QFont("Source Code Pro", 10, QFont.Bold), bounding_rect)
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-
-        color = QColor('#FFFFFF') if exec_type == 'exec' else node_color
-
-        if exec_type == 'exec':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                brush = QBrush(QColor(255, 255, 255, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-        elif exec_type == 'data':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                r = node_color.red()
-                g = node_color.green()
-                b = node_color.blue()
-                brush = QBrush(QColor(r, g, b, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-
-        pen = QPen(color)
-        pen.setWidth(1)
-        painter.setPen(pen)
-        painter.drawEllipse(QRectF(padding+w/4, padding+h/4, w/2, h/2))
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-
-        painter.setRenderHint(QPainter.Antialiasing)
-
-        if design_style == 'extended':
-            NIPainter_Ghostly.draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect)
-        elif design_style == 'small':
-            if option.state & QStyle.State_MouseOver:  # use special dark background color when mouse hovers
-                NIPainter_Ghostly.draw_NI_minimalistic(painter, c, w, h, background_color=c.darker())
-            else:
-                NIPainter_Ghostly.draw_NI_minimalistic(painter, c, w, h)
-
-    def draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect):
-        """
-        :param painter: painter from paint event
-        :param c: NodeItem's theme color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeItem's bounding rect
-        :param title_rect: NI's title label's bounding rect
-        """
-
-        background_color = QColor(31, 31, 36, 150)
-
-        body_gradient = QRadialGradient(QPointF(bounding_rect.topLeft().x() + w,
-                                                bounding_rect.topLeft().y() - h),
-                                        pythagoras(2 * h, 2 * w))
-        body_gradient.setColorAt(0, QColor(c.red() / 10 + 100, c.green() / 10 + 100, c.blue() / 10 + 100, 100))
-        body_gradient.setColorAt(0.7, background_color)
-        body_gradient.setColorAt(1, background_color)
-
-        painter.setBrush(body_gradient)
-        painter.setBrush(QColor(28, 28, 28, 170))
-        pen = QPen(c.darker())
-        pen.setWidth(1)
-        painter.setPen(pen)
-        body_path = NIPainter_Ghostly.get_extended_body_path(5, w, h)
-        painter.drawPath(body_path)
-
-    def get_extended_body_path(c_s, w, h):
-        """
-        Very similar to 'extended tron'
-        :param c_s: corner size/corner radius
-        :param w: width
-        :param h: height
-        """
-
-        path = QPainterPath()
-        path.moveTo(+w / 2, -h / 2 + c_s)
-        path.lineTo(+w / 2 - c_s, -h / 2)
-        path.lineTo(-w / 2 + c_s, -h / 2)
-        path.lineTo(-w / 2, -h / 2 + c_s)
-        path.lineTo(-w / 2, +h / 2 - c_s)
-        path.lineTo(-w / 2 + c_s, +h / 2)
-        path.lineTo(+w / 2 - c_s, +h / 2)
-        path.lineTo(+w / 2, +h / 2 - c_s)
-        path.closeSubpath()
-        return path
-
-    def draw_NI_minimalistic(painter, c, w, h, background_color=QColor(30, 30, 30, 170)):
-        """
-        :param painter: painter from paint event
-        :param c_s: corner size/corner radius
-        :param c: color
-        :param w: width
-        :param h: height
-        :param background_color: std background color
-        """
-        c_s = 10
-        path = NIPainter_Ghostly.get_extended_body_path(c_s, w, h)  # equals the minimalistic in this case
-
-        painter.setBrush(background_color)
-        # pen = QPen(QColor('#333333'))  # QPen(c)
-        # pen.setWidth(1)
-        # painter.setPen(pen)
-        painter.setPen(Qt.NoPen)
-
-        painter.drawPath(path)
-
-
-class NIPainter_Blender(NIPainter):
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        if design_style == 'extended':
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, QColor('#FFFFFF'),
-                2,
-                QFont('Poppins', 15),
-                bounding_rect
-            )
-        else:
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color,
-                2,
-                QFont('K2D', 20, QFont.Bold, True),
-                bounding_rect
-            )
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        if exec_type == 'exec':
-            c = QColor('#FFFFFF')
-        else:
-            c = node_color
-        NIPainter.paint_PI_label_default(painter, label_str, c, QFont("Source Code Pro", 10, QFont.Bold), bounding_rect)
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-
-        color = QColor('#FFFFFF') if exec_type == 'exec' else node_color
-
-        if exec_type == 'exec':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                brush = QBrush(QColor(255, 255, 255, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-        elif exec_type == 'data':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                r = node_color.red()
-                g = node_color.green()
-                b = node_color.blue()
-                brush = QBrush(QColor(r, g, b, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-
-        pen = QPen(color)
-        pen.setWidth(1)
-        painter.setPen(pen)
-        painter.drawEllipse(QRectF(padding + w / 4, padding + h / 4, w / 2, h / 2))
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-
-        painter.setRenderHint(QPainter.Antialiasing)
-
-        if design_style == 'extended':
-            NIPainter_Blender.draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect)
-        elif design_style == 'small':
-            NIPainter_Blender.draw_NI_minimalistic(painter, c, w, h, bounding_rect)
-
-    def draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect):
-        """
-        :param painter: painter from paint event
-        :param c: NodeItem's theme color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeItem's bounding rect
-        :param title_rect: NI's title label's bounding rect
-        """
-
-        background_color = QColor(100, 100, 100, 150)
-        header_color = QColor(c.red(), c.green(), c.blue(), 180)
-
-        rel_header_height = NIPainter.get_header_rect(w, h, title_rect).height()/h
-        gradient = QLinearGradient(bounding_rect.topLeft(), bounding_rect.bottomLeft())
-        gradient.setColorAt(0, header_color)
-        gradient.setColorAt(rel_header_height, header_color)
-        gradient.setColorAt(rel_header_height+0.0001, background_color)
-        gradient.setColorAt(1, background_color)
-
-        painter.setBrush(gradient)
-        painter.setPen(QPen(c.darker()))
-        painter.drawRoundedRect(bounding_rect, 7, 7)
-
-    def draw_NI_minimalistic(painter, c, w, h, bounding_rect, background_color=QColor(30, 30, 30, 150)):
-        """
-        :param painter: painter from paint event
-        :param c_s: corner size/corner radius
-        :param c: color
-        :param w: width
-        :param h: height
-        :param background_color: std background color
-        """
-        c_s = 15
-        painter.setBrush(NIPainter.interpolate_color(c, background_color, 0.97))
-        painter.setPen(QPen(c))
-        painter.drawRoundedRect(bounding_rect, c_s, c_s)
-
-
-class NIPainter_Easy(NIPainter):
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        if design_style == 'extended':
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, QColor('#312b29'),
-                2,
-                QFont('ASAP', 13, QFont.Bold),
-                bounding_rect
-            )
-        else:
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color,
-                2,
-                QFont('Poppins', 15, QFont.Thin),
-                bounding_rect
-            )
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        c = None
-        if not connected:
-            c = QColor('#53585c')
-        else:
-            if exec_type == 'exec':
-                c = QColor('#cccccc')
-            else:
-                c = node_color
-        NIPainter.paint_PI_label_default(painter, label_str, c, QFont("Courier New", 10, QFont.Bold), bounding_rect)
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-        color = None
-        if not connected:
-            color = QColor('#53585c')
-        else:
-            if exec_type == 'exec':
-                color = QColor('#dddddd')
-            else:
-                color = node_color
-
-        if exec_type == 'exec':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                brush = QBrush(QColor(255, 255, 255, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-        elif exec_type == 'data':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                r = node_color.red()
-                g = node_color.green()
-                b = node_color.blue()
-                brush = QBrush(QColor(r, g, b, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-
-        brush = QBrush(QColor(color))
-        painter.setBrush(brush)
-        painter.setPen(Qt.NoPen)
-
-        painter.drawEllipse(QRectF(padding+w/8, padding+h/8, 3*w/4, 3*h/4))
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-
-        painter.setRenderHint(QPainter.Antialiasing)
-
-        if design_style == 'extended':
-            NIPainter_Easy.draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect)
-        elif design_style == 'small':
-            NIPainter_Easy.draw_NI_minimalistic(painter, c, w, h, bounding_rect)
-
-    def draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect):
-        """
-        :param painter: painter from paint event
-        :param c: NodeItem's theme color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeItem's bounding rect
-        :param title_rect: NI's title label's bounding rect
-        """
-
-        background_color = QColor('#2b2e33')
-        header_color = c
-
-        rel_header_height = NIPainter.get_header_rect(w, h, title_rect).height()/h
-        gradient = QLinearGradient(bounding_rect.topLeft(), bounding_rect.bottomLeft())
-        gradient.setColorAt(0, header_color)
-        gradient.setColorAt(rel_header_height, header_color)
-        gradient.setColorAt(rel_header_height+0.0001, background_color)
-        gradient.setColorAt(1, background_color)
-
-        painter.setBrush(gradient)
-        painter.setPen(Qt.NoPen) #QPen(c.darker()))
-        painter.drawRoundedRect(bounding_rect, 9, 9)
-
-    def draw_NI_minimalistic(painter, c, w, h, bounding_rect, background_color=QColor('#2b2e33')):
-        """
-        :param painter: painter from paint event
-        :param c_s: corner size/corner radius
-        :param c: color
-        :param w: width
-        :param h: height
-        :param background_color: std background color
-        """
-        c_s = 10
-        painter.setBrush(NIPainter.interpolate_color(c, background_color, 0.97))
-        painter.setPen(Qt.NoPen)
-        painter.drawRoundedRect(bounding_rect, c_s, c_s)
-
-
-class NIPainter_Peasy(NIPainter):
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        if design_style == 'extended':
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, QColor('#312b29'),
-                2,
-                QFont('ASAP', 13, QFont.Bold),
-                bounding_rect
-            )
-        else:
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color,
-                2,
-                QFont('Poppins', 15, QFont.Thin),
-                bounding_rect
-            )
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        c = None
-        if not connected:
-            c = QColor('#53585c')
-        else:
-            if exec_type == 'exec':
-                c = QColor('#cccccc')
-            else:
-                c = node_color
-        NIPainter.paint_PI_label_default(painter, label_str, c, QFont("Courier New", 10, QFont.Bold), bounding_rect)
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-        color = None
-        if not connected:
-            color = QColor('#53585c')
-        else:
-            if exec_type == 'exec':
-                color = QColor('#dddddd')
-            else:
-                color = node_color
-
-        if exec_type == 'exec':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                brush = QBrush(QColor(255, 255, 255, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-        elif exec_type == 'data':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                r = node_color.red()
-                g = node_color.green()
-                b = node_color.blue()
-                brush = QBrush(QColor(r, g, b, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-
-        brush = QBrush(QColor(color))
-        painter.setBrush(brush)
-        painter.setPen(Qt.NoPen)
-
-        painter.drawEllipse(QRectF(padding+w/8, padding+h/8, 3*w/4, 3*h/4))
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-
-        painter.setRenderHint(QPainter.Antialiasing)
-
-        if design_style == 'extended':
-            NIPainter_Peasy.draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect)
-        elif design_style == 'small':
-            NIPainter_Peasy.draw_NI_minimalistic(painter, c, w, h, bounding_rect)
-
-    def draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect):
-        """
-        :param painter: painter from paint event
-        :param c: NodeItem's theme color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeItem's bounding rect
-        :param title_rect: NI's title label's bounding rect
-        """
-
-        background_color = QColor('#212429')
-        header_color = c
-
-        rel_header_height = NIPainter.get_header_rect(w, h, title_rect).height()/h
-        gradient = QLinearGradient(bounding_rect.topLeft(), bounding_rect.bottomLeft())
-        gradient.setColorAt(0, header_color)
-        gradient.setColorAt(rel_header_height, header_color)
-        gradient.setColorAt(rel_header_height+0.0001, background_color)
-        gradient.setColorAt(1, background_color)
-
-        painter.setBrush(gradient)
-        painter.setPen(Qt.NoPen) #QPen(c.darker()))
-        painter.drawRoundedRect(bounding_rect, 9, 9)
-
-    def draw_NI_minimalistic(painter, c, w, h, bounding_rect, background_color=QColor('#212429')):
-        """
-        :param painter: painter from paint event
-        :param c_s: corner size/corner radius
-        :param c: color
-        :param w: width
-        :param h: height
-        :param background_color: std background color
-        """
-        c_s = 10
-        painter.setBrush(NIPainter.interpolate_color(c, background_color, 0.97))
-        painter.setPen(Qt.NoPen)
-        painter.drawRoundedRect(bounding_rect, c_s, c_s)
-
-
-class NIPainter_Ueli(NIPainter):
-
-    def paint_NI_title_label(painter, option, hovering, design_style, title_str, node_color, bounding_rect):
-        if design_style == 'extended':
-            painter.setPen(QPen(QColor(node_color.name())))
-            painter.setFont(QFont('Poppins', 13))
-            painter.drawText(bounding_rect, Qt.AlignLeft | Qt.AlignVCenter, title_str)
-        else:
-            NIPainter.paint_NI_title_label_default(
-                painter, design_style, title_str, node_color,
-                2,
-                QFont('Poppins', 15, QFont.Thin),
-                bounding_rect
-            )
-
-    def paint_PI_label(painter, option, exec_type, connected, label_str, node_color, bounding_rect):
-        c = None
-        if not connected:
-            c = QColor('#53585c')
-        else:
-            if exec_type == 'exec':
-                c = QColor('#cccccc')
-            else:
-                c = node_color
-        NIPainter.paint_PI_label_default(painter, label_str, c, QFont("Courier New", 10, QFont.Bold), bounding_rect)
-
-    def paint_PI(painter, option, node_color, exec_type, connected, padding, w, h):
-        color = None
-        if not connected:
-            color = QColor('#53585c')
-        else:
-            if exec_type == 'exec':
-                color = QColor('#dddddd')
-            else:
-                color = node_color
-
-        if exec_type == 'exec':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                brush = QBrush(QColor(255, 255, 255, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-        elif exec_type == 'data':
-            if connected or \
-                    option.state & QStyle.State_MouseOver:  # also fill when mouse hovers
-                r = node_color.red()
-                g = node_color.green()
-                b = node_color.blue()
-                brush = QBrush(QColor(r, g, b, 100))
-                painter.setBrush(brush)
-            else:
-                painter.setBrush(Qt.NoBrush)
-
-        brush = QBrush(QColor(color))
-        painter.setBrush(brush)
-        painter.setPen(Qt.NoPen)
-
-        painter.drawEllipse(QRectF(padding+w/8, padding+h/8, 3*w/4, 3*h/4))
-
-    def paint_NI(design_style,
-                 painter, option,
-                 c: QColor, w: int, h: int, bounding_rect, title_rect):
-
-        painter.setRenderHint(QPainter.Antialiasing)
-
-        if design_style == 'extended':
-            NIPainter_Ueli.draw_NI_extended_background(painter, c, w, h, bounding_rect, title_rect)
-        elif design_style == 'small':
-            NIPainter_Ueli.draw_NI_minimalistic(painter, c, w, h, bounding_rect)
-
-    def draw_NI_extended_background(painter, c, w, h, bounding_rect: QRectF, title_rect):
-        """
-        :param painter: painter from paint event
-        :param c: NodeItem's theme color
-        :param w: width
-        :param h: height
-        :param bounding_rect: NodeItem's bounding rect
-        :param title_rect: NI's title label's bounding rect
-        """
-
-        background_color = QColor('#212429')
-        header_color = c
-
-        header_height = NIPainter.get_header_rect(w, h, title_rect).height()
-        rel_header_height = header_height/h
-        gradient = QLinearGradient(bounding_rect.topLeft(), bounding_rect.bottomLeft())
-        gradient.setColorAt(0, header_color)
-        gradient.setColorAt(rel_header_height, header_color)
-        gradient.setColorAt(rel_header_height+0.0001, background_color)
-        gradient.setColorAt(1, background_color)
-
-        painter.setBrush(QBrush(background_color))
-        painter.setPen(Qt.NoPen)  # QPen(c.darker()))
-        painter.drawRoundedRect(QRectF(
-            QPointF(bounding_rect.left(), bounding_rect.top()+header_height),
-            bounding_rect.bottomRight()
-        ), 6, 6)
-
-    def draw_NI_minimalistic(painter, c, w, h, bounding_rect, background_color=QColor('#212429')):
-        """
-        :param painter: painter from paint event
-        :param c_s: corner size/corner radius
-        :param c: color
-        :param w: width
-        :param h: height
-        :param background_color: std background color
-        """
-        c_s = 10
-        painter.setBrush(NIPainter.interpolate_color(c, background_color, 0.97))
-        painter.setPen(Qt.NoPen)
-        painter.drawRoundedRect(bounding_rect, c_s, c_s)
diff --git a/src/NodeItemWidget.py b/src/NodeItemWidget.py
index 5c9d9eb..1208ebd 100644
--- a/src/NodeItemWidget.py
+++ b/src/NodeItemWidget.py
@@ -1,7 +1,7 @@
 from PySide2.QtCore import QPointF, QRectF, Qt
 from PySide2.QtWidgets import QGraphicsWidget, QGraphicsLinearLayout
 
-from .FlowProxyWidget import FlowProxyWidget
+from .FlowViewProxyWidget import FlowViewProxyWidget
 # from .Node import Node
 from .NodeItem_TitleLabel import TitleLabel
 from .PortItem import InputPortItem, OutputPortItem
@@ -13,13 +13,16 @@ def __init__(self, node, node_item):
 
         self.node = node
         self.node_item = node_item
-        self.flow = self.node_item.flow
+        self.flow_view = self.node_item.flow_view
 
         self.title_label = TitleLabel(node, node_item)
+        self.main_widget_proxy: FlowViewProxyWidget = None
+        if self.node_item.main_widget:
+            self.main_widget_proxy = FlowViewProxyWidget(self.flow_view)
+            self.main_widget_proxy.setWidget(self.node_item.main_widget)
         self.body_layout: QGraphicsLinearLayout = None
         self.inputs_layout: QGraphicsLinearLayout = None
         self.outputs_layout: QGraphicsLinearLayout = None
-        self.main_widget_proxy: FlowProxyWidget = None
         self.setLayout(self.setup_layout())
 
     def setup_layout(self) -> QGraphicsLinearLayout:
@@ -52,21 +55,24 @@ def setup_layout(self) -> QGraphicsLinearLayout:
         self.body_layout.addItem(self.outputs_layout)
         self.body_layout.setAlignment(self.outputs_layout, Qt.AlignVCenter | Qt.AlignRight)
 
-        if self.node_item.main_widget:
-            self.main_widget_proxy = FlowProxyWidget(self.flow)
-            self.main_widget_proxy.setWidget(self.node_item.main_widget)
-
-            if self.node.main_widget_pos == 'between ports':
-                self.body_layout.insertItem(1, self.main_widget_proxy)
-                self.body_layout.insertStretch(2)
-                layout.addItem(self.body_layout)
 
-            elif self.node.main_widget_pos == 'below ports':
-                layout.addItem(self.body_layout)
-                layout.addItem(self.main_widget_proxy)
-                layout.setAlignment(self.main_widget_proxy, Qt.AlignHCenter)
-        else:
-            layout.addItem(self.body_layout)
+        # if self.node_item.main_widget:
+        #     self.main_widget_proxy = FlowProxyWidget(self.flow)
+        #     self.main_widget_proxy.setWidget(self.node_item.main_widget)
+        #
+        #     if self.node.main_widget_pos == 'between ports':
+        #         self.body_layout.insertItem(1, self.main_widget_proxy)
+        #         self.body_layout.insertStretch(2)
+        #         layout.addItem(self.body_layout)
+        #
+        #     elif self.node.main_widget_pos == 'below ports':
+        #         layout.addItem(self.body_layout)
+        #         layout.addItem(self.main_widget_proxy)
+        #         layout.setAlignment(self.main_widget_proxy, Qt.AlignHCenter)
+        # else:
+        #     layout.addItem(self.body_layout)
+
+        layout.addItem(self.body_layout)
 
         return layout
 
@@ -92,10 +98,16 @@ def rebuild_ui(self):
         for out in self.node.outputs:
             self.add_output_to_layout(out.item)
 
+        if self.node_item.main_widget:
+            self.add_main_widget_to_layout()
+
     def update_shape(self):
 
-        # if not self.initializing:   # just to make sure
-        #     self.rebuild_ui()       # (hopefully) temporary fix -> see rebuild_ui() docstring
+        # makes extended node items shrink according to resizing input widgets
+        if not self.node_item.initializing:
+            self.rebuild_ui()
+        # strangely, this only works for small node items without this, not for extended ones
+
         mw = self.node_item.main_widget
         if mw is not None:  # maybe the main_widget got resized
             self.main_widget_proxy.setMaximumSize(mw.size())
@@ -126,6 +138,15 @@ def update_shape(self):
             self.title_label.setPos(QPointF(-self.title_label.boundingRect().width() / 2,
                                             -self.title_label.boundingRect().height() / 2))
 
+    def add_main_widget_to_layout(self):
+        if self.node.main_widget_pos == 'between ports':
+            self.body_layout.insertItem(1, self.main_widget_proxy)
+            self.body_layout.insertStretch(2)
+
+        elif self.node.main_widget_pos == 'below ports':
+            self.layout().addItem(self.main_widget_proxy)
+            self.layout().setAlignment(self.main_widget_proxy, Qt.AlignHCenter)
+
     def add_input_to_layout(self, inp: InputPortItem):
         if self.inputs_layout.count() > 0:
             self.inputs_layout.addStretch()
diff --git a/src/NodeItem_TitleLabel.py b/src/NodeItem_TitleLabel.py
index a47551b..57956f3 100644
--- a/src/NodeItem_TitleLabel.py
+++ b/src/NodeItem_TitleLabel.py
@@ -42,7 +42,8 @@ def sizeHint(self, which, constraint=...):
 
     def paint(self, painter, option, widget=None):
 
-        self.node_item.session_design.flow_theme.node_item_painter.paint_NI_title_label(
+        self.node_item.session_design.flow_theme.paint_NI_title_label(
+            self.node,
             painter, option, self.hovering,
             self.design_style(), self.title_str, self.node_item.color,
             self.boundingRect()
diff --git a/src/NodeObjPort.py b/src/NodeObjPort.py
index 2ab2ba8..8f15031 100644
--- a/src/NodeObjPort.py
+++ b/src/NodeObjPort.py
@@ -31,7 +31,17 @@ def disconnected(self):
         self.has_been_disconnected.emit()
 
     def config_data(self):
-        pass
+        data_dict = {'type': self.type_,
+                     'label': self.label_str}
+
+        # has_widget = True if self.item.widget else False
+        # data_dict['has widget'] = has_widget
+        # if has_widget:
+        #     data_dict['widget name'] = self.widget_name
+        #     data_dict['widget data'] = None if self.type_ == 'exec' else self.item.widget.get_data()
+        #     data_dict['widget position'] = self.widget_pos
+
+        return data_dict
 
 
 class NodeObjInput(NodeObjPort):
@@ -42,7 +52,7 @@ def __init__(self, node, type_, label_str='', widget_name=None, widget_pos='besi
         self.widget_pos = widget_pos
         self.widget_config_data = config_data
 
-        self.item = InputPortItem(self.node, self)
+        self.item = None  # InputPortItem(self.node, self)
 
 
     def connected(self):
@@ -51,6 +61,11 @@ def connected(self):
             self.node.update(self.node.inputs.index(self))
 
     def get_val(self):
+        if self.item is None:
+            raise Exception(
+                'The node item has not been initialized yet.'
+            )
+
         if len(self.connections) == 0:
             if self.item.widget:
                 return self.item.widget.get_val()
@@ -68,25 +83,12 @@ def update(self):
            not self.node.is_active():
             self.node.update(self.node.inputs.index(self))
 
-    def config_data(self):
-        data_dict = {'type': self.type_,
-                     'label': self.label_str}
-
-        has_widget = True if self.item.widget else False
-        data_dict['has widget'] = has_widget
-        if has_widget:
-            data_dict['widget name'] = self.widget_name
-            data_dict['widget data'] = None if self.type_ == 'exec' else self.item.widget.get_data()
-            data_dict['widget position'] = self.widget_pos
-
-        return data_dict
-
 
 class NodeObjOutput(NodeObjPort):
     def __init__(self, node, type_, label_str=''):
         super().__init__(node, PortObjPos.OUTPUT, type_, label_str)
 
-        self.item = OutputPortItem(node, self)
+        self.item = None  # OutputPortItem(node, self)
 
     def exec(self):
         for c in self.connections:
@@ -106,14 +108,11 @@ def set_val(self, val):
         # if algorithm mode would be exec flow, all data will be required instead of actively forward propagated
         if self.node.flow.alg_mode == FlowAlg.DATA:  # and \
                 # not self.node.initializing
-            self.updated_val()
-
-    def updated_val(self):
-        for c in self.connections:
-            c.activate()
-
-    def config_data(self):
-        data_dict = {'type': self.type_,
-                     'label': self.label_str}
-
-        return data_dict
+            for c in self.connections:
+                c.activate()
+                # c.queue()
+    #         self.updated_val()
+    #
+    # def updated_val(self):
+    #     for c in self.connections:
+    #         c.activate()
diff --git a/src/PortInstance.py b/src/PortInstance.py
deleted file mode 100644
index 680588a..0000000
--- a/src/PortInstance.py
+++ /dev/null
@@ -1,331 +0,0 @@
-from PySide2.QtWidgets import QGraphicsItem, QGraphicsGridLayout, QGraphicsWidget, \
-    QGraphicsLayoutItem
-from PySide2.QtCore import Qt, QRectF, QPointF, QSizeF
-from PySide2.QtGui import QFontMetricsF, QFont
-
-from .PortInstanceInputWidgets import StdSpinBoxInputWidget, StdLineEditInputWidget_NoBorder, \
-    StdLineEditInputWidget
-from .global_tools.strings import get_longest_line, shorten
-from .RC import PortPos, FlowAlg
-
-from .FlowProxyWidget import FlowProxyWidget
-
-
-class PortInstance(QGraphicsGridLayout):
-
-    def __init__(self, parent_node_instance, io_pos, type_='', label_str=''):
-        super(PortInstance, self).__init__()
-
-        # GENERAL ATTRIBUTES
-        self.val = None
-        self.parent_node_instance = parent_node_instance
-        self.io_pos = io_pos
-        self.type_ = type_
-        self.label_str = label_str
-        self.connections = []  # connections stored here
-
-        # gate/pin
-        self.pin = PortInstPin(self, parent_node_instance)
-
-        # label
-        self.label = PortInstLabel(self, parent_node_instance)
-
-
-    def setup_ui(self):
-        pass  # reimplemented in subclasses
-
-    def get_val(self):
-        # Debugger.write('get value in', self.direction, 'port instance',
-        #                self.parent_node_instance.inputs.index(
-        #                         self) if self.direction == 'input' else self.parent_node_instance.outputs.index(self),
-        #                     'of', self.parent_node_instance.parent_node.title)
-        # Debugger.write('val is', self.val)
-        pass
-
-    def connected(self):
-        pass
-
-    def disconnected(self):
-        pass
-
-    def config_data(self):
-        pass  # reimplemented
-
-
-class InputPortInstance(PortInstance):
-    def __init__(self, parent_node_instance, type_='', label_str='',
-                 config_data=None, widget_name=None, widget_pos='besides'):
-
-        super(InputPortInstance, self).__init__(parent_node_instance, PortPos.INPUT, type_, label_str)
-
-        self.widget_name = widget_name
-        self.widget_pos = widget_pos
-        self.widget = None
-        self.proxy: FlowProxyWidget = None
-
-        if config_data is not None:
-            self.create_widget()
-            try:
-                self.widget.set_data(config_data)
-            except Exception as e:
-                print('Exception while setting data in', self.parent_node_instance.parent_node.title,
-                      'NodeInstance\'s input widget:', e, ' (was this intended?)')
-        else:
-            self.create_widget()
-
-        self.setup_ui()
-
-    def setup_ui(self):
-        self.setSpacing(5)
-        self.addItem(self.pin, 0, 0)
-        self.setAlignment(self.pin, Qt.AlignVCenter | Qt.AlignLeft)
-        self.addItem(self.label, 0, 1)
-        self.setAlignment(self.label, Qt.AlignVCenter | Qt.AlignLeft)
-
-        if self.widget is not None:
-            if self.widget_pos == 'besides':
-                self.addItem(self.proxy, 0, 2)
-            elif self.widget_pos == 'below':
-                self.addItem(self.proxy, 1, 0, 1, 2)
-            self.setAlignment(self.proxy, Qt.AlignCenter)
-
-    def create_widget(self, configuration=None):
-        if (self.type_ and self.type_ == 'data') or (configuration and configuration['type'] == 'data'):
-
-            params = (self, self.parent_node_instance)
-
-            if self.widget_name is None:  # no input widget
-                return
-            elif self.widget_name == 'std line edit s':
-                self.widget = StdLineEditInputWidget(params, size='small')
-            elif self.widget_name == 'std line edit m' or self.widget_name == 'std line edit':
-                self.widget = StdLineEditInputWidget(params)
-            elif self.widget_name == 'std line edit l':
-                self.widget = StdLineEditInputWidget(params, size='large')
-            elif self.widget_name == 'std line edit s r':
-                self.widget = StdLineEditInputWidget(params, size='small', resize=True)
-            elif self.widget_name == 'std line edit m r':
-                self.widget = StdLineEditInputWidget(params, resize=True)
-            elif self.widget_name == 'std line edit l r':
-                self.widget = StdLineEditInputWidget(params, size='large', resize=True)
-            elif self.widget_name == 'std line edit s r nb':
-                self.widget = StdLineEditInputWidget_NoBorder(params, size='small',
-                                                              resize=True)
-            elif self.widget_name == 'std line edit m r nb':
-                self.widget = StdLineEditInputWidget_NoBorder(params,
-                                                              resize=True)
-            elif self.widget_name == 'std line edit l r nb':
-                self.widget = StdLineEditInputWidget_NoBorder(params, size='large',
-                                                              resize=True)
-            elif self.widget_name == 'std spin box':
-                self.widget = StdSpinBoxInputWidget(params)
-            else:  # custom input widget
-                self.widget = self.get_input_widget_class(self.widget_name)((self, self.parent_node_instance))
-
-            self.proxy = FlowProxyWidget(self.parent_node_instance.flow, self.parent_node_instance)
-            self.proxy.setWidget(self.widget)
-
-    def get_input_widget_class(self, widget_name):
-        """Returns the CLASS of a defined custom input widget by given name"""
-        # custom_node_input_widget_classes = \
-        #     self.parent_node_instance.flow.script.main_window.custom_node_input_widget_classes
-        # widget_class = custom_node_input_widget_classes[self.parent_node_instance.parent_node][widget_name]
-        # return widget_class
-        return self.parent_node_instance.parent_node.custom_input_widgets[widget_name]
-
-    def connected(self):
-        """Disables the widget and causes update"""
-        if self.widget:
-            self.widget.setEnabled(False)
-        if self.type_ == 'data':
-            self.update()
-
-    def disconnected(self):
-        """Enables the widget again"""
-        if self.widget:
-            self.widget.setEnabled(True)
-
-    def get_val(self):
-        if len(self.connections) == 0:
-            if self.widget:
-                return self.widget.get_val()
-            else:
-                return None
-        else:
-            # Debugger.write('calling connected port for val')
-            return self.connections[0].get_val()
-
-    def update(self):
-        """applies on INPUT; called NI externally (from another NI)"""
-        if (self.parent_node_instance.is_active() and self.type_ == 'exec') or \
-           not self.parent_node_instance.is_active():
-            self.parent_node_instance.update(self.parent_node_instance.inputs.index(self))
-
-    def config_data(self):
-        data_dict = {'type': self.type_,
-                     'label': self.label_str}
-
-        has_widget = True if self.widget else False
-        data_dict['has widget'] = has_widget
-        if has_widget:
-            data_dict['widget name'] = self.widget_name
-            data_dict['widget data'] = None if self.type_ == 'exec' else self.widget.get_data()
-            data_dict['widget position'] = self.widget_pos
-
-        return data_dict
-
-
-class OutputPortInstance(PortInstance):
-    def __init__(self, parent_node_instance, type_='', label_str=''):
-        super(OutputPortInstance, self).__init__(parent_node_instance, PortPos.OUTPUT, type_, label_str)
-
-        self.setup_ui()
-
-    def setup_ui(self):
-        self.setSpacing(5)
-        self.addItem(self.label, 0, 0)
-        self.setAlignment(self.label, Qt.AlignVCenter | Qt.AlignRight)
-        self.addItem(self.pin, 0, 1)
-
-        self.setAlignment(self.pin, Qt.AlignVCenter | Qt.AlignRight)
-
-    def exec(self):
-        for c in self.connections:
-            c.activate()
-
-    def get_val(self):
-        # Debugger.debug('returning val directly')
-        if self.parent_node_instance.flow.alg_mode == FlowAlg.EXEC:
-            self.parent_node_instance.update()
-        return self.val
-
-    def set_val(self, val):
-        """applies on OUTPUT; called NI internally"""
-        # Debugger.write('setting value of output port of', self.parent_node_instance.parent_node.title,
-        #                     'NodeInstance to', val)
-
-        # note that val COULD be of object type and therefore already changed (because the original object did)
-        self.val = val
-
-        # if algorithm mode would be exec flow, all data will be required instead of actively forward propagated
-        if self.parent_node_instance.flow.alg_mode == FlowAlg.DATA and \
-                not self.parent_node_instance.initializing:
-            self.updated_val()
-
-    def updated_val(self):
-        """applies on DATA OUTPUT; called NI internally"""
-        for c in self.connections:
-            c.activate()
-
-    def config_data(self):
-        data_dict = {'type': self.type_,
-                     'label': self.label_str}
-
-        return data_dict
-
-
-# CONTENTS -------------------------------------------------------------------------------------------------------------
-
-
-class PortInstPin(QGraphicsWidget):
-    def __init__(self, parent_port_instance, parent_node_instance):
-        super(PortInstPin, self).__init__(parent_node_instance)
-
-        self.setGraphicsItem(self)
-        self.setAcceptHoverEvents(True)
-        self.hovered = False
-        self.setCursor(Qt.CrossCursor)
-        self.tool_tip_pos = None
-
-        self.parent_port_instance = parent_port_instance
-        self.parent_node_instance = parent_node_instance
-        self.padding = 2
-        self.painting_width = 15
-        self.painting_height = 15
-        self.width = self.painting_width+2*self.padding
-        self.height = self.painting_height+2*self.padding
-        self.port_local_pos = None
-
-    def boundingRect(self):
-        return QRectF(QPointF(0, 0), self.geometry().size())
-
-    def setGeometry(self, rect):
-        self.prepareGeometryChange()
-        QGraphicsLayoutItem.setGeometry(self, rect)
-        self.setPos(rect.topLeft())
-
-    def sizeHint(self, which, constraint=...):
-        return QSizeF(self.width, self.height)
-
-    def paint(self, painter, option, widget=None):
-        self.parent_node_instance.session_design.flow_theme.node_inst_painter.paint_PI(
-            painter, option, self.parent_node_instance.color,
-            self.parent_port_instance.type_,
-            len(self.parent_port_instance.connections) > 0,
-            self.padding, self.painting_width, self.painting_height
-        )
-
-    def mousePressEvent(self, event):
-        event.accept()
-
-    def hoverEnterEvent(self, event):
-        if self.parent_port_instance.type_ == 'data' and self.parent_port_instance.io_pos == PortPos.OUTPUT:
-            self.setToolTip(shorten(str(self.parent_port_instance.val), 1000, line_break=True))
-
-        # hover all connections
-        # self.parent_node_instance.flow.hovered_port_inst_gate = self
-        # self.parent_node_instance.flow.update()
-        self.hovered = True
-
-        QGraphicsItem.hoverEnterEvent(self, event)
-
-    def hoverLeaveEvent(self, event):
-
-        # turn connection highlighting off
-        # self.parent_node_instance.flow.hovered_port_inst_gate = None
-        # self.parent_node_instance.flow.update()
-        self.hovered = False
-
-        QGraphicsItem.hoverLeaveEvent(self, event)
-
-    def get_scene_center_pos(self):
-        return QPointF(self.scenePos().x() + self.boundingRect().width()/2,
-                       self.scenePos().y() + self.boundingRect().height()/2)
-
-
-class PortInstLabel(QGraphicsWidget):
-    def __init__(self, parent_port_instance, parent_node_instance):
-        super(PortInstLabel, self).__init__(parent_node_instance)
-        self.setGraphicsItem(self)
-
-        self.parent_port_instance = parent_port_instance
-        self.parent_node_instance = parent_node_instance
-
-        self.font = QFont("Source Code Pro", 10, QFont.Bold)
-        font_metrics = QFontMetricsF(self.font)  # approximately! the designs can use different fonts
-        self.width = font_metrics.width(get_longest_line(self.parent_port_instance.label_str))
-        self.height = font_metrics.height() * (self.parent_port_instance.label_str.count('\n') + 1)
-        self.port_local_pos = None
-
-    def boundingRect(self):
-        return QRectF(QPointF(0, 0), self.geometry().size())
-
-    def setGeometry(self, rect):
-        self.prepareGeometryChange()
-        QGraphicsLayoutItem.setGeometry(self, rect)
-        self.setPos(rect.topLeft())
-
-    def sizeHint(self, which, constraint=...):
-        return QSizeF(self.width, self.height)
-
-    def paint(self, painter, option, widget=None):
-        self.parent_node_instance.session_design.flow_theme.node_inst_painter.paint_PI_label(
-            painter, option,
-            self.parent_port_instance.type_,
-            len(self.parent_port_instance.connections) > 0,
-            self.parent_port_instance.label_str,
-            self.parent_node_instance.color,
-            self.boundingRect()
-        )
-
-
diff --git a/src/PortInstanceInputWidgets.py b/src/PortInstanceInputWidgets.py
deleted file mode 100644
index 2f986a2..0000000
--- a/src/PortInstanceInputWidgets.py
+++ /dev/null
@@ -1,133 +0,0 @@
-from PySide2.QtGui import QFontMetrics
-from PySide2.QtWidgets import QSpinBox, QLineEdit
-
-from .WidgetBaseClasses import IWB
-from .retain import M
-
-
-class StdSpinBoxInputWidget(QSpinBox, IWB):
-    def __init__(self, params):
-        QSpinBox.__init__(self)
-        IWB.__init__(self, params)
-
-        self.port_local_pos = None
-
-        self.setFixedWidth(50)
-        self.setFixedHeight(25)
-        self.setStyleSheet("""
-            QSpinBox {
-                color: white;
-                background: transparent;
-            }
-        """)
-        self.setMaximum(1000000)
-        self.editingFinished.connect(self.editing_finished)
-
-    def editing_finished(self):
-        self.parent_node_instance.update(self.parent_node_instance.inputs.index(self.parent_port_instance))
-
-    def remove_event(self):
-        pass
-
-    def get_val(self):
-        return self.value()
-
-    def get_data(self):
-        return self.value()
-
-    def set_data(self, data):
-        self.setValue(data)
-
-
-class StdLineEditInputWidget(QLineEdit, IWB):
-    def __init__(self, params, size='medium', resize=False):
-        IWB.__init__(self, params)
-        QLineEdit.__init__(self)
-
-        self.port_local_pos = None
-        self.resizing = resize
-
-        if size == 'small':
-            self.base_width = 30
-        elif size == 'medium':
-            self.base_width = 70
-        elif size == 'large':
-            self.base_width = 150
-
-        self.setFixedWidth(self.base_width)
-
-        self.setFixedHeight(25)
-        self.setPlaceholderText('')
-        self.setStyleSheet("""
-            QLineEdit{
-                border-radius: 10px;
-                background-color: transparent;
-                border: 1px solid #404040;
-                color: #aaaaaa;
-                padding: 3px;
-            }
-            QLineEdit:hover {
-                background-color: rgba(59, 156, 217, 150);
-            }
-            QLineEdit:disabled{
-                color: #777777;
-            }
-        """)
-        f = self.font()
-        f.setPointSize(10)
-        self.setFont(f)
-        self.textChanged.connect(M(self.text_changed))
-        self.editingFinished.connect(M(self.editing_finished))
-
-    def text_changed(self, new_text):
-        if self.resizing:
-            fm = QFontMetrics(self.font())
-            text_width = fm.width(new_text)
-            new_width = text_width+15
-            self.setFixedWidth(new_width if new_width > self.base_width else self.base_width)
-
-            self.parent_node_instance.update_shape()
-            # self.parent_node_instance.rebuild_ui()  # see rebuild_ui() for explanation
-
-    def editing_finished(self):
-        self.parent_node_instance.update(self.parent_node_instance.inputs.index(self.parent_port_instance))
-
-    def remove_event(self):
-        pass
-
-    def get_val(self):
-        val = None
-        try:
-            val = eval(self.text())
-        except Exception as e:
-            # type(eval(json.dumps(self.text()))) could be 'dict' <- need that for typing in dicts later if I want to
-            val = self.text()
-        return val
-
-    def get_data(self):
-        return self.text()
-
-    def set_data(self, data):
-        if type(data) == str:
-            self.setText(data)
-
-
-class StdLineEditInputWidget_NoBorder(StdLineEditInputWidget):
-    def __init__(self, params, size='medium', resize=False):
-        StdLineEditInputWidget.__init__(self, params, size, resize)
-
-        self.setStyleSheet("""
-            QLineEdit{
-                border: none;
-                border-radius: 5px;
-                background-color: transparent;
-                color: #aaaaaa;
-                padding: 3px;
-            }
-            QLineEdit:hover {
-                background-color: rgba(59, 156, 217, 150);
-            }
-            QLineEdit:disabled{
-                color: #777777;
-            }
-        """)
diff --git a/src/PortItem.py b/src/PortItem.py
index 19533ca..29ca669 100644
--- a/src/PortItem.py
+++ b/src/PortItem.py
@@ -7,37 +7,28 @@
     StdLineEditInputWidget
 from .global_tools.strings import get_longest_line, shorten
 
-from .FlowProxyWidget import FlowProxyWidget
+from .FlowViewProxyWidget import FlowViewProxyWidget
 
 
 class PortItem(QGraphicsGridLayout):
 
-    def __init__(self, node, port, flow):
+    def __init__(self, node, port, flow_view):
         super(PortItem, self).__init__()
 
-        # # GENERAL ATTRIBUTES
-        # self.val = None
-        # self.parent_node_instance = parent_node_instance
-        # self.io_pos = io_pos
-        # self.type_ = type_
-        # self.label_str = label_str
-        # self.connections = []  # connections stored here
         self.node = node
         self.port = port
-        self.flow = flow
+        self.flow_view = flow_view
 
         self.port.has_been_connected.connect(self.port_connected)
         self.port.has_been_disconnected.connect(self.port_disconnected)
 
-        # gate/pin
         self.pin = PortItemPin(self.port, self.node)
 
-        # label
         self.label = PortItemLabel(self.port, self.node)
 
 
     def setup_ui(self):
-        pass  # reimplemented in subclasses
+        pass
 
     def port_connected(self):
         pass
@@ -51,7 +42,7 @@ def __init__(self, node, port):
         super().__init__(node, port, node.flow)
 
         self.widget = None
-        self.proxy: FlowProxyWidget = None
+        self.proxy: FlowViewProxyWidget = None
 
         if self.port.widget_config_data is not None:
             self.create_widget()
@@ -114,7 +105,7 @@ def create_widget(self, configuration=None):
             else:  # custom input widget
                 self.widget = self.get_input_widget_class(wn)(params)
 
-            self.proxy = FlowProxyWidget(self.flow, parent=self.node.item)
+            self.proxy = FlowViewProxyWidget(self.flow_view, parent=self.node.item)
             self.proxy.setWidget(self.widget)
 
     def get_input_widget_class(self, widget_name):
@@ -184,11 +175,16 @@ def sizeHint(self, which, constraint=...):
         return QSizeF(self.width, self.height)
 
     def paint(self, painter, option, widget=None):
-        self.node_item.session_design.flow_theme.node_item_painter.paint_PI(
-            painter, option, self.node_item.color,
-            self.port.type_,
-            len(self.port.connections) > 0,
-            self.padding, self.painting_width, self.painting_height
+        self.node_item.session_design.flow_theme.paint_PI(
+            node=self.node,
+            painter=painter,
+            option=option,
+            node_color=self.node_item.color,
+            type_=self.port.type_,
+            connected=len(self.port.connections) > 0,
+            padding=self.padding,
+            w=self.painting_width,
+            h=self.painting_height
         )
 
     def mousePressEvent(self, event):
@@ -246,7 +242,8 @@ def sizeHint(self, which, constraint=...):
         return QSizeF(self.width, self.height)
 
     def paint(self, painter, option, widget=None):
-        self.node_item.session_design.flow_theme.node_item_painter.paint_PI_label(
+        self.node_item.session_design.flow_theme.paint_PI_label(
+            self.node,
             painter, option,
             self.port.type_,
             len(self.port.connections) > 0,
diff --git a/src/PortItemInputWidgets.py b/src/PortItemInputWidgets.py
index 36f07f1..05053b5 100644
--- a/src/PortItemInputWidgets.py
+++ b/src/PortItemInputWidgets.py
@@ -1,30 +1,42 @@
-from PySide2.QtGui import QFontMetrics
+import os
+
+from PySide2.QtCore import Signal
+from PySide2.QtGui import QFontMetrics, QColor
 from PySide2.QtWidgets import QSpinBox, QLineEdit
 
+
 from .WidgetBaseClasses import IWB
 from .retain import M
+# from custom_src.ryvencore.src.WidgetBaseClasses import IWB
+# from custom_src.ryvencore.src.retain import M
 
 
 class StdSpinBoxInputWidget(QSpinBox, IWB):
+
+    trigger_update = Signal(int)
+
     def __init__(self, params):
         QSpinBox.__init__(self)
         IWB.__init__(self, params)
 
+        self.trigger_update.connect(self.node.update)
+
         self.port_local_pos = None
 
         self.setFixedWidth(50)
         self.setFixedHeight(25)
-        self.setStyleSheet("""
-            QSpinBox {
-                color: white;
-                background: transparent;
-            }
-        """)
+        # self.setStyleSheet("""
+        #     QSpinBox {
+        #         color: white;
+        #         background: transparent;
+        #     }
+        # """)
         self.setMaximum(1000000)
         self.editingFinished.connect(self.editing_finished)
 
     def editing_finished(self):
-        self.node.update(self.node.inputs.index(self.input))
+        # self.node.update(self.node.inputs.index(self.input))
+        self.trigger_update.emit(self.node.inputs.index(self.input))
 
     def remove_event(self):
         pass
@@ -40,10 +52,15 @@ def set_data(self, data):
 
 
 class StdLineEditInputWidget(QLineEdit, IWB):
+
+    trigger_update = Signal(int)
+
     def __init__(self, params, size='medium', resize=False):
         IWB.__init__(self, params)
         QLineEdit.__init__(self)
 
+        self.trigger_update.connect(self.node.update)
+
         self.port_local_pos = None
         self.resizing = resize
 
@@ -56,23 +73,29 @@ def __init__(self, params, size='medium', resize=False):
 
         self.setFixedWidth(self.base_width)
 
-        self.setFixedHeight(25)
+        # self.setFixedHeight(25)
         self.setPlaceholderText('')
-        self.setStyleSheet("""
-            QLineEdit{
-                border-radius: 10px;
-                background-color: transparent;
-                border: 1px solid #404040;
-                color: #aaaaaa;
-                padding: 3px;
-            }
-            QLineEdit:hover {
-                background-color: rgba(59, 156, 217, 150);
-            }
-            QLineEdit:disabled{
-                color: #777777;
-            }
-        """)
+
+        # / *border - color: '''+self.node.color+'''; * /
+
+        # c = QColor('#8c8c8c')
+        # background_color = f'rgba({c.red()}, {c.green()}, {c.blue()}, 0.2)'
+
+        if self.node.style == 'small':
+            self.setStyleSheet('''
+QLineEdit{
+    padding: 1px 1px ;
+    background: transparent;
+}
+
+            ''')
+        else:
+            self.setStyleSheet('''
+QLineEdit{ 
+    padding: 1px 1px ;
+}
+            ''')
+
         f = self.font()
         f.setPointSize(10)
         self.setFont(f)
@@ -90,7 +113,8 @@ def text_changed(self, new_text):
             # self.parent_node_instance.rebuild_ui()  # see rebuild_ui() for explanation
 
     def editing_finished(self):
-        self.node.update(self.node.inputs.index(self.input))
+        # self.node.update(self.node.inputs.index(self.input))
+        self.trigger_update.emit(self.node.inputs.index(self.input))
 
     def remove_event(self):
         pass
@@ -116,18 +140,16 @@ class StdLineEditInputWidget_NoBorder(StdLineEditInputWidget):
     def __init__(self, params, size='medium', resize=False):
         StdLineEditInputWidget.__init__(self, params, size, resize)
 
-        self.setStyleSheet("""
-            QLineEdit{
-                border: none;
-                border-radius: 5px;
-                background-color: transparent;
-                color: #aaaaaa;
-                padding: 3px;
-            }
-            QLineEdit:hover {
-                background-color: rgba(59, 156, 217, 150);
-            }
-            QLineEdit:disabled{
-                color: #777777;
-            }
-        """)
+        c = QColor(self.node.color)
+
+        hover_color = f'rgba({c.red()}, {c.green()}, {c.blue()}, 0.5)'
+
+        self.setStyleSheet(self.styleSheet() + '''
+QLineEdit {
+    border: none;
+}
+QLineEdit:hover {
+    background-color: '''+hover_color+''';
+    border: none;
+}
+        ''')
diff --git a/src/Script.py b/src/Script.py
index 5637230..63f3983 100644
--- a/src/Script.py
+++ b/src/Script.py
@@ -1,41 +1,106 @@
-from PySide2.QtCore import QObject, Signal
+import time
+
+from PySide2.QtCore import QObject, Signal, QTimer
 
 from .Flow import Flow
-from .FlowWorkerThread import FlowWorkerThread
-from .RC import FlowVPUpdateMode, FlowAlg
+from .FlowView import FlowView
+from .SessionThreadingBridge import SessionThreadingBridge
 from .logging.Logger import Logger
 from .script_variables.VarsManager import VarsManager
 
 
 class Script(QObject):
 
-    # name_changed = Signal(str)  ->  Script()
+    create_flow_view_request = Signal(object, tuple)
+    generate_flow_view_config_request = Signal(dict)
 
-    def __init__(self, session, title: str = None, config: dict = None, flow_size: list = None, flow_parent=None,
+    def __init__(self, session, title: str = None, content_data: dict = None, flow_size: list = None,
                  create_default_logs=True):
-        super(Script, self).__init__()
+        super(Script, self).__init__(parent=session)
 
         self.session = session
         self.logger = Logger(self, create_default_logs)
         self.vars_manager = None
         self.title = title
-        self.flow = None
+        self.flow = Flow(self.session, self, self)
+        self.flow_view = None
+
+        self.init_flow_config = None
+        self.init_flow_view_config = None
+        if content_data:
+            self.init_flow_config = content_data['flow'] if content_data is not None else None
+            if 'flow widget config' in content_data:
+                self.init_flow_view_config = content_data['flow widget config'] if content_data is not None else None
+            else:  # backwards compatibility
+                self.init_flow_view_config = content_data['flow']
+
+        self.init_flow_size = flow_size
+        self.init_flow_gui_parent = self.session.gui_parent
 
         # TODO: move the thumbnail source to the list widget
         self._thumbnail_source = ''  # URL to the Script's thumbnail picture
 
-        if config:
-            self.title = config['name']
-            self.vars_manager = VarsManager(self, config['variables'])
-            self.flow = Flow(session, self, flow_size, config['flow'], parent=flow_parent)
+        if self.session.threaded:
+            self.create_flow_view_request.connect(
+                self.session.threading_bridge.script_request__create_flow_view
+            )
+            self.tmp_data = None
+
+            if self.session.gui_parent is None:
+                raise Exception(
+                    "When using threading, you must provide a gui_parent."
+                )
+
+        flow_view_params = (session, self, self.flow, self.init_flow_view_config, flow_size, self.session.gui_parent)
+
+        # TITLE, VARS MANAGER
+        if content_data:
+            self.title = content_data['name']
+            self.vars_manager = VarsManager(self, content_data['variables'])
         else:
-            self.flow = Flow(session, self, flow_size, parent=flow_parent)
             self.vars_manager = VarsManager(self)
 
-    def config_data(self) -> dict:
+        # GUI
+        if self.session.threaded:
+            QTimer.singleShot(0, self.compute_something)
+
+            self.create_flow_view_request.emit(self, flow_view_params)
+            while self.tmp_data is None:
+                time.sleep(0.001)
+            flow_view = self.tmp_data
+        else:
+            flow_view = FlowView(*flow_view_params)
+        self.flow_view_created(flow_view)
+
+
+    def flow_view_created(self, flow_view):
+
+        self.flow_view = flow_view
+
+        self.generate_flow_view_config_request.connect(self.flow_view.generate_config_data)
+
+        if self.init_flow_config is not None:
+            self.flow.load(config=self.init_flow_config)
+
+
+    def serialize(self) -> dict:
         """Returns the config data of the script, including variables and flow content"""
-        script_dict = {'name': self.title,
-                       'variables': self.vars_manager.config_data(),
-                       'flow': self.flow.config_data()}
 
-        return script_dict
\ No newline at end of file
+        abstract_flow_data = self.flow.generate_config_data()
+        self.generate_flow_view_config_request.emit(abstract_flow_data)
+
+        # # the flow widget currently creates the whole config
+        # self.flow_view._temp_config_data = None
+        # self.generate_flow_view_config_request.emit()
+        while self.flow_view._temp_config_data is None:
+            time.sleep(0.001)  # join threads
+        flow_config, flow_view_config = self.flow_view._temp_config_data
+
+        script_dict = {
+            'name': self.title,
+            'variables': self.vars_manager.config_data(),
+            'flow': flow_config,
+            'flow widget config': flow_view_config
+        }
+
+        return script_dict
diff --git a/src/Session.py b/src/Session.py
index 558e15f..0a2ad0b 100644
--- a/src/Session.py
+++ b/src/Session.py
@@ -1,9 +1,13 @@
-from PySide2.QtCore import QObject, Signal
+import time
+
+from PySide2.QtCore import QObject, Signal, QThread
 from PySide2.QtGui import QFontDatabase
+from PySide2.QtWidgets import QWidget
 
 from .Connection import DataConnection, ExecConnection
 from .GlobalAttributes import Location
 from .Script import Script
+from .SessionThreadingBridge import SessionThreadingBridge
 from .global_tools.Debugger import Debugger
 from .Design import Design
 
@@ -19,45 +23,51 @@ class Session(QObject):
 
     def __init__(
             self,
-            flow_performance_mode: str = 'pretty',
-            animations_enabled: bool = True,
-            flow_theme_name: str = 'ueli',
-            # debug_messages_enabled: bool = False,
-            threading_enabled: bool = True,
-            flow_data_conn_class=DataConnection,
-            flow_exec_conn_class=ExecConnection,
-            project: dict = None
+            threaded: bool = False,
+            gui_parent: QWidget = None,
+            gui_thread: QThread = None,
+            flow_theme_name=None,
+            performance_mode=None,
+            parent: QObject = None
     ):
-        super().__init__()
+        super().__init__(parent=parent)
 
         self._register_fonts()
 
         self.scripts: [Script] = []
         self.nodes = []  # list of node CLASSES
-        self.threading_enabled = threading_enabled
-
-        # connections
-        self.flow_data_conn_class = flow_data_conn_class
-        self.flow_exec_conn_class = flow_exec_conn_class
-
-        self.design = Design(
-            performance_mode=flow_performance_mode,
-            animations_enabled=animations_enabled
-        )
-
-        self.design.set_flow_theme(name=flow_theme_name)
-        self.design.set_flow_theme(name=flow_theme_name)  # temporary
-        #   the double call is just a temporary fix for an issue I will address in a future release.
-        #   Problem: because the signal emitted when setting a flow theme is directly connected to the according slots
-        #   in NodeItem as well as NodeItem_TitleLabel, the NodeItem's slot (which starts an animation which
-        #   uses the title label's current and theme dependent color) could get called before the title
-        #   label's slot has been called to reinitialize this color. This results in wrong color end points for the
-        #   title label when activating animations.
-        #   This is pretty nasty since I cannot think of a nice fix for this issue other that not letting the slot
-        #   methods be called directly from the emitted signal but instead through a defined procedure like before.
-
-        if project:
-            self.load(project)
+        self.threaded = threaded
+        self.threading_bridge = None
+        self.gui_parent = gui_parent
+        if self.threaded:
+            self.threading_bridge = SessionThreadingBridge()
+            self.threading_bridge.moveToThread(gui_parent.thread())
+
+        # if threaded:
+        #     self.custom_thread = self.thread()
+        #     self.gui_thread = self.thread().thread()
+
+        # # connections
+        # self.flow_data_conn_class = flow_data_conn_class
+        # self.flow_exec_conn_class = flow_exec_conn_class
+
+        self.design = Design()
+        if flow_theme_name:
+            self.design.set_flow_theme(name=flow_theme_name)
+        if performance_mode:
+            self.design.set_performance_mode(performance_mode)
+
+        # if flow_theme_name:
+        #     self.design.set_flow_theme(name=flow_theme_name)
+        #     self.design.set_flow_theme(name=flow_theme_name)  # temporary
+        #     # the double call is just a temporary fix for an issue I will address in a future release.
+        #     # Problem: because the signal emitted when setting a flow theme is directly connected to the according slots
+        #     # in NodeItem as well as NodeItem_TitleLabel, the NodeItem's slot (which starts an animation which
+        #     # uses the title label's current and theme dependent color) could get called before the title
+        #     # label's slot has been called to reinitialize this color. This results in wrong color end points for the
+        #     # title label when activating animations.
+        #     # This is pretty nasty since I cannot think of a nice fix for this issue other that not letting the slot
+        #     # methods be called directly from the emitted signal but instead through a defined procedure like before.
 
 
     def _register_fonts(self):
@@ -79,20 +89,21 @@ def register_node(self, node_class):
         self.nodes.append(node_class)
 
 
-    def create_script(self, title: str, flow_size: list = None, flow_parent=None, create_default_logs=True) -> Script:
+    def create_script(self, title: str, flow_size: list = None, create_default_logs=True) -> Script:
         """Creates and returns a new script"""
 
-        script = Script(session=self, title=title, flow_size=flow_size, flow_parent=flow_parent,
-                        create_default_logs=create_default_logs)
+        script = Script(session=self, title=title, flow_size=flow_size, create_default_logs=create_default_logs)
+
         self.scripts.append(script)
         self.new_script_created.emit(script)
+
         return script
 
 
     def _load_script(self, config: dict):
         """Loads a script from a project dict"""
 
-        script = Script(session=self, config=config)
+        script = Script(session=self, content_data=config)
         self.scripts.append(script)
         self.new_script_created.emit(script)
 
@@ -134,13 +145,16 @@ def load(self, project: dict) -> bool:
             self._load_script(config=s)
 
 
-    def serialize(self) -> list:
+    def serialize(self) -> dict:
         """Returns a list with 'config data' of all scripts for saving the project"""
 
-        scripts_data = []
+        data = {}
+        scripts_list = []
         for script in self.scripts:
-            scripts_data.append(script.config_data())
-        return scripts_data
+            scripts_list.append(script.serialize())
+        data['scripts'] = scripts_list
+
+        return data
 
 
     def all_nodes(self):
diff --git a/src/SessionThreadingBridge.py b/src/SessionThreadingBridge.py
new file mode 100644
index 0000000..2dde855
--- /dev/null
+++ b/src/SessionThreadingBridge.py
@@ -0,0 +1,13 @@
+from PySide2.QtCore import QObject
+
+from .FlowView import FlowView
+
+
+class SessionThreadingBridge(QObject):
+    """
+    ...
+    """
+
+    def script_request__create_flow_view(self, script, params):
+        view = FlowView(*params)
+        script.tmp_data = view
diff --git a/src/custom_list_widgets/ListWidget_NameLineEdit.py b/src/custom_list_widgets/ListWidget_NameLineEdit.py
index 89476fa..68ff95c 100644
--- a/src/custom_list_widgets/ListWidget_NameLineEdit.py
+++ b/src/custom_list_widgets/ListWidget_NameLineEdit.py
@@ -9,6 +9,12 @@ class ListWidget_NameLineEdit(QLineEdit):
     def __init__(self, text, parent):
         super(ListWidget_NameLineEdit, self).__init__(text, parent)
 
+#         self.setStyleSheet('''
+# QLineEdit:disabled {
+#     color: white;
+# }
+#         ''')
+
 
     def focusOutEvent(self, arg__1):
         self.unfocused.emit()
diff --git a/src/custom_list_widgets/ScriptsListWidget.py b/src/custom_list_widgets/ScriptsListWidget.py
index 08f354e..543e8dc 100644
--- a/src/custom_list_widgets/ScriptsListWidget.py
+++ b/src/custom_list_widgets/ScriptsListWidget.py
@@ -32,6 +32,10 @@ def setup_UI(self):
         self.list_layout = QVBoxLayout()
         self.list_layout.setAlignment(Qt.AlignTop)
 
+        # le = QLineEdit('test1')
+        # le.setEnabled(False)
+        # self.list_layout.addWidget(le)
+
         main_layout.addLayout(self.list_layout)
 
         self.new_script_title_lineedit = QLineEdit()
diff --git a/src/global_tools/stuff.py b/src/global_tools/stuff.py
deleted file mode 100644
index a037051..0000000
--- a/src/global_tools/stuff.py
+++ /dev/null
@@ -1,2 +0,0 @@
-def sort_nodes(nodes):
-    return sorted(nodes, key=lambda x: x.title.lower())
\ No newline at end of file
diff --git a/src/logging/LogWidget.py b/src/logging/LogWidget.py
index 1637ed7..93e32a5 100644
--- a/src/logging/LogWidget.py
+++ b/src/logging/LogWidget.py
@@ -20,7 +20,7 @@ def __init__(self, log: Log):
         self.header_layout = QHBoxLayout()
 
         title_label = QLabel(self.log.title)
-        title_label.setFont(QFont('Poppins', 13))
+        title_label.setFont(QFont('Poppins', 12))
         self.header_layout.addWidget(title_label)
 
         self.remove_button = QPushButton('x')
@@ -36,27 +36,27 @@ def __init__(self, log: Log):
 
         self.setLayout(self.main_layout)
 
-        self.enabled_style_sheet = '''
-            QLabel {
-                border: None;
-            }
-            QWidget {
-                color: #e9f4fb;
-            }
-        '''
-        self.disabled_style_sheet = '''
-            QLabel {
-                border: None;
-            }
-            QWidget {
-                color: #e9f4fb;
-            }
-            QPlainTextEdit {
-                background: black; 
-                color: grey;
-            }
-        '''
-        self.setStyleSheet(self.enabled_style_sheet)
+        # self.enabled_style_sheet = '''
+        #     QLabel {
+        #         border: None;
+        #     }
+        #     QWidget {
+        #         color: #e9f4fb;
+        #     }
+        # '''
+        # self.disabled_style_sheet = '''
+        #     QLabel {
+        #         border: None;
+        #     }
+        #     QWidget {
+        #         color: #e9f4fb;
+        #     }
+        #     QPlainTextEdit {
+        #         background: black;
+        #         color: grey;
+        #     }
+        # '''
+        # self.setStyleSheet(self.enabled_style_sheet)
 
     def write(self, msg: str):
         self.text_edit.appendPlainText(msg)
@@ -66,11 +66,11 @@ def clear(self):
 
     def disable(self):
         self.remove_button.show()
-        self.setStyleSheet(self.disabled_style_sheet)
+        # self.setStyleSheet(self.disabled_style_sheet)
 
     def enable(self):
         self.remove_button.hide()
-        self.setStyleSheet(self.enabled_style_sheet)
+        # self.setStyleSheet(self.enabled_style_sheet)
         self.show()
 
     def remove_clicked(self):
diff --git a/src/node_choice_widget/NodeChoiceWidget.py b/src/node_choice_widget/PlaceNodeWidget.py
similarity index 91%
rename from src/node_choice_widget/NodeChoiceWidget.py
rename to src/node_choice_widget/PlaceNodeWidget.py
index 130d7f7..1785fab 100644
--- a/src/node_choice_widget/NodeChoiceWidget.py
+++ b/src/node_choice_widget/PlaceNodeWidget.py
@@ -6,11 +6,12 @@
 from ..node_choice_widget.NodeWidget import NodeWidget
 
 
-class NodeChoiceWidget(QWidget):
-    def __init__(self, flow, nodes):
-        super(NodeChoiceWidget, self).__init__()
+class PlaceNodeWidget(QWidget):
+    # TODO: rename "node" occurrences accordingly to new "node class" system
+    def __init__(self, flow_view, nodes):
+        super(PlaceNodeWidget, self).__init__()
 
-        self.flow = flow
+        self.flow_view = flow_view
 
         self.all_nodes = sort_nodes(nodes)  # copy, no ref
 
@@ -59,7 +60,7 @@ def __init__(self, flow, nodes):
 
         self.update_view('')
 
-        self.setStyleSheet(self.flow.session.design.node_choice_stylesheet)
+        self.setStyleSheet(self.flow_view.session.design.node_selection_stylesheet)
 
         self.search_line_edit.setFocus()
 
@@ -70,7 +71,7 @@ def mousePressEvent(self, event):
 
     def keyPressEvent(self, event):
         if event.key() == Qt.Key_Escape:
-            self.flow.hide_node_choice_widget()
+            self.flow_view.hide_place_node_widget()
 
         elif event.key() == Qt.Key_Down:
             index = self.active_node_widget_index+1 if \
@@ -182,7 +183,7 @@ def set_active_node_widget_index(self, index):
 
 
     def node_widget_chosen(self):  # gets called when the user clicks on a node widget with the mouse
-        self.flow.ignore_mouse_event = True  # otherwise, it will receive a mouse press event
+        self.flow_view._ignore_mouse_event = True  # otherwise, it will receive a mouse press event
 
         index = int(self.sender().objectName()[self.sender().objectName().rindex('_')+1:])
         self.place_node(index)
@@ -191,9 +192,10 @@ def node_widget_chosen(self):  # gets called when the user clicks on a node widg
     def place_node(self, index):
         node_index = index
         node = self.current_nodes[node_index]
-        self.flow.place_node__cmd(node)
+        self.flow_view.create_node__cmd(node)
+        # self.flow.create_node_request.emit(node, None)
 
-        self.flow.hide_node_choice_widget()
+        self.flow_view.hide_place_node_widget()
 
 
 def sort_nodes(nodes):
diff --git a/src/ryvencore.egg-info/PKG-INFO b/src/ryvencore.egg-info/PKG-INFO
deleted file mode 100644
index c4d4a3c..0000000
--- a/src/ryvencore.egg-info/PKG-INFO
+++ /dev/null
@@ -1,10 +0,0 @@
-Metadata-Version: 1.0
-Name: ryvencore
-Version: 0.0.1
-Summary: PySide-based library for creating flow-based visual programming editors
-Home-page: UNKNOWN
-Author: UNKNOWN
-Author-email: UNKNOWN
-License: UNKNOWN
-Description: UNKNOWN
-Platform: UNKNOWN
diff --git a/src/ryvencore.egg-info/SOURCES.txt b/src/ryvencore.egg-info/SOURCES.txt
deleted file mode 100644
index d1488f6..0000000
--- a/src/ryvencore.egg-info/SOURCES.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-setup.py
-src/ryvencore.py
-src/ryvencore.egg-info/PKG-INFO
-src/ryvencore.egg-info/SOURCES.txt
-src/ryvencore.egg-info/dependency_links.txt
-src/ryvencore.egg-info/top_level.txt
\ No newline at end of file
diff --git a/src/ryvencore.egg-info/dependency_links.txt b/src/ryvencore.egg-info/dependency_links.txt
deleted file mode 100644
index 8b13789..0000000
--- a/src/ryvencore.egg-info/dependency_links.txt
+++ /dev/null
@@ -1,1 +0,0 @@
-
diff --git a/src/ryvencore.egg-info/top_level.txt b/src/ryvencore.egg-info/top_level.txt
deleted file mode 100644
index a2d7040..0000000
--- a/src/ryvencore.egg-info/top_level.txt
+++ /dev/null
@@ -1,1 +0,0 @@
-ryvencore
diff --git a/src/ryvencore.py b/src/ryvencore.py
index 890e5bf..f4eacf7 100644
--- a/src/ryvencore.py
+++ b/src/ryvencore.py
@@ -1,6 +1,5 @@
 from .Node import Node
 from .NodePort import NodeInput, NodeOutput
-from .NodeItem import NodeItem
 from .Session import Session
 from .WidgetBaseClasses import MWB, IWB
 from .global_tools.Debugger import Debugger
@@ -57,4 +56,4 @@ class Retain:
 import os
 from .GlobalAttributes import Location
 Location.PACKAGE_PATH = os.path.normpath(os.path.dirname(__file__)+'/../')
-print(Location.PACKAGE_PATH)
+# print(Location.PACKAGE_PATH)


Test patch:
diff --git a/src/threading_tests.py b/src/threading_tests.py
new file mode 100644
index 0000000..fc99af2
--- /dev/null
+++ b/src/threading_tests.py
@@ -0,0 +1,52 @@
+import sys
+
+from PySide2.QtWidgets import QMainWindow, QApplication
+from PySide2.QtCore import QObject, Signal, QThread
+
+
+class ThreadBridge(QObject):
+
+    done = Signal()
+
+    def do_something(self):
+        self.done.emit()
+
+
+class Controller(QObject):
+
+    some_request = Signal()
+
+    def __init__(self, main_thread):
+        super().__init__()
+        self.bridge = ThreadBridge()
+        self.bridge.moveToThread(main_thread)
+
+    def create(self):
+        self.some_request.connect(self.bridge.do_something)
+        self.bridge.done.connect(self.finish)
+        self.some_request.emit()
+
+    def finish(self):
+        print('success!')
+
+
+class MainWindow(QMainWindow):
+    init_controller_signal = Signal()
+
+    def __init__(self):
+        super().__init__()
+
+        self.controller_thread = QThread(self)
+        self.controller_thread.start()
+
+        self.controller = Controller(self.thread())
+        self.init_controller_signal.connect(self.controller.create)
+        self.controller.moveToThread(self.controller_thread)
+        self.init_controller_signal.emit()
+
+
+if __name__ == "__main__":
+    app = QApplication()
+    mw = MainWindow()
+    mw.show()
+    sys.exit(app.exec_())