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

Issue:
master



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-ryven.yml
similarity index 86%
rename from .github/workflows/publish-package.yml
rename to .github/workflows/publish-ryven.yml
index 297d1908..efc47cbf 100644
--- a/.github/workflows/publish-package.yml
+++ b/.github/workflows/publish-ryven.yml
@@ -1,4 +1,4 @@
-name: Publish package distribution to TestPyPi and PyPi
+name: Publish Ryven wheel to TestPyPi and PyPi
 on:
   create:
     tags:
@@ -12,6 +12,8 @@ jobs:
         with:
           python-version: 3.x
           architecture: x64
+      - name: Change directory to ryven-editor
+        run: cd ryven-editor
       - name: Install pypa/build
         run: python -m pip install build --user
       - name: Build binary wheel and source tarball
diff --git a/.github/workflows/publish-ryvencore-qt.yml b/.github/workflows/publish-ryvencore-qt.yml
new file mode 100644
index 00000000..202f301a
--- /dev/null
+++ b/.github/workflows/publish-ryvencore-qt.yml
@@ -0,0 +1,29 @@
+name: Publish ryvencore-qt wheel to TestPyPi and PyPi
+on:
+  create:
+    tags:
+      - '*'
+jobs:
+  Build-and-Publish:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
+        with:
+          python-version: 3.x
+          architecture: x64
+      - name: Change directory to ryvencore-qt
+        run: cd ryvencore-qt
+      - name: Install pypa/build
+        run: python -m pip install build --user
+      - name: Build binary wheel and source tarball
+        run: python -m build --sdist --wheel --outdir dist/
+      - name: Publish distribution to TestPyPI
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          password: ${{ secrets.TEST_PYPI_API_TOKEN }}
+          repository-url: https://test.pypi.org/legacy/
+      - name: Publish distribution to PyPI
+        uses: pypa/gh-action-pypi-publish@master
+        with:
+          password: ${{ secrets.PYPI_API_TOKEN }}
\ No newline at end of file
diff --git a/README.md b/README.md
index 9ba0603a..08417e42 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,18 @@
-> This project is not very actively maintained. With the latest release - which I'm currently working on getting mostly stable - the project became quite accurately what I wanted it to be, and I'll have limited time for it in the future. If you are a contributor and interested in maintaining, please [contact me](mailto:l.thomm@mailbox.org).
+> This project is not receiving substantial updates, and I'll have limited time for it in the future. With the latest release the project became quite accurately what I want it to be: an easy and flexible editor and framework to explore use cases of flow-based visual scripting in Python. If you have questions or further ideas feel free to open issues or fork the project and try it yourself.
 
 <p align="center">
   <img src="./docs/img/logo.png" alt="drawing" width="70%"/>
 </p>
 
-Ryven is an experimental node editor written in Python, it implements a Qt-based visual interface for flow-based programming in Python. It provides a powerful system for developing nodes executing any Python code, and an editor for building graphs using those nodes. Ryven features a bunch of configuration options and a headless mode for running graphs without any GUI. Some relevant GitHub repos:
+Ryven is an experimental node editor written in Python. It implements a Qt-based visual interface for flow-based visual scripting in Python. It provides a powerful system for developing nodes executing any Python code, and an editor for building graphs using those nodes. Ryven features a bunch of configuration options and a headless mode for running graphs without any GUI. Some relevant GitHub repos:
 
-* [ryvencore](https://github.com/leon-thomm/ryvencore): backend / core framework
-* [ryvencore-qt](https://github.com/leon-thomm/ryvencore-qt): Qt frontend classes for ryvencore
-* [ryven-blender](https://github.com/leon-thomm/ryven-blender), [ryven-unreal](https://github.com/leon-thomm/ryven-unreal): Ryven plugins for Blender and UE4
-  * _not actively maintained_
-* [PythonOCC nodes for Ryven](https://github.com/Tanneguydv/Pythonocc-nodes-for-Ryven): WIP Ryven nodes for PythonOCC (3D CAD)
+* [ryvencore](https://github.com/leon-thomm/ryvencore): backend / core library
+* [ryven-blender](https://github.com/leon-thomm/ryven-blender), [ryven-unreal](https://github.com/leon-thomm/ryven-unreal): Ryven plugins for Blender and UE4 (_deprecated_)
+* [PythonOCC nodes for Ryven](https://github.com/Tanneguydv/Pythonocc-nodes-for-Ryven): WIP Ryven nodes for PythonOCC (3D CAD) (_deprecated_)
 * [ironflow](https://github.com/pyiron/ironflow): WIP node interface in jupyter for [pyiron](https://github.com/pyiron) based on ryvencore
 
+The `ryvencore-qt` library adds Qt-based GUI classes for ryvencore (`./ryvencore-qt/`), and the Ryven editor assembles them into a fully-featured cross-platform application (`./ryven-editor/`).
+
 ## Installation and Configuration
 
 Once you have Python and pip installed, Ryven is available on PyPI via
@@ -21,15 +21,13 @@ Once you have Python and pip installed, Ryven is available on PyPI via
 pip install ryven
 ```
 
-*The current default version on PyPI is significantly outdated, you may currently instead `pip install ryven --pre`*.
-
 There is also a [conda-forge package](https://anaconda.org/conda-forge/ryven) (`conda install -c conda-forge ryven`).
 
-Ryven can be launched from the command line with `ryven`. If you installed Ryven into a Python virtual environment, the environment needs to be activated first.
+Ryven can be launched from the command line by typing `ryven`. If you installed Ryven into a Python virtual environment (or a conda environment), the environment needs to be activated first.
 
 Ryven itself only comes with some small example nodes. You should use Ryven either to develop nodes, or use a third-party nodes package for your use case if there is one. The example nodes are - indeed - just examples, and not stable in any way, so you should not depend on them.
 
-When installed, ryven will create a directory `.ryven` in your user home with the following structure:
+When installed, ryven will create a directory `~/.ryven/` in your user home with the following structure:
 
 ```
 ~/.ryven
@@ -61,9 +59,9 @@ Ryven can be configured in four ways:
 4. using a GUI in the startup dialog
    * you can also convert the manual configuration to cmd line args (or a config file) in the dialog
 
-See `ryven --help` for a list of available options.
+Type `ryven --help` for a list of available options.
 
-To deploy a Ryven project headless, without any GUI, use the `ryven-console` command.
+To deploy a Ryven project headless (without any GUI) use the `ryven-console` command.
 
 <details>
 <summary>Example: headless deployment with REPL access</summary>
@@ -87,8 +85,8 @@ For more information, visit https://leon-thomm.github.io/ryvencore/
 </details>
 
 ## Editor Usage
-
-Quickstart guide:
+<details>
+<summary>quick start guide</summary>
 
 * open Ryven by running `ryven` from the command line
 * you should see the startup dialog
@@ -106,6 +104,8 @@ Quickstart guide:
 * you can pan around also with the right mouse button (hold and drag)
 * and zoom via `ctrl + scroll`
 
+</details>
+
 ## Developing Nodes
 
 <details>
@@ -267,8 +267,6 @@ Please find further resources on the GitHub wiki page in this repository.
 
 ## Features
 
-A (possibly incomplete) list of features:
-
 - **headless mode** to run projects without GUI dependencies at high performance
 - **sophisticated nodes system** allowing for stateful nodes and widgets
 - **cross-platform**; running anywhere where Qt runs (for GUI), or simply Python (headless)
@@ -277,80 +275,9 @@ A (possibly incomplete) list of features:
 - **custom Qt widgets support**
 - various **themes**, including light
 - **right-click operations system for nodes**
-- **variables add-on** with observer mechanism, to build nodes that automatically adapt to change of data
-- very basic **logging support**
-- primitive, very experimental **stylus support** for adding handwritten notes on touch devices
-
-<!--
-
-### Nodes API in action
-
-<table>
-<tr>
-<th>stateful nodes</th>
-<th>exec nodes</th>
-<th>custom Qt widgets</th>
-<th>variables</th>
-<th>logging</th>
-</tr>
-<tr>
-
-<td>
-<sub>
-
-is this text small, yes it is :>>>
-
-```python
-this is a test
-```
-
-</sub>
-</td>
-
-<td>
-<sub>
-
-```python
-asdf
-```
-
-</sub>
-</td>
-
-<td>
-<sub>
-
-```python
-qewr
-```
-
-</sub>
-</td>
-
-<td>
-<sub>
-
-```python
-this is a test
-```
-
-</sub>
-</td>
-
-<td>
-<sub>
-
-```python
-this is a test
-```
-
-</sub>
-</td>
-
-</tr>
-</table>
-
--->
+- **variables system** with observer mechanism, to build nodes that automatically adapt to change of data
+- basic Python **logging support**
+<!-- - primitive, very experimental **stylus support** for adding handwritten notes on touch devices -->
 
 ## License
 
@@ -361,4 +288,4 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
 
 ## Credits
 
-A huge thanks to the contributors. This project does not exist without the open-source community. I personally want to particularly thank the people listed in the CREDITS.md file, which have strongly impacted the project.
\ No newline at end of file
+Contributions are highly appreciated. This project does not exist without the open-source community. I want to particularly thank the people listed in the `CREDITS.md` file.
\ No newline at end of file
diff --git a/docs/node tutorials/README.md b/docs/node tutorials/README.md
deleted file mode 100644
index d668868a..00000000
--- a/docs/node tutorials/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-**TODO: OUTDATED**
-
-This directory hosts some tutorials for nodes built for Ryven to make it easier to get started. Not everything you see here might be 100% up to date with the most recent version. Feel free to extend.
\ No newline at end of file
diff --git a/docs/node tutorials/basic operators.md b/docs/node tutorials/basic operators.md
deleted file mode 100644
index 68a7c6ac..00000000
--- a/docs/node tutorials/basic operators.md
+++ /dev/null
@@ -1,157 +0,0 @@
-This tutorial shows how to create nodes for some basic operations/relations, in particular **arithmetic operations**, **logic operations**, and **value comparisons**. The full nodes package is part of the `std` package, see `ryven/example_nodes/std/basic_operators.py`.
-
-As always, we should first define a parent node class for this scope. It can be completely empty, but it's good practice to always include it as this prevents node identification issues which might occur in complex hierarchies in case you add it later.
-
-```python
-from ryven.node_env import *
-
-
-class NodeBase(Node):
-    version = 'v0.1'
-
-    # most of our operators are binary, so let's already put this here (we can override that in subclasses for the exceptions)
-    init_inputs = [
-        NodeInputBP(dtype=dtypes.Data(size='s')),
-        NodeInputBP(dtype=dtypes.Data(size='s')),
-    ]
-    # operators in python can be defined on objects of different classes, so let's just use Data dtype inputs for now. recall that Data inputs will evaluate input expressions (as opposed to just interpreting it as string for example)
-
-    init_outputs = [
-        NodeOutputBP(),
-    ]
-
-    style = 'small'
-```
-
-Let's leave it like this just for now, we can later extend this class. A simple node definition for addition can now look just like this
-
-```python
-class PlusNode(NodeBase):
-
-    title = '+'
-    version = 'v0.1'
-
-    def update_event(self, inp=-1):
-        # adding the first two inputs and pushing the sum to the output
-        self.set_output_val(0, self.input(0) + self.input(1))
-```
-
-That was easy, no? If we export this node
-
-```python
-export_nodes(
-    PlusNode,
-)
-```
-
-we can see it's working as expected
-
-![](img/plus.png)
-
-We can make all the other nodes exactly the same way. However, there is one improvement that might be really useful, which is a dynamic number of inputs for chaining the operation (`a/b/c` becomes `(a/b)/c`). Notice that so far the implementation above is of *combinational* type, so the node does not have any states. Hence, there's no need to preserve and rebuild the state, which makes it simple. This will change now, as the semantics of the node depends on the number of inputs.
-
-As this is useful for all our nodes, let's put all this into our `NodeBase` class
-
-```python
-class NodeBase(Node):
-
-    version = 'v0.1'
-
-    init_inputs = [
-        NodeInputBP(dtype=dtypes.Data(size='s')),
-        NodeInputBP(dtype=dtypes.Data(size='s')),
-    ]
-
-    init_outputs = [
-        NodeOutputBP(),
-    ]
-
-    style = 'small'
-
-    def __init__(self, params):
-        super().__init__(params)
-
-        self.num_inputs = 2
-
-        # we will use actions (right-click operations) for adding and removing nodes
-        self.actions['add input'] = {'method': self.add_inp}
-
-    def add_inp(self):
-        """appends a new input"""
-        self.create_input_dt(
-            dtype=dtypes.Data(size='s')
-        )
-        self.num_inputs += 1
-
-        # now that some input was added manually, it should be removable again
-        self.actions['remove input'] = {'method': self.rem_inp}
-
-    def rem_inp(self):
-        """removes the last input"""
-        self.delete_input(-1)
-        self.num_inputs -= 1
-
-        # we need at least two inputs
-        if self.num_inputs == 2:
-            del self.actions['remove input']
-
-    # now we need to define storage and reload of the state
-    # the state is internally defined only by the value of self.num_inputs
-    # notice that the actual inputs themselves are stored and reloaded automatically by Ryven,
-    # so we only need to store our own variables
-
-    def get_state(self) -> dict:
-        return {
-            'num inputs': self.num_inputs,
-        }
-
-    def set_state(self, data: dict, version):
-        self.num_inputs = data['num inputs']
-```
-
-To make the implementations less repetitive, let's also abstract away the `update_event`:
-
-```python
-    def update_event(self, inp=-1):
-        self.set_output_val(0, self.apply_op(
-            [self.input(i) for i in range(self.num_inputs)]
-        ))
-
-    def apply_op(self, values: list):
-        pass
-```
-
-Now subclasses only need to implement this `apply_op` method which gets a list of values with variable length. Our `PlusNode` now looks like this:
-
-```python
-
-class PlusNode(NodeBase):
-    """performs addition of all inputs and returns the sum"""
-
-    title = '+'
-    version = 'v0.1'
-
-    def apply_op(self, values: list):
-        res = values[0]
-        for v in values[1:]:
-            res = res + v
-        return res
-```
-
-And that's it. All the other operators can be defined the same way.
-
-```python
-class ANDNode(NodeBase):
-    """logically ANDs all inputs"""
-
-    title = 'AND'
-    version = 'v0.1'
-
-    def apply_op(self, values: list):
-        res = values[0]
-        for v in values[1:]:
-            res = res and v
-        return res
-```
-
-Notice that this implementation is not exactly performant, it rather serves as example for making nodes generally.
diff --git a/docs/node tutorials/doc_examples/custom_widgets/nodes.py b/docs/node tutorials/doc_examples/custom_widgets/nodes.py
deleted file mode 100644
index 5cf8602d..00000000
--- a/docs/node tutorials/doc_examples/custom_widgets/nodes.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from ryven.node_env import *
-widgets = import_guis(__file__)  # this loads all exported widgets from gui.py into the object
-
-
-class MyNode(Node):
-    title = 'my node'
-
-    # this one gets automatically created once for each object
-    main_widget_class = widgets.MyMainWidget
-    main_widget_pos = 'below ports'  # or 'between ports'
-
-    # you can use those for your data inputs
-    input_widget_classes = {
-        'my inp widget': widgets.MyInputWidget
-    }
-    # for example:
-    init_inputs = [
-        NodeInputBP(label='more data', add_data={'widget name': 'my inp widget', 'widget pos': 'below'})
-    ]
-
-    # or:
-    def __init__(self, params):
-        super().__init__(params)
-
-        # note that this happens *before* init_inputs are being built
-        self.create_input(label='some data', add_data={'widget name': 'my inp widget', 'widget pos': 'besides'})
-        # the widget name must match your registered alias/key in the dict above
-        # the widget pos can be 'besides' (the port pin) or 'below' (the port pin)
-
-    def update_event(self, inp=-1):
-        print('I have been updated!!')
-        print(self.input(0), self.input(1))
-
-
-export_nodes(
-    MyNode,
-)
diff --git a/docs/node tutorials/doc_examples/custom_widgets/widgets.py b/docs/node tutorials/doc_examples/custom_widgets/widgets.py
deleted file mode 100644
index 430700c1..00000000
--- a/docs/node tutorials/doc_examples/custom_widgets/widgets.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from ryven.gui_env import *
-
-from qtpy.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QLineEdit
-
-
-class MyMainWidget(MWB, QWidget):
-    def __init__(self, params):
-        MWB.__init__(self, params)
-        QWidget.__init__(self)
-
-        self.setLayout(QVBoxLayout())
-        self.layout().addWidget(QLabel('click it!'))
-        b = QPushButton('click me')
-        b.clicked.connect(self.button_clicked)
-        self.layout().addWidget(b)
-
-    def button_clicked(self):
-        print('I have been clicked!!')
-        self.update_node()
-
-
-class MyInputWidget(IWB, QLineEdit):
-    def __init__(self, params):
-        IWB.__init__(self, params)
-        QLineEdit.__init__(self)
-
-        self.setPlaceholderText('type something...')
-        self.editingFinished.connect(self.update_node)
-
-    # override this from IWB
-    def get_val(self):
-        return self.text()
-
-    # triggered when the input is connected and it received some data
-    def val_update_event(self, val):
-        self.setText(str(val))
-
-
-export_widgets(
-    MyMainWidget,
-    MyInputWidget,
-)
diff --git a/docs/node tutorials/doc_examples/mypackage/nodes.py b/docs/node tutorials/doc_examples/mypackage/nodes.py
deleted file mode 100644
index 2f4bb392..00000000
--- a/docs/node tutorials/doc_examples/mypackage/nodes.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from ryven.node_env import *
-
-
-class NodeBase(Node):
-
-    def __init__(self, params):
-        super().__init__(params)
-
-        # here we could add some stuff for all nodes below...
-        self.actions['asdf'] = {'method': self.asdf}
-
-    def asdf(self):
-        self.create_input_dt(dtype=dtypes.Integer(default=1, bounds=(1, 100)),
-                             label='scale')
-
-
-import random
-
-class Rand_Node(NodeBase):
-    """Generates random float in range"""
-
-    title = 'random'
-    init_inputs = [
-        NodeInputBP(dtype=dtypes.Integer(default=1, bounds=(1, 100)), label='scale'),
-    ]
-    init_outputs = [
-        NodeOutputBP(),
-    ]
-    color = '#aabb44'
-
-    def update_event(self, inp=-1):
-        self.set_output_val(0, round(random.random() * self.input(0), 3))
-
-
-class Print_Node(NodeBase):
-    """Prints your data"""
-
-    title = 'print'
-    init_inputs = [
-        NodeInputBP(dtype=dtypes.Data(size='s')),
-    ]
-    color = '#3355dd'
-
-    def update_event(self, inp=-1):
-        print(self.input(0))
-
-
-export_nodes(
-    Rand_Node,
-    Print_Node,
-)
diff --git a/docs/node tutorials/img/plus.png b/docs/node tutorials/img/plus.png
deleted file mode 100644
index a8b32150..00000000
Binary files a/docs/node tutorials/img/plus.png and /dev/null differ
diff --git a/docs/node tutorials/img/plus2.png b/docs/node tutorials/img/plus2.png
deleted file mode 100644
index ec97cc55..00000000
Binary files a/docs/node tutorials/img/plus2.png and /dev/null differ
diff --git a/MANIFEST.in b/ryven-editor/MANIFEST.in
similarity index 100%
rename from MANIFEST.in
rename to ryven-editor/MANIFEST.in
diff --git a/pyproject.toml b/ryven-editor/pyproject.toml
similarity index 100%
rename from pyproject.toml
rename to ryven-editor/pyproject.toml
diff --git a/rebuild.sh b/ryven-editor/rebuild.sh
similarity index 100%
rename from rebuild.sh
rename to ryven-editor/rebuild.sh
diff --git a/recreate_uic_files.sh b/ryven-editor/recreate_uic_files.sh
similarity index 100%
rename from recreate_uic_files.sh
rename to ryven-editor/recreate_uic_files.sh
diff --git a/ryven/__init__.py b/ryven-editor/ryven/__init__.py
similarity index 100%
rename from ryven/__init__.py
rename to ryven-editor/ryven/__init__.py
diff --git a/ryven/example_nodes/OpenCV/README.md b/ryven-editor/ryven/example_nodes/OpenCV/README.md
similarity index 100%
rename from ryven/example_nodes/OpenCV/README.md
rename to ryven-editor/ryven/example_nodes/OpenCV/README.md
diff --git a/ryven/example_nodes/OpenCV/nodes.py b/ryven-editor/ryven/example_nodes/OpenCV/nodes.py
similarity index 100%
rename from ryven/example_nodes/OpenCV/nodes.py
rename to ryven-editor/ryven/example_nodes/OpenCV/nodes.py
diff --git a/ryven/example_nodes/OpenCV/widgets.py b/ryven-editor/ryven/example_nodes/OpenCV/widgets.py
similarity index 100%
rename from ryven/example_nodes/OpenCV/widgets.py
rename to ryven-editor/ryven/example_nodes/OpenCV/widgets.py
diff --git a/ryven/example_nodes/linalg/README.md b/ryven-editor/ryven/example_nodes/linalg/README.md
similarity index 100%
rename from ryven/example_nodes/linalg/README.md
rename to ryven-editor/ryven/example_nodes/linalg/README.md
diff --git a/ryven/example_nodes/linalg/gui.py b/ryven-editor/ryven/example_nodes/linalg/gui.py
similarity index 100%
rename from ryven/example_nodes/linalg/gui.py
rename to ryven-editor/ryven/example_nodes/linalg/gui.py
diff --git a/ryven/example_nodes/linalg/nodes.py b/ryven-editor/ryven/example_nodes/linalg/nodes.py
similarity index 100%
rename from ryven/example_nodes/linalg/nodes.py
rename to ryven-editor/ryven/example_nodes/linalg/nodes.py
diff --git a/ryven/example_nodes/std/README.md b/ryven-editor/ryven/example_nodes/std/README.md
similarity index 100%
rename from ryven/example_nodes/std/README.md
rename to ryven-editor/ryven/example_nodes/std/README.md
diff --git a/ryven/example_nodes/std/basic_operators.py b/ryven-editor/ryven/example_nodes/std/basic_operators.py
similarity index 100%
rename from ryven/example_nodes/std/basic_operators.py
rename to ryven-editor/ryven/example_nodes/std/basic_operators.py
diff --git a/ryven/example_nodes/std/control_structures.py b/ryven-editor/ryven/example_nodes/std/control_structures.py
similarity index 100%
rename from ryven/example_nodes/std/control_structures.py
rename to ryven-editor/ryven/example_nodes/std/control_structures.py
diff --git a/ryven/example_nodes/std/gui.py b/ryven-editor/ryven/example_nodes/std/gui.py
similarity index 100%
rename from ryven/example_nodes/std/gui.py
rename to ryven-editor/ryven/example_nodes/std/gui.py
diff --git a/ryven/example_nodes/std/nodes.py b/ryven-editor/ryven/example_nodes/std/nodes.py
similarity index 100%
rename from ryven/example_nodes/std/nodes.py
rename to ryven-editor/ryven/example_nodes/std/nodes.py
diff --git a/ryven/example_nodes/std/special_nodes.py b/ryven-editor/ryven/example_nodes/std/special_nodes.py
similarity index 100%
rename from ryven/example_nodes/std/special_nodes.py
rename to ryven-editor/ryven/example_nodes/std/special_nodes.py
diff --git a/ryven/examples_projects/basics.json b/ryven-editor/ryven/examples_projects/basics.json
similarity index 100%
rename from ryven/examples_projects/basics.json
rename to ryven-editor/ryven/examples_projects/basics.json
diff --git a/ryven/examples_projects/matrices.json b/ryven-editor/ryven/examples_projects/matrices.json
similarity index 100%
rename from ryven/examples_projects/matrices.json
rename to ryven-editor/ryven/examples_projects/matrices.json
diff --git a/ryven/gui/__init__.py b/ryven-editor/ryven/gui/__init__.py
similarity index 100%
rename from ryven/gui/__init__.py
rename to ryven-editor/ryven/gui/__init__.py
diff --git a/ryven/gui/code_editor/CodeEditorWidget.py b/ryven-editor/ryven/gui/code_editor/CodeEditorWidget.py
similarity index 100%
rename from ryven/gui/code_editor/CodeEditorWidget.py
rename to ryven-editor/ryven/gui/code_editor/CodeEditorWidget.py
diff --git a/ryven/gui/code_editor/CodePreviewWidget.py b/ryven-editor/ryven/gui/code_editor/CodePreviewWidget.py
similarity index 100%
rename from ryven/gui/code_editor/CodePreviewWidget.py
rename to ryven-editor/ryven/gui/code_editor/CodePreviewWidget.py
diff --git a/ryven/gui/code_editor/EditSrcCodeInfoDialog.py b/ryven-editor/ryven/gui/code_editor/EditSrcCodeInfoDialog.py
similarity index 100%
rename from ryven/gui/code_editor/EditSrcCodeInfoDialog.py
rename to ryven-editor/ryven/gui/code_editor/EditSrcCodeInfoDialog.py
diff --git a/ryven/gui/code_editor/SourceCodeUpdater.py b/ryven-editor/ryven/gui/code_editor/SourceCodeUpdater.py
similarity index 99%
rename from ryven/gui/code_editor/SourceCodeUpdater.py
rename to ryven-editor/ryven/gui/code_editor/SourceCodeUpdater.py
index d84d5cab..a426fe30 100644
--- a/ryven/gui/code_editor/SourceCodeUpdater.py
+++ b/ryven-editor/ryven/gui/code_editor/SourceCodeUpdater.py
@@ -51,9 +51,7 @@ def override_code(obj: object, new_class_src) -> Union[None, Exception]:
             return e
 
 
-    #
     # below is the old implementation, for documentation purposes only; see discussion below
-    #
 
 
     @staticmethod
diff --git a/ryven/gui/code_editor/__init__.py b/ryven-editor/ryven/gui/code_editor/__init__.py
similarity index 100%
rename from ryven/gui/code_editor/__init__.py
rename to ryven-editor/ryven/gui/code_editor/__init__.py
diff --git a/ryven/gui/code_editor/codes_storage.py b/ryven-editor/ryven/gui/code_editor/codes_storage.py
similarity index 100%
rename from ryven/gui/code_editor/codes_storage.py
rename to ryven-editor/ryven/gui/code_editor/codes_storage.py
diff --git a/ryven/gui/code_editor/pygments/__init__.py b/ryven-editor/ryven/gui/code_editor/pygments/__init__.py
similarity index 100%
rename from ryven/gui/code_editor/pygments/__init__.py
rename to ryven-editor/ryven/gui/code_editor/pygments/__init__.py
diff --git a/ryven/gui/code_editor/pygments/dracula.py b/ryven-editor/ryven/gui/code_editor/pygments/dracula.py
similarity index 100%
rename from ryven/gui/code_editor/pygments/dracula.py
rename to ryven-editor/ryven/gui/code_editor/pygments/dracula.py
diff --git a/ryven/gui/code_editor/pygments/light.py b/ryven-editor/ryven/gui/code_editor/pygments/light.py
similarity index 100%
rename from ryven/gui/code_editor/pygments/light.py
rename to ryven-editor/ryven/gui/code_editor/pygments/light.py
diff --git a/ryven/gui/dialogs.py b/ryven-editor/ryven/gui/dialogs.py
similarity index 100%
rename from ryven/gui/dialogs.py
rename to ryven-editor/ryven/gui/dialogs.py
diff --git a/ryven/gui/flow_ui.py b/ryven-editor/ryven/gui/flow_ui.py
similarity index 100%
rename from ryven/gui/flow_ui.py
rename to ryven-editor/ryven/gui/flow_ui.py
diff --git a/ryven/gui/main_console.py b/ryven-editor/ryven/gui/main_console.py
similarity index 100%
rename from ryven/gui/main_console.py
rename to ryven-editor/ryven/gui/main_console.py
diff --git a/ryven/gui/main_window.py b/ryven-editor/ryven/gui/main_window.py
similarity index 99%
rename from ryven/gui/main_window.py
rename to ryven-editor/ryven/gui/main_window.py
index 7376b1c3..b49de3b6 100644
--- a/ryven/gui/main_window.py
+++ b/ryven-editor/ryven/gui/main_window.py
@@ -45,10 +45,8 @@ def __init__(
         self.node_packages = {}  # {Node: str}
         self.flow_UIs = {}
 
-        #
         # Init Session GUI
-        #
-
+        
         self.session_gui = rc.SessionGUI(self)
         self.core_session = self.session_gui.core_session
         if self.config.verbose:
@@ -67,10 +65,8 @@ def __init__(
         self.session_gui.design.set_performance_mode(self.config.performance_mode)
         self.session_gui.design.set_animations_enabled(self.config.animations)
 
-        #
         # Assemble Window UI
-        #
-
+        
         self.setup_ui()
 
         self.flow_view_theme_actions = []
@@ -80,18 +76,14 @@ def __init__(
         self.setWindowIcon(QIcon(abs_path_from_package_dir('resources/pics/Ryven_icon.png')))
         self.ui.flows_tab_widget.removeTab(0)  # remove placeholder tab
 
-        #
         # Configure window-wide Shortcuts
-        #
-
+        
         save_shortcut = QShortcut(QKeySequence.Save, self)
         save_shortcut.activated.connect(self.on_save_project_triggered)
         import_nodes_shortcut = QShortcut(QKeySequence('Ctrl+i'), self)
         import_nodes_shortcut.activated.connect(self.on_import_nodes_triggered)
 
-        #
         # Setup Main Console
-        #
 
         MainConsole.instance.session = self.session_gui
         MainConsole.instance.reset_interpreter()
@@ -106,9 +98,7 @@ def console_ref_monkeypatch(self):
             'console ref': {'method': self.console_ref_monkeypatch}
         }
 
-        #
         # Setup ryvencore Session and load project
-        #
 
         self.import_nodes(path=abs_path_from_package_dir('main/packages/built_in/'))
 
@@ -246,7 +236,7 @@ def load_stylesheet(self, ss):
             self.session_gui.set_stylesheet(ss_content)
             self.setStyleSheet(ss_content)
 
-    # SLOTS
+    # slots
 
     def on_import_nodes_triggered(self):
         file_path = QFileDialog.getOpenFileName(self, 'select nodes file', abs_path_from_ryven_dir('nodes'), 'Python File (*.py)')[0]
@@ -340,7 +330,7 @@ def on_delete_flow_triggered(self):
 
         self.core_session.delete_flow(flow)
 
-    # SESSION
+    # session
 
     def flow_created(self, flow, flow_view):
         flow_widget = FlowUI(self, flow, flow_view)
diff --git a/ryven/gui/startup_dialog/StartupDialog.py b/ryven-editor/ryven/gui/startup_dialog/StartupDialog.py
similarity index 100%
rename from ryven/gui/startup_dialog/StartupDialog.py
rename to ryven-editor/ryven/gui/startup_dialog/StartupDialog.py
diff --git a/ryven/gui/startup_dialog/__init__.py b/ryven-editor/ryven/gui/startup_dialog/__init__.py
similarity index 100%
rename from ryven/gui/startup_dialog/__init__.py
rename to ryven-editor/ryven/gui/startup_dialog/__init__.py
diff --git a/ryven/gui/std_input_widgets.py b/ryven-editor/ryven/gui/std_input_widgets.py
similarity index 100%
rename from ryven/gui/std_input_widgets.py
rename to ryven-editor/ryven/gui/std_input_widgets.py
diff --git a/ryven/gui/styling/__init__.py b/ryven-editor/ryven/gui/styling/__init__.py
similarity index 100%
rename from ryven/gui/styling/__init__.py
rename to ryven-editor/ryven/gui/styling/__init__.py
diff --git a/ryven/gui/styling/design_config.json b/ryven-editor/ryven/gui/styling/design_config.json
similarity index 100%
rename from ryven/gui/styling/design_config.json
rename to ryven-editor/ryven/gui/styling/design_config.json
diff --git a/ryven/gui/styling/window_theme.py b/ryven-editor/ryven/gui/styling/window_theme.py
similarity index 100%
rename from ryven/gui/styling/window_theme.py
rename to ryven-editor/ryven/gui/styling/window_theme.py
diff --git a/ryven/gui/uic/__init__.py b/ryven-editor/ryven/gui/uic/__init__.py
similarity index 100%
rename from ryven/gui/uic/__init__.py
rename to ryven-editor/ryven/gui/uic/__init__.py
diff --git a/ryven/gui/uic/flow.ui b/ryven-editor/ryven/gui/uic/flow.ui
similarity index 100%
rename from ryven/gui/uic/flow.ui
rename to ryven-editor/ryven/gui/uic/flow.ui
diff --git a/ryven/gui/uic/main_window.ui b/ryven-editor/ryven/gui/uic/main_window.ui
similarity index 100%
rename from ryven/gui/uic/main_window.ui
rename to ryven-editor/ryven/gui/uic/main_window.ui
diff --git a/ryven/gui/uic/ui_flow.py b/ryven-editor/ryven/gui/uic/ui_flow.py
similarity index 100%
rename from ryven/gui/uic/ui_flow.py
rename to ryven-editor/ryven/gui/uic/ui_flow.py
diff --git a/ryven/gui/uic/ui_main_window.py b/ryven-editor/ryven/gui/uic/ui_main_window.py
similarity index 100%
rename from ryven/gui/uic/ui_main_window.py
rename to ryven-editor/ryven/gui/uic/ui_main_window.py
diff --git a/ryven/gui_env.py b/ryven-editor/ryven/gui_env.py
similarity index 100%
rename from ryven/gui_env.py
rename to ryven-editor/ryven/gui_env.py
diff --git a/ryven/main/Ryven.py b/ryven-editor/ryven/main/Ryven.py
similarity index 100%
rename from ryven/main/Ryven.py
rename to ryven-editor/ryven/main/Ryven.py
diff --git a/ryven/main/RyvenConsole.py b/ryven-editor/ryven/main/RyvenConsole.py
similarity index 100%
rename from ryven/main/RyvenConsole.py
rename to ryven-editor/ryven/main/RyvenConsole.py
diff --git a/ryven/main/__init__.py b/ryven-editor/ryven/main/__init__.py
similarity index 100%
rename from ryven/main/__init__.py
rename to ryven-editor/ryven/main/__init__.py
diff --git a/ryven/main/args_parser.py b/ryven-editor/ryven/main/args_parser.py
similarity index 100%
rename from ryven/main/args_parser.py
rename to ryven-editor/ryven/main/args_parser.py
diff --git a/ryven/main/config.py b/ryven-editor/ryven/main/config.py
similarity index 100%
rename from ryven/main/config.py
rename to ryven-editor/ryven/main/config.py
diff --git a/ryven/main/packages/__init__.py b/ryven-editor/ryven/main/packages/__init__.py
similarity index 100%
rename from ryven/main/packages/__init__.py
rename to ryven-editor/ryven/main/packages/__init__.py
diff --git a/ryven/main/packages/built_in/__init__.py b/ryven-editor/ryven/main/packages/built_in/__init__.py
similarity index 100%
rename from ryven/main/packages/built_in/__init__.py
rename to ryven-editor/ryven/main/packages/built_in/__init__.py
diff --git a/ryven/main/packages/built_in/gui.py b/ryven-editor/ryven/main/packages/built_in/gui.py
similarity index 100%
rename from ryven/main/packages/built_in/gui.py
rename to ryven-editor/ryven/main/packages/built_in/gui.py
diff --git a/ryven/main/packages/built_in/nodes.py b/ryven-editor/ryven/main/packages/built_in/nodes.py
similarity index 100%
rename from ryven/main/packages/built_in/nodes.py
rename to ryven-editor/ryven/main/packages/built_in/nodes.py
diff --git a/ryven/main/packages/gui_env.py b/ryven-editor/ryven/main/packages/gui_env.py
similarity index 100%
rename from ryven/main/packages/gui_env.py
rename to ryven-editor/ryven/main/packages/gui_env.py
diff --git a/ryven/main/packages/node_env.py b/ryven-editor/ryven/main/packages/node_env.py
similarity index 100%
rename from ryven/main/packages/node_env.py
rename to ryven-editor/ryven/main/packages/node_env.py
diff --git a/ryven/main/packages/nodes_package.py b/ryven-editor/ryven/main/packages/nodes_package.py
similarity index 100%
rename from ryven/main/packages/nodes_package.py
rename to ryven-editor/ryven/main/packages/nodes_package.py
diff --git a/ryven/main/utils.py b/ryven-editor/ryven/main/utils.py
similarity index 100%
rename from ryven/main/utils.py
rename to ryven-editor/ryven/main/utils.py
diff --git a/ryven/node_env.py b/ryven-editor/ryven/node_env.py
similarity index 100%
rename from ryven/node_env.py
rename to ryven-editor/ryven/node_env.py
diff --git a/ryven/resources/fonts/__init__.py b/ryven-editor/ryven/resources/fonts/__init__.py
similarity index 100%
rename from ryven/resources/fonts/__init__.py
rename to ryven-editor/ryven/resources/fonts/__init__.py
diff --git a/ryven/resources/fonts/asap/Asap-Bold.ttf b/ryven-editor/ryven/resources/fonts/asap/Asap-Bold.ttf
similarity index 100%
rename from ryven/resources/fonts/asap/Asap-Bold.ttf
rename to ryven-editor/ryven/resources/fonts/asap/Asap-Bold.ttf
diff --git a/ryven/resources/fonts/asap/Asap-BoldItalic.ttf b/ryven-editor/ryven/resources/fonts/asap/Asap-BoldItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/asap/Asap-BoldItalic.ttf
rename to ryven-editor/ryven/resources/fonts/asap/Asap-BoldItalic.ttf
diff --git a/ryven/resources/fonts/asap/Asap-Italic.ttf b/ryven-editor/ryven/resources/fonts/asap/Asap-Italic.ttf
similarity index 100%
rename from ryven/resources/fonts/asap/Asap-Italic.ttf
rename to ryven-editor/ryven/resources/fonts/asap/Asap-Italic.ttf
diff --git a/ryven/resources/fonts/asap/Asap-Medium.ttf b/ryven-editor/ryven/resources/fonts/asap/Asap-Medium.ttf
similarity index 100%
rename from ryven/resources/fonts/asap/Asap-Medium.ttf
rename to ryven-editor/ryven/resources/fonts/asap/Asap-Medium.ttf
diff --git a/ryven/resources/fonts/asap/Asap-MediumItalic.ttf b/ryven-editor/ryven/resources/fonts/asap/Asap-MediumItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/asap/Asap-MediumItalic.ttf
rename to ryven-editor/ryven/resources/fonts/asap/Asap-MediumItalic.ttf
diff --git a/ryven/resources/fonts/asap/Asap-Regular.ttf b/ryven-editor/ryven/resources/fonts/asap/Asap-Regular.ttf
similarity index 100%
rename from ryven/resources/fonts/asap/Asap-Regular.ttf
rename to ryven-editor/ryven/resources/fonts/asap/Asap-Regular.ttf
diff --git a/ryven/resources/fonts/asap/Asap-SemiBold.ttf b/ryven-editor/ryven/resources/fonts/asap/Asap-SemiBold.ttf
similarity index 100%
rename from ryven/resources/fonts/asap/Asap-SemiBold.ttf
rename to ryven-editor/ryven/resources/fonts/asap/Asap-SemiBold.ttf
diff --git a/ryven/resources/fonts/asap/Asap-SemiBoldItalic.ttf b/ryven-editor/ryven/resources/fonts/asap/Asap-SemiBoldItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/asap/Asap-SemiBoldItalic.ttf
rename to ryven-editor/ryven/resources/fonts/asap/Asap-SemiBoldItalic.ttf
diff --git a/ryven/resources/fonts/asap/OFL.txt b/ryven-editor/ryven/resources/fonts/asap/OFL.txt
similarity index 100%
rename from ryven/resources/fonts/asap/OFL.txt
rename to ryven-editor/ryven/resources/fonts/asap/OFL.txt
diff --git a/ryven/resources/fonts/asap/__init__.py b/ryven-editor/ryven/resources/fonts/asap/__init__.py
similarity index 100%
rename from ryven/resources/fonts/asap/__init__.py
rename to ryven-editor/ryven/resources/fonts/asap/__init__.py
diff --git a/ryven/resources/fonts/poppins/OFL.txt b/ryven-editor/ryven/resources/fonts/poppins/OFL.txt
similarity index 100%
rename from ryven/resources/fonts/poppins/OFL.txt
rename to ryven-editor/ryven/resources/fonts/poppins/OFL.txt
diff --git a/ryven/resources/fonts/poppins/Poppins-Black.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-Black.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-Black.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-Black.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-BlackItalic.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-BlackItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-BlackItalic.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-BlackItalic.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-Bold.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-Bold.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-Bold.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-Bold.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-BoldItalic.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-BoldItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-BoldItalic.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-BoldItalic.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-ExtraBold.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-ExtraBold.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-ExtraBold.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-ExtraBold.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-ExtraBoldItalic.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-ExtraBoldItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-ExtraBoldItalic.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-ExtraBoldItalic.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-ExtraLight.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-ExtraLight.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-ExtraLight.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-ExtraLight.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-ExtraLightItalic.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-ExtraLightItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-ExtraLightItalic.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-ExtraLightItalic.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-Italic.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-Italic.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-Italic.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-Italic.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-Light.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-Light.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-Light.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-Light.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-LightItalic.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-LightItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-LightItalic.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-LightItalic.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-Medium.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-Medium.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-Medium.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-Medium.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-MediumItalic.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-MediumItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-MediumItalic.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-MediumItalic.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-Regular.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-Regular.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-Regular.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-Regular.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-SemiBold.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-SemiBold.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-SemiBold.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-SemiBold.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-SemiBoldItalic.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-SemiBoldItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-SemiBoldItalic.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-SemiBoldItalic.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-Thin.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-Thin.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-Thin.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-Thin.ttf
diff --git a/ryven/resources/fonts/poppins/Poppins-ThinItalic.ttf b/ryven-editor/ryven/resources/fonts/poppins/Poppins-ThinItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/poppins/Poppins-ThinItalic.ttf
rename to ryven-editor/ryven/resources/fonts/poppins/Poppins-ThinItalic.ttf
diff --git a/ryven/resources/fonts/poppins/__init__.py b/ryven-editor/ryven/resources/fonts/poppins/__init__.py
similarity index 100%
rename from ryven/resources/fonts/poppins/__init__.py
rename to ryven-editor/ryven/resources/fonts/poppins/__init__.py
diff --git a/ryven/resources/fonts/source_code_pro/OFL.txt b/ryven-editor/ryven/resources/fonts/source_code_pro/OFL.txt
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/OFL.txt
rename to ryven-editor/ryven/resources/fonts/source_code_pro/OFL.txt
diff --git a/ryven/resources/fonts/source_code_pro/SourceCodePro-Black.ttf b/ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-Black.ttf
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/SourceCodePro-Black.ttf
rename to ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-Black.ttf
diff --git a/ryven/resources/fonts/source_code_pro/SourceCodePro-BlackItalic.ttf b/ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-BlackItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/SourceCodePro-BlackItalic.ttf
rename to ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-BlackItalic.ttf
diff --git a/ryven/resources/fonts/source_code_pro/SourceCodePro-Bold.ttf b/ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-Bold.ttf
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/SourceCodePro-Bold.ttf
rename to ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-Bold.ttf
diff --git a/ryven/resources/fonts/source_code_pro/SourceCodePro-BoldItalic.ttf b/ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-BoldItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/SourceCodePro-BoldItalic.ttf
rename to ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-BoldItalic.ttf
diff --git a/ryven/resources/fonts/source_code_pro/SourceCodePro-ExtraLight.ttf b/ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-ExtraLight.ttf
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/SourceCodePro-ExtraLight.ttf
rename to ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-ExtraLight.ttf
diff --git a/ryven/resources/fonts/source_code_pro/SourceCodePro-ExtraLightItalic.ttf b/ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-ExtraLightItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/SourceCodePro-ExtraLightItalic.ttf
rename to ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-ExtraLightItalic.ttf
diff --git a/ryven/resources/fonts/source_code_pro/SourceCodePro-Italic.ttf b/ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-Italic.ttf
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/SourceCodePro-Italic.ttf
rename to ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-Italic.ttf
diff --git a/ryven/resources/fonts/source_code_pro/SourceCodePro-Light.ttf b/ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-Light.ttf
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/SourceCodePro-Light.ttf
rename to ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-Light.ttf
diff --git a/ryven/resources/fonts/source_code_pro/SourceCodePro-LightItalic.ttf b/ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-LightItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/SourceCodePro-LightItalic.ttf
rename to ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-LightItalic.ttf
diff --git a/ryven/resources/fonts/source_code_pro/SourceCodePro-Medium.ttf b/ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-Medium.ttf
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/SourceCodePro-Medium.ttf
rename to ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-Medium.ttf
diff --git a/ryven/resources/fonts/source_code_pro/SourceCodePro-MediumItalic.ttf b/ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-MediumItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/SourceCodePro-MediumItalic.ttf
rename to ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-MediumItalic.ttf
diff --git a/ryven/resources/fonts/source_code_pro/SourceCodePro-Regular.ttf b/ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-Regular.ttf
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/SourceCodePro-Regular.ttf
rename to ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-Regular.ttf
diff --git a/ryven/resources/fonts/source_code_pro/SourceCodePro-SemiBold.ttf b/ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-SemiBold.ttf
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/SourceCodePro-SemiBold.ttf
rename to ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-SemiBold.ttf
diff --git a/ryven/resources/fonts/source_code_pro/SourceCodePro-SemiBoldItalic.ttf b/ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-SemiBoldItalic.ttf
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/SourceCodePro-SemiBoldItalic.ttf
rename to ryven-editor/ryven/resources/fonts/source_code_pro/SourceCodePro-SemiBoldItalic.ttf
diff --git a/ryven/resources/fonts/source_code_pro/__init__.py b/ryven-editor/ryven/resources/fonts/source_code_pro/__init__.py
similarity index 100%
rename from ryven/resources/fonts/source_code_pro/__init__.py
rename to ryven-editor/ryven/resources/fonts/source_code_pro/__init__.py
diff --git a/ryven/resources/pics/Ryven_icon.jpg b/ryven-editor/ryven/resources/pics/Ryven_icon.jpg
similarity index 100%
rename from ryven/resources/pics/Ryven_icon.jpg
rename to ryven-editor/ryven/resources/pics/Ryven_icon.jpg
diff --git a/ryven/resources/pics/Ryven_icon.png b/ryven-editor/ryven/resources/pics/Ryven_icon.png
similarity index 100%
rename from ryven/resources/pics/Ryven_icon.png
rename to ryven-editor/ryven/resources/pics/Ryven_icon.png
diff --git a/ryven/resources/pics/Ryven_icon_blurred.png b/ryven-editor/ryven/resources/pics/Ryven_icon_blurred.png
similarity index 100%
rename from ryven/resources/pics/Ryven_icon_blurred.png
rename to ryven-editor/ryven/resources/pics/Ryven_icon_blurred.png
diff --git a/ryven/resources/pics/logo.png b/ryven-editor/ryven/resources/pics/logo.png
similarity index 100%
rename from ryven/resources/pics/logo.png
rename to ryven-editor/ryven/resources/pics/logo.png
diff --git a/ryven/resources/stylesheets/icons/branch-closed.svg b/ryven-editor/ryven/resources/stylesheets/icons/branch-closed.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/branch-closed.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/branch-closed.svg
diff --git a/ryven/resources/stylesheets/icons/branch-end.svg b/ryven-editor/ryven/resources/stylesheets/icons/branch-end.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/branch-end.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/branch-end.svg
diff --git a/ryven/resources/stylesheets/icons/branch-more.svg b/ryven-editor/ryven/resources/stylesheets/icons/branch-more.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/branch-more.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/branch-more.svg
diff --git a/ryven/resources/stylesheets/icons/branch-open.svg b/ryven-editor/ryven/resources/stylesheets/icons/branch-open.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/branch-open.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/branch-open.svg
diff --git a/ryven/resources/stylesheets/icons/checkbox_checked.svg b/ryven-editor/ryven/resources/stylesheets/icons/checkbox_checked.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/checkbox_checked.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/checkbox_checked.svg
diff --git a/ryven/resources/stylesheets/icons/checkbox_indeterminate.svg b/ryven-editor/ryven/resources/stylesheets/icons/checkbox_indeterminate.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/checkbox_indeterminate.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/checkbox_indeterminate.svg
diff --git a/ryven/resources/stylesheets/icons/checkbox_unchecked.svg b/ryven-editor/ryven/resources/stylesheets/icons/checkbox_unchecked.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/checkbox_unchecked.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/checkbox_unchecked.svg
diff --git a/ryven/resources/stylesheets/icons/close.svg b/ryven-editor/ryven/resources/stylesheets/icons/close.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/close.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/close.svg
diff --git a/ryven/resources/stylesheets/icons/downarrow.svg b/ryven-editor/ryven/resources/stylesheets/icons/downarrow.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/downarrow.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/downarrow.svg
diff --git a/ryven/resources/stylesheets/icons/downarrow2.svg b/ryven-editor/ryven/resources/stylesheets/icons/downarrow2.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/downarrow2.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/downarrow2.svg
diff --git a/ryven/resources/stylesheets/icons/float.svg b/ryven-editor/ryven/resources/stylesheets/icons/float.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/float.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/float.svg
diff --git a/ryven/resources/stylesheets/icons/leftarrow.svg b/ryven-editor/ryven/resources/stylesheets/icons/leftarrow.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/leftarrow.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/leftarrow.svg
diff --git a/ryven/resources/stylesheets/icons/leftarrow2.svg b/ryven-editor/ryven/resources/stylesheets/icons/leftarrow2.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/leftarrow2.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/leftarrow2.svg
diff --git a/ryven/resources/stylesheets/icons/orig/base.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/base.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/base.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/base.svg
diff --git a/ryven/resources/stylesheets/icons/orig/branch-closed.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/branch-closed.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/branch-closed.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/branch-closed.svg
diff --git a/ryven/resources/stylesheets/icons/orig/branch-end.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/branch-end.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/branch-end.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/branch-end.svg
diff --git a/ryven/resources/stylesheets/icons/orig/branch-more.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/branch-more.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/branch-more.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/branch-more.svg
diff --git a/ryven/resources/stylesheets/icons/orig/branch-open.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/branch-open.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/branch-open.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/branch-open.svg
diff --git a/ryven/resources/stylesheets/icons/orig/checkbox_checked.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/checkbox_checked.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/checkbox_checked.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/checkbox_checked.svg
diff --git a/ryven/resources/stylesheets/icons/orig/checkbox_indeterminate.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/checkbox_indeterminate.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/checkbox_indeterminate.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/checkbox_indeterminate.svg
diff --git a/ryven/resources/stylesheets/icons/orig/checkbox_unchecked.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/checkbox_unchecked.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/checkbox_unchecked.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/checkbox_unchecked.svg
diff --git a/ryven/resources/stylesheets/icons/orig/close.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/close.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/close.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/close.svg
diff --git a/ryven/resources/stylesheets/icons/orig/downarrow.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/downarrow.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/downarrow.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/downarrow.svg
diff --git a/ryven/resources/stylesheets/icons/orig/downarrow2.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/downarrow2.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/downarrow2.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/downarrow2.svg
diff --git a/ryven/resources/stylesheets/icons/orig/float.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/float.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/float.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/float.svg
diff --git a/ryven/resources/stylesheets/icons/orig/leftarrow.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/leftarrow.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/leftarrow.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/leftarrow.svg
diff --git a/ryven/resources/stylesheets/icons/orig/leftarrow2.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/leftarrow2.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/leftarrow2.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/leftarrow2.svg
diff --git a/ryven/resources/stylesheets/icons/orig/radiobutton_checked.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/radiobutton_checked.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/radiobutton_checked.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/radiobutton_checked.svg
diff --git a/ryven/resources/stylesheets/icons/orig/radiobutton_unchecked.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/radiobutton_unchecked.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/radiobutton_unchecked.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/radiobutton_unchecked.svg
diff --git a/ryven/resources/stylesheets/icons/orig/rightarrow.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/rightarrow.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/rightarrow.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/rightarrow.svg
diff --git a/ryven/resources/stylesheets/icons/orig/rightarrow2.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/rightarrow2.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/rightarrow2.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/rightarrow2.svg
diff --git a/ryven/resources/stylesheets/icons/orig/sizegrip.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/sizegrip.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/sizegrip.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/sizegrip.svg
diff --git a/ryven/resources/stylesheets/icons/orig/slider.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/slider.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/slider.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/slider.svg
diff --git a/ryven/resources/stylesheets/icons/orig/splitter-horizontal.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/splitter-horizontal.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/splitter-horizontal.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/splitter-horizontal.svg
diff --git a/ryven/resources/stylesheets/icons/orig/splitter-vertical.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/splitter-vertical.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/splitter-vertical.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/splitter-vertical.svg
diff --git a/ryven/resources/stylesheets/icons/orig/tab_close.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/tab_close.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/tab_close.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/tab_close.svg
diff --git a/ryven/resources/stylesheets/icons/orig/toolbar-handle-horizontal.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/toolbar-handle-horizontal.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/toolbar-handle-horizontal.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/toolbar-handle-horizontal.svg
diff --git a/ryven/resources/stylesheets/icons/orig/toolbar-handle-vertical.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/toolbar-handle-vertical.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/toolbar-handle-vertical.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/toolbar-handle-vertical.svg
diff --git a/ryven/resources/stylesheets/icons/orig/uparrow.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/uparrow.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/uparrow.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/uparrow.svg
diff --git a/ryven/resources/stylesheets/icons/orig/uparrow2.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/uparrow2.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/uparrow2.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/uparrow2.svg
diff --git a/ryven/resources/stylesheets/icons/orig/vline.svg b/ryven-editor/ryven/resources/stylesheets/icons/orig/vline.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/orig/vline.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/orig/vline.svg
diff --git a/ryven/resources/stylesheets/icons/radiobutton_checked.svg b/ryven-editor/ryven/resources/stylesheets/icons/radiobutton_checked.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/radiobutton_checked.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/radiobutton_checked.svg
diff --git a/ryven/resources/stylesheets/icons/radiobutton_unchecked.svg b/ryven-editor/ryven/resources/stylesheets/icons/radiobutton_unchecked.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/radiobutton_unchecked.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/radiobutton_unchecked.svg
diff --git a/ryven/resources/stylesheets/icons/rightarrow.svg b/ryven-editor/ryven/resources/stylesheets/icons/rightarrow.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/rightarrow.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/rightarrow.svg
diff --git a/ryven/resources/stylesheets/icons/rightarrow2.svg b/ryven-editor/ryven/resources/stylesheets/icons/rightarrow2.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/rightarrow2.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/rightarrow2.svg
diff --git a/ryven/resources/stylesheets/icons/sizegrip.svg b/ryven-editor/ryven/resources/stylesheets/icons/sizegrip.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/sizegrip.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/sizegrip.svg
diff --git a/ryven/resources/stylesheets/icons/slider.svg b/ryven-editor/ryven/resources/stylesheets/icons/slider.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/slider.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/slider.svg
diff --git a/ryven/resources/stylesheets/icons/splitter-horizontal.svg b/ryven-editor/ryven/resources/stylesheets/icons/splitter-horizontal.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/splitter-horizontal.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/splitter-horizontal.svg
diff --git a/ryven/resources/stylesheets/icons/splitter-vertical.svg b/ryven-editor/ryven/resources/stylesheets/icons/splitter-vertical.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/splitter-vertical.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/splitter-vertical.svg
diff --git a/ryven/resources/stylesheets/icons/tab_close.svg b/ryven-editor/ryven/resources/stylesheets/icons/tab_close.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/tab_close.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/tab_close.svg
diff --git a/ryven/resources/stylesheets/icons/toolbar-handle-horizontal.svg b/ryven-editor/ryven/resources/stylesheets/icons/toolbar-handle-horizontal.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/toolbar-handle-horizontal.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/toolbar-handle-horizontal.svg
diff --git a/ryven/resources/stylesheets/icons/toolbar-handle-vertical.svg b/ryven-editor/ryven/resources/stylesheets/icons/toolbar-handle-vertical.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/toolbar-handle-vertical.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/toolbar-handle-vertical.svg
diff --git a/ryven/resources/stylesheets/icons/uparrow.svg b/ryven-editor/ryven/resources/stylesheets/icons/uparrow.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/uparrow.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/uparrow.svg
diff --git a/ryven/resources/stylesheets/icons/uparrow2.svg b/ryven-editor/ryven/resources/stylesheets/icons/uparrow2.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/uparrow2.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/uparrow2.svg
diff --git a/ryven/resources/stylesheets/icons/vline.svg b/ryven-editor/ryven/resources/stylesheets/icons/vline.svg
similarity index 100%
rename from ryven/resources/stylesheets/icons/vline.svg
rename to ryven-editor/ryven/resources/stylesheets/icons/vline.svg
diff --git a/ryven/resources/stylesheets/style_template.css b/ryven-editor/ryven/resources/stylesheets/style_template.css
similarity index 100%
rename from ryven/resources/stylesheets/style_template.css
rename to ryven-editor/ryven/resources/stylesheets/style_template.css
diff --git a/ryven/unused/EditVal_Dialog.py b/ryven-editor/ryven/unused/EditVal_Dialog.py
similarity index 100%
rename from ryven/unused/EditVal_Dialog.py
rename to ryven-editor/ryven/unused/EditVal_Dialog.py
diff --git a/ryven/unused/NodeDetailsWidget.py b/ryven-editor/ryven/unused/NodeDetailsWidget.py
similarity index 100%
rename from ryven/unused/NodeDetailsWidget.py
rename to ryven-editor/ryven/unused/NodeDetailsWidget.py
diff --git a/ryven/unused/NodesTreeListWidget.py b/ryven-editor/ryven/unused/NodesTreeListWidget.py
similarity index 100%
rename from ryven/unused/NodesTreeListWidget.py
rename to ryven-editor/ryven/unused/NodesTreeListWidget.py
diff --git a/setup.cfg b/ryven-editor/setup.cfg
similarity index 100%
rename from setup.cfg
rename to ryven-editor/setup.cfg
diff --git a/setup.py b/ryven-editor/setup.py
similarity index 100%
rename from setup.py
rename to ryven-editor/setup.py
diff --git a/ryven/examples_projects/basics_OLD.json b/ryven/examples_projects/basics_OLD.json
deleted file mode 100644
index d658ca84..00000000
--- a/ryven/examples_projects/basics_OLD.json
+++ /dev/null
@@ -1,647 +0,0 @@
-{
-    "general info": {
-        "type": "Ryven project file"
-    },
-    "required packages": [
-        {
-            "name": "std",
-            "dir": "C:\\Users\\nutri\\projects\\ryven projects\\Ryven_mmorys\\Ryven\\ryven\\example_nodes\\std"
-        }
-    ],
-    "scripts": [
-        {
-            "title": "hello world",
-            "variables": {
-                "a": {
-                    "serialized": "gASVMQEAAAAAAABdlChN2wtN3AtN3QtN3gtN3wtN4AtN4QtN4gtN4wtN5AtN5QtN5gtN5wtN6AtN6QtN6gtN6wtN7AtN7QtN7gtN7wtN8AtN8QtN8gtN8wtN9AtN9QtN9gtN9wtN+AtN+QtN+gtN+wtN/AtN/QtN/gtN/wtNAAxNAQxNAgxNAwxNBAxNBQxNBgxNBwxNCAxNCQxNCgxNCwxNDAxNDQxNDgxNDwxNEAxNEQxNEgxNEwxNFAxNFQxNFgxNFwxNGAxNGQxNGgxNGwxNHAxNHQxNHgxNHwxNIAxNIQxNIgxNIwxNJAxNJQxNJgxNJwxNKAxNKQxNKgxNKwxNLAxNLQxNLgxNLwxNMAxNMQxNMgxNMwxNNAxNNQxNNgxNNwxNOAxNOQxNOgxNOwxNPAxNPQxNPgxlLg=="
-                },
-                "ctr": {
-                    "serialized": "gASVBAAAAAAAAABNPgwu"
-                }
-            },
-            "flow": {
-                "algorithm mode": "data",
-                "nodes": [
-                    {
-                        "identifier": "std.Storage_Node",
-                        "version": null,
-                        "state data": "gASVOwEAAAAAAAB9lIwEZGF0YZRdlChN2wtN3AtN3QtN3gtN3wtN4AtN4QtN4gtN4wtN5AtN5QtN5gtN5wtN6AtN6QtN6gtN6wtN7AtN7QtN7gtN7wtN8AtN8QtN8gtN8wtN9AtN9QtN9gtN9wtN+AtN+QtN+gtN+wtN/AtN/QtN/gtN/wtNAAxNAQxNAgxNAwxNBAxNBQxNBgxNBwxNCAxNCQxNCgxNCwxNDAxNDQxNDgxNDwxNEAxNEQxNEgxNEwxNFAxNFQxNFgxNFwxNGAxNGQxNGgxNGwxNHAxNHQxNHgxNHwxNIAxNIQxNIgxNIwxNJAxNJQxNJgxNJwxNKAxNKQxNKgxNKwxNLAxNLQxNLgxNLwxNMAxNMQxNMgxNMwxNNAxNNQxNNgxNNwxNOAxNOQxNOgxNOwxNPAxNPQxNPgxlcy4=",
-                        "additional data": {
-                            "special actions": {
-                                "clear": {
-                                    "method": "clear"
-                                }
-                            },
-                            "display title": "store"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 6,
-                                "has widget": false
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 7
-                            }
-                        ],
-                        "GID": 5,
-                        "pos x": 475.4795814751692,
-                        "pos y": 264.55248052403067,
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "std.Eval_Node",
-                        "version": null,
-                        "state data": "gASVOQAAAAAAAAB9lCiMEG51bSBwYXJhbSBpbnB1dHOUSwGMD2V4cHJlc3Npb24gY29kZZSMCmlucFswXVstMV2UdS4=",
-                        "additional data": {
-                            "special actions": {
-                                "add input": {
-                                    "method": "add_param_input"
-                                },
-                                "remove input 0": {
-                                    "method": "remove_param_input",
-                                    "data": 0
-                                }
-                            },
-                            "display title": "eval"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 9,
-                                "has widget": false
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 10
-                            }
-                        ],
-                        "GID": 8,
-                        "pos x": 462.86363383615935,
-                        "pos y": 370.17323807221123,
-                        "main widget data": "gASVGAAAAAAAAAB9lIwEdGV4dJSMCmlucFswXVstMV2Ucy4=",
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "built_in.Result_Node",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {
-                            "special actions": {},
-                            "display title": "result"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 12,
-                                "has widget": false
-                            }
-                        ],
-                        "outputs": [],
-                        "GID": 11,
-                        "pos x": 796.5680046166863,
-                        "pos y": 369.6301042263301,
-                        "main widget data": "gAR9lC4=",
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "std.Slider_Node",
-                        "version": null,
-                        "state data": "gASVEwAAAAAAAAB9lIwDdmFslEc/4Q5WBBiTdXMu",
-                        "additional data": {
-                            "special actions": {},
-                            "display title": "slider"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "scl",
-                                "GID": 14,
-                                "val": "gARLAi4=",
-                                "dtype": "DType.Integer",
-                                "dtype state": "gASVLAAAAAAAAAB9lCiMB2RlZmF1bHSUSwGMA3ZhbJRLAIwDZG9jlIwAlIwGYm91bmRzlE51Lg==",
-                                "has widget": true,
-                                "widget data": "gASVDAAAAAAAAAB9lIwDdmFslEsCcy4="
-                            },
-                            {
-                                "type": "data",
-                                "label": "round",
-                                "GID": 15,
-                                "val": "gAROLg==",
-                                "dtype": "DType.Boolean",
-                                "dtype state": "gASVKgAAAAAAAAB9lCiMB2RlZmF1bHSUiYwDdmFslImMA2RvY5SMAJSMBmJvdW5kc5ROdS4=",
-                                "has widget": true,
-                                "widget data": "gASVDwAAAAAAAAB9lIwHY2hlY2tlZJSJcy4="
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 16
-                            }
-                        ],
-                        "GID": 13,
-                        "pos x": 273.19774517706185,
-                        "pos y": 503.4372312306748,
-                        "main widget data": "gASVDQAAAAAAAAB9lIwDdmFslE0VAnMu",
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "built_in.Result_Node",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {
-                            "special actions": {},
-                            "display title": "result"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 18,
-                                "has widget": false
-                            }
-                        ],
-                        "outputs": [],
-                        "GID": 17,
-                        "pos x": 571.2515010942456,
-                        "pos y": 555.0662156323955,
-                        "main widget data": "gAR9lC4=",
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "built_in.SetVar_Node",
-                        "version": null,
-                        "state data": "gASVDgAAAAAAAAB9lIwGYWN0aXZllIlzLg==",
-                        "additional data": {
-                            "special actions": {
-                                "make active": {
-                                    "method": "action_make_active"
-                                }
-                            },
-                            "display title": "set var"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "var",
-                                "GID": 20,
-                                "val": "gASVBQAAAAAAAACMAWGULg==",
-                                "dtype": "DType.String",
-                                "dtype state": "gASVNwAAAAAAAAB9lCiMB2RlZmF1bHSUjACUjAN2YWyUaAKMA2RvY5RoAowGYm91bmRzlE6MBHNpemWUjAFtlHUu",
-                                "has widget": true,
-                                "widget data": "gASVDwAAAAAAAAB9lIwEdGV4dJSMAWGUcy4="
-                            },
-                            {
-                                "type": "data",
-                                "label": "val",
-                                "GID": 21,
-                                "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBc5R1Lg==",
-                                "has widget": true,
-                                "widget data": "gASVaQIAAAAAAAB9lIwEdGV4dJRYWAIAAFszMDM1LCAzMDM2LCAzMDM3LCAzMDM4LCAzMDM5LCAzMDQwLCAzMDQxLCAzMDQyLCAzMDQzLCAzMDQ0LCAzMDQ1LCAzMDQ2LCAzMDQ3LCAzMDQ4LCAzMDQ5LCAzMDUwLCAzMDUxLCAzMDUyLCAzMDUzLCAzMDU0LCAzMDU1LCAzMDU2LCAzMDU3LCAzMDU4LCAzMDU5LCAzMDYwLCAzMDYxLCAzMDYyLCAzMDYzLCAzMDY0LCAzMDY1LCAzMDY2LCAzMDY3LCAzMDY4LCAzMDY5LCAzMDcwLCAzMDcxLCAzMDcyLCAzMDczLCAzMDc0LCAzMDc1LCAzMDc2LCAzMDc3LCAzMDc4LCAzMDc5LCAzMDgwLCAzMDgxLCAzMDgyLCAzMDgzLCAzMDg0LCAzMDg1LCAzMDg2LCAzMDg3LCAzMDg4LCAzMDg5LCAzMDkwLCAzMDkxLCAzMDkyLCAzMDkzLCAzMDk0LCAzMDk1LCAzMDk2LCAzMDk3LCAzMDk4LCAzMDk5LCAzMTAwLCAzMTAxLCAzMTAyLCAzMTAzLCAzMTA0LCAzMTA1LCAzMTA2LCAzMTA3LCAzMTA4LCAzMTA5LCAzMTEwLCAzMTExLCAzMTEyLCAzMTEzLCAzMTE0LCAzMTE1LCAzMTE2LCAzMTE3LCAzMTE4LCAzMTE5LCAzMTIwLCAzMTIxLCAzMTIyLCAzMTIzLCAzMTI0LCAzMTI1LCAzMTI2LCAzMTI3LCAzMTI4LCAzMTI5LCAzMTMwLCAzMTMxLCAzMTMyLCAzMTMzLCAzMTM0XZRzLg=="
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "val",
-                                "GID": 22
-                            }
-                        ],
-                        "GID": 19,
-                        "pos x": 724.2936205842011,
-                        "pos y": 249.9898748171978,
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "built_in.GetVar_Node",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {
-                            "special actions": {},
-                            "display title": "get var"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 24,
-                                "val": "gASVBQAAAAAAAACMAWGULg==",
-                                "dtype": "DType.String",
-                                "dtype state": "gASVNwAAAAAAAAB9lCiMB2RlZmF1bHSUjACUjAN2YWyUaAKMA2RvY5RoAowGYm91bmRzlE6MBHNpemWUjAFtlHUu",
-                                "has widget": true,
-                                "widget data": "gASVDwAAAAAAAAB9lIwEdGV4dJSMAWGUcy4="
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "val",
-                                "GID": 25
-                            }
-                        ],
-                        "GID": 23,
-                        "pos x": 164.80323966981427,
-                        "pos y": 368.23084671814206,
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "std.Clock_Node",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {
-                            "special actions": {
-                                "start": {
-                                    "method": "start"
-                                },
-                                "stop": {
-                                    "method": "stop"
-                                }
-                            },
-                            "display title": "clock"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "delay",
-                                "GID": 27,
-                                "val": "gASVCgAAAAAAAABHP4R64UeuFHsu",
-                                "dtype": "DType.Float",
-                                "dtype state": "gASVRwAAAAAAAAB9lCiMB2RlZmF1bHSURz+5mZmZmZmajAN2YWyURwAAAAAAAAAAjANkb2OUjACUjAZib3VuZHOUTowIZGVjaW1hbHOUSwp1Lg==",
-                                "has widget": true,
-                                "widget data": "gASVEgAAAAAAAAB9lIwEdGV4dJSMBDAuMDGUcy4="
-                            },
-                            {
-                                "type": "data",
-                                "label": "iterations",
-                                "GID": 28,
-                                "val": "gARLZC4=",
-                                "dtype": "DType.Integer",
-                                "dtype state": "gASVOAAAAAAAAAB9lCiMB2RlZmF1bHSUSv////+MA3ZhbJRLAIwDZG9jlIwAlIwGYm91bmRzlEr/////TegDhpR1Lg==",
-                                "has widget": true,
-                                "widget data": "gASVDAAAAAAAAAB9lIwDdmFslEtkcy4="
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "exec",
-                                "label": "",
-                                "GID": 29
-                            }
-                        ],
-                        "GID": 26,
-                        "pos x": 231.23622828581165,
-                        "pos y": 114.6139516414438,
-                        "main widget data": "gAR9lC4=",
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "built_in.SetVar_Node",
-                        "version": null,
-                        "state data": "gASVDgAAAAAAAAB9lIwGYWN0aXZllIhzLg==",
-                        "additional data": {
-                            "special actions": {
-                                "make passive": {
-                                    "method": "action_make_passive"
-                                }
-                            },
-                            "display title": "set var"
-                        },
-                        "inputs": [
-                            {
-                                "type": "exec",
-                                "label": "",
-                                "GID": 31
-                            },
-                            {
-                                "type": "data",
-                                "label": "var",
-                                "GID": 32,
-                                "val": "gASVBwAAAAAAAACMA2N0cpQu",
-                                "dtype": "DType.String",
-                                "dtype state": "gASVNwAAAAAAAAB9lCiMB2RlZmF1bHSUjACUjAN2YWyUaAKMA2RvY5RoAowGYm91bmRzlE6MBHNpemWUjAFtlHUu",
-                                "has widget": true,
-                                "widget data": "gASVEQAAAAAAAAB9lIwEdGV4dJSMA2N0cpRzLg=="
-                            },
-                            {
-                                "type": "data",
-                                "label": "val",
-                                "GID": 33,
-                                "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBc5R1Lg==",
-                                "has widget": true,
-                                "widget data": "gASVEgAAAAAAAAB9lIwEdGV4dJSMBDMxMzWUcy4="
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "exec",
-                                "label": "",
-                                "GID": 34
-                            },
-                            {
-                                "type": "data",
-                                "label": "val",
-                                "GID": 35
-                            }
-                        ],
-                        "GID": 30,
-                        "pos x": 846.1910218628586,
-                        "pos y": 133.30644603168525,
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "built_in.GetVar_Node",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {
-                            "special actions": {},
-                            "display title": "get var"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 37,
-                                "val": "gASVBwAAAAAAAACMA2N0cpQu",
-                                "dtype": "DType.String",
-                                "dtype state": "gASVNwAAAAAAAAB9lCiMB2RlZmF1bHSUjACUjAN2YWyUaAKMA2RvY5RoAowGYm91bmRzlE6MBHNpemWUjAFtlHUu",
-                                "has widget": true,
-                                "widget data": "gASVEQAAAAAAAAB9lIwEdGV4dJSMA2N0cpRzLg=="
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "val",
-                                "GID": 38
-                            }
-                        ],
-                        "GID": 36,
-                        "pos x": 196.31117068512344,
-                        "pos y": 265.7809649154535,
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "std.Plus_Node",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {
-                            "special actions": {
-                                "add input": {
-                                    "method": "add_operand_input"
-                                },
-                                "remove input 0": {
-                                    "method": "remove_operand_input",
-                                    "data": 0
-                                },
-                                "remove input 1": {
-                                    "method": "remove_operand_input",
-                                    "data": 1
-                                }
-                            },
-                            "display title": "+"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 40,
-                                "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBc5R1Lg==",
-                                "has widget": true,
-                                "widget data": "gASVEgAAAAAAAAB9lIwEdGV4dJSMBDMxMzSUcy4="
-                            },
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 41,
-                                "val": "gARLAS4=",
-                                "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBc5R1Lg==",
-                                "has widget": true,
-                                "widget data": "gASVDwAAAAAAAAB9lIwEdGV4dJSMATGUcy4="
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 42
-                            }
-                        ],
-                        "GID": 39,
-                        "pos x": 585.6887913914879,
-                        "pos y": 174.51150293252385,
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "std.LinkIN_Node",
-                        "version": null,
-                        "state data": "gASVMAAAAAAAAAB9lIwCSUSUjCRjNWVmZjE5MC0yNzllLTQ3M2MtYTQ3Ny1iNjJmMjg0MGUzOTiUcy4=",
-                        "additional data": {
-                            "special actions": {
-                                "add input": {
-                                    "method": "add_inp"
-                                },
-                                "remove inp": {},
-                                "copy ID": {
-                                    "method": "copy_ID"
-                                }
-                            },
-                            "display title": "link"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 44,
-                                "has widget": false
-                            }
-                        ],
-                        "outputs": [],
-                        "GID": 43,
-                        "pos x": 566.7044488232787,
-                        "pos y": 478.94789784811906,
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    }
-                ],
-                "connections": [
-                    {
-                        "GID": 45,
-                        "parent node index": 0,
-                        "output port index": 0,
-                        "connected node": 5,
-                        "connected input port index": 1
-                    },
-                    {
-                        "GID": 46,
-                        "parent node index": 1,
-                        "output port index": 0,
-                        "connected node": 2,
-                        "connected input port index": 0
-                    },
-                    {
-                        "GID": 47,
-                        "parent node index": 3,
-                        "output port index": 0,
-                        "connected node": 4,
-                        "connected input port index": 0
-                    },
-                    {
-                        "GID": 48,
-                        "parent node index": 3,
-                        "output port index": 0,
-                        "connected node": 11,
-                        "connected input port index": 0
-                    },
-                    {
-                        "GID": 49,
-                        "parent node index": 6,
-                        "output port index": 0,
-                        "connected node": 1,
-                        "connected input port index": 0
-                    },
-                    {
-                        "GID": 50,
-                        "parent node index": 7,
-                        "output port index": 0,
-                        "connected node": 8,
-                        "connected input port index": 0
-                    },
-                    {
-                        "GID": 51,
-                        "parent node index": 9,
-                        "output port index": 0,
-                        "connected node": 0,
-                        "connected input port index": 0
-                    },
-                    {
-                        "GID": 52,
-                        "parent node index": 9,
-                        "output port index": 0,
-                        "connected node": 10,
-                        "connected input port index": 0
-                    },
-                    {
-                        "GID": 53,
-                        "parent node index": 10,
-                        "output port index": 0,
-                        "connected node": 8,
-                        "connected input port index": 2
-                    }
-                ],
-                "GID": 4,
-                "flow view": {
-                    "drawings": [],
-                    "view size": [
-                        6400.0,
-                        4800.0
-                    ]
-                }
-            },
-            "GID": 1
-        },
-        {
-            "title": "script 2",
-            "variables": {},
-            "flow": {
-                "algorithm mode": "data",
-                "nodes": [
-                    {
-                        "identifier": "std.LinkOUT_Node",
-                        "version": null,
-                        "state data": "gASVNwAAAAAAAAB9lIwJbGlua2VkIElElIwkYzVlZmYxOTAtMjc5ZS00NzNjLWE0NzctYjYyZjI4NDBlMzk4lHMu",
-                        "additional data": {
-                            "special actions": {
-                                "link to ID": {
-                                    "method": "choose_link_ID"
-                                }
-                            },
-                            "display title": "link"
-                        },
-                        "inputs": [],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 59
-                            }
-                        ],
-                        "GID": 58,
-                        "pos x": 384.5648465384588,
-                        "pos y": 259.39811467640766,
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "built_in.Result_Node",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {
-                            "special actions": {},
-                            "display title": "result"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 61,
-                                "has widget": false
-                            }
-                        ],
-                        "outputs": [],
-                        "GID": 60,
-                        "pos x": 642.5648465384588,
-                        "pos y": 259.39811467640766,
-                        "main widget data": "gAR9lC4=",
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    }
-                ],
-                "connections": [
-                    {
-                        "GID": 62,
-                        "parent node index": 0,
-                        "output port index": 0,
-                        "connected node": 1,
-                        "connected input port index": 0
-                    }
-                ],
-                "GID": 57,
-                "flow view": {
-                    "drawings": [],
-                    "view size": [
-                        6400.0,
-                        4800.0
-                    ]
-                }
-            },
-            "GID": 54
-        }
-    ]
-}
\ No newline at end of file
diff --git a/ryven/examples_projects/matrices_OLD.json b/ryven/examples_projects/matrices_OLD.json
deleted file mode 100644
index eb8328ed..00000000
--- a/ryven/examples_projects/matrices_OLD.json
+++ /dev/null
@@ -1,602 +0,0 @@
-{
-    "general info": {
-        "type": "Ryven project file"
-    },
-    "required packages": [
-        {
-            "name": "std",
-            "dir": "C:\\Users\\nutri\\projects\\ryven projects\\Ryven_mmorys\\Ryven\\ryven\\example_nodes\\std"
-        },
-        {
-            "name": "linalg",
-            "dir": "C:\\Users\\nutri\\projects\\ryven projects\\Ryven_mmorys\\Ryven\\ryven\\example_nodes\\linalg"
-        }
-    ],
-    "scripts": [
-        {
-            "title": "hello world",
-            "variables": {
-                "a": {
-                    "serialized": "gARLSi4="
-                },
-                "mat": {
-                    "serialized": "gASVywAAAAAAAACMFW51bXB5LmNvcmUubXVsdGlhcnJheZSMDF9yZWNvbnN0cnVjdJSTlIwFbnVtcHmUjAduZGFycmF5lJOUSwCFlEMBYpSHlFKUKEsBSwJLAoaUaAOMBWR0eXBllJOUjANjMTaUiYiHlFKUKEsDjAE8lE5OTkr/////Sv////9LAHSUYolDQAAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAAAAAAAAAAAADwvwAAAAAAgFJAAAAAAAAAAACUdJRiLg=="
-                }
-            },
-            "flow": {
-                "algorithm mode": "data",
-                "nodes": [
-                    {
-                        "identifier": "linalg.Matrix_Node",
-                        "version": null,
-                        "state data": "gASVVAAAAAAAAAB9lCiMEm1haW4gd2lkZ2V0IGhpZGRlbpSJjBFleHByZXNzaW9uIG1hdHJpeJRdlChdlCiMAjFqlIwBMZRlXZQojAQwLTFqlIwGdignYScplGVldS4=",
-                        "additional data": {
-                            "special actions": {
-                                "hide preview": {
-                                    "method": "action_hide_mw"
-                                }
-                            },
-                            "display title": "Matrix"
-                        },
-                        "inputs": [],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 6
-                            }
-                        ],
-                        "GID": 5,
-                        "pos x": 268.009788256565,
-                        "pos y": 113.19818962396498,
-                        "main widget data": "gASVMwAAAAAAAAB9lCiMBHRleHSUjBsgICAgMWogICAgICAxCiAgMC0xaiB2KCdhJymUjAVzaG93bpSIdS4=",
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "linalg.ShowMatrix",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {
-                            "special actions": {},
-                            "display title": "Show Matrix"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 8,
-                                "has widget": false
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 9
-                            }
-                        ],
-                        "GID": 7,
-                        "pos x": 523.009788256565,
-                        "pos y": 119.19818962396498,
-                        "main widget data": "gASVNwAAAAAAAAB9lCiMBHRleHSUjB8gICAgIDFqICAoMSswaikKICAgIC0xaiAoNzQrMGoplIwFc2hvd26UiHUu",
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "built_in.SetVar_Node",
-                        "version": null,
-                        "state data": "gASVDgAAAAAAAAB9lIwGYWN0aXZllIlzLg==",
-                        "additional data": {
-                            "special actions": {
-                                "make active": {
-                                    "method": "action_make_active"
-                                }
-                            },
-                            "display title": "set var"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "var",
-                                "GID": 11,
-                                "val": "gAROLg==",
-                                "dtype": "DType.String",
-                                "dtype state": "gASVNwAAAAAAAAB9lCiMB2RlZmF1bHSUjACUjAN2YWyUaAKMA2RvY5RoAowGYm91bmRzlE6MBHNpemWUjAFtlHUu",
-                                "has widget": true,
-                                "widget data": "gASVDwAAAAAAAAB9lIwEdGV4dJSMAWGUcy4="
-                            },
-                            {
-                                "type": "data",
-                                "label": "val",
-                                "GID": 12,
-                                "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBc5R1Lg==",
-                                "has widget": true,
-                                "widget data": "gASVEAAAAAAAAAB9lIwEdGV4dJSMAjc0lHMu"
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "val",
-                                "GID": 13
-                            }
-                        ],
-                        "GID": 10,
-                        "pos x": 549.009788256565,
-                        "pos y": 265.198189623965,
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "std.Slider_Node",
-                        "version": null,
-                        "state data": "gASVEwAAAAAAAAB9lIwDdmFslEc/53S8an7523Mu",
-                        "additional data": {
-                            "special actions": {},
-                            "display title": "slider"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "scl",
-                                "GID": 15,
-                                "val": "gARLMi4=",
-                                "dtype": "DType.Integer",
-                                "dtype state": "gASVLAAAAAAAAAB9lCiMB2RlZmF1bHSUSwGMA3ZhbJRLAIwDZG9jlIwAlIwGYm91bmRzlE51Lg==",
-                                "has widget": true,
-                                "widget data": "gASVDAAAAAAAAAB9lIwDdmFslEsycy4="
-                            },
-                            {
-                                "type": "data",
-                                "label": "round",
-                                "GID": 16,
-                                "val": "gASILg==",
-                                "dtype": "DType.Boolean",
-                                "dtype state": "gASVKgAAAAAAAAB9lCiMB2RlZmF1bHSUiYwDdmFslImMA2RvY5SMAJSMBmJvdW5kc5ROdS4=",
-                                "has widget": true,
-                                "widget data": "gASVDwAAAAAAAAB9lIwHY2hlY2tlZJSIcy4="
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 17
-                            }
-                        ],
-                        "GID": 14,
-                        "pos x": 214.009788256565,
-                        "pos y": 264.198189623965,
-                        "main widget data": "gASVDQAAAAAAAAB9lIwDdmFslE3dAnMu",
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "std.Multiply_Node",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {
-                            "special actions": {
-                                "add input": {
-                                    "method": "add_operand_input"
-                                },
-                                "remove input 0": {
-                                    "method": "remove_operand_input",
-                                    "data": 0
-                                },
-                                "remove input 1": {
-                                    "method": "remove_operand_input",
-                                    "data": 1
-                                }
-                            },
-                            "display title": "*"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 19,
-                                "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBc5R1Lg==",
-                                "has widget": true,
-                                "widget data": "gASVEAAAAAAAAAB9lIwEdGV4dJSMAjM3lHMu"
-                            },
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 20,
-                                "val": "gARLAi4=",
-                                "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBc5R1Lg==",
-                                "has widget": true,
-                                "widget data": "gASVDwAAAAAAAAB9lIwEdGV4dJSMATKUcy4="
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 21
-                            }
-                        ],
-                        "GID": 18,
-                        "pos x": 385.009788256565,
-                        "pos y": 291.198189623965,
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "built_in.SetVar_Node",
-                        "version": null,
-                        "state data": "gASVDgAAAAAAAAB9lIwGYWN0aXZllIlzLg==",
-                        "additional data": {
-                            "special actions": {
-                                "make active": {
-                                    "method": "action_make_active"
-                                }
-                            },
-                            "display title": "set var"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "var",
-                                "GID": 23,
-                                "val": "gASVBwAAAAAAAACMA21hdJQu",
-                                "dtype": "DType.String",
-                                "dtype state": "gASVNwAAAAAAAAB9lCiMB2RlZmF1bHSUjACUjAN2YWyUaAKMA2RvY5RoAowGYm91bmRzlE6MBHNpemWUjAFtlHUu",
-                                "has widget": true,
-                                "widget data": "gASVEQAAAAAAAAB9lIwEdGV4dJSMA21hdJRzLg=="
-                            },
-                            {
-                                "type": "data",
-                                "label": "val",
-                                "GID": 24,
-                                "dtype": "DType.Data",
-                                "dtype state": "gASVNQAAAAAAAAB9lCiMB2RlZmF1bHSUTowDdmFslE6MA2RvY5SMAJSMBmJvdW5kc5ROjARzaXpllIwBc5R1Lg==",
-                                "has widget": true,
-                                "widget data": "gASVNAAAAAAAAAB9lIwEdGV4dJSMJltbIDAuKzEuaiAgMS4rMC5qXQogWyAwLi0xLmogNzQuKzAual1dlHMu"
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "val",
-                                "GID": 25
-                            }
-                        ],
-                        "GID": 22,
-                        "pos x": 851.7300738632727,
-                        "pos y": 132.0220031444171,
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "built_in.GetVar_Node",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {
-                            "special actions": {},
-                            "display title": "get var"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 27,
-                                "val": "gASVBwAAAAAAAACMA21hdJQu",
-                                "dtype": "DType.String",
-                                "dtype state": "gASVNwAAAAAAAAB9lCiMB2RlZmF1bHSUjACUjAN2YWyUaAKMA2RvY5RoAowGYm91bmRzlE6MBHNpemWUjAFtlHUu",
-                                "has widget": true,
-                                "widget data": "gASVEQAAAAAAAAB9lIwEdGV4dJSMA21hdJRzLg=="
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "val",
-                                "GID": 28
-                            }
-                        ],
-                        "GID": 26,
-                        "pos x": 152.06383333061729,
-                        "pos y": 405.2129042004253,
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "linalg.InverseMatrix",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {
-                            "special actions": {},
-                            "display title": "Inverse"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 30,
-                                "has widget": false
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 31
-                            }
-                        ],
-                        "GID": 29,
-                        "pos x": 175.96624820900752,
-                        "pos y": 548.4374023593136,
-                        "main widget data": "gASVRwAAAAAAAAB9lCiMBHRleHSUjC8gICAtMC45ODY3aiAgICAgMC4wMTMzagooMC4wMTMzKzBqKSAoMC4wMTMzKzBqKZSMBXNob3dulIh1Lg==",
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "linalg.DetOfMatrix",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {
-                            "special actions": {},
-                            "display title": "Determinant"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 33,
-                                "has widget": false
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 34
-                            }
-                        ],
-                        "GID": 32,
-                        "pos x": 611.0383971650748,
-                        "pos y": 548.1962980663284,
-                        "main widget data": "gASVLgAAAAAAAAB9lCiMBHRleHSUjBYtMC4wMTMzMzMzMzMzMzMzMzMzMzhqlIwFc2hvd26UiHUu",
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "linalg.SolveLEq",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {
-                            "special actions": {},
-                            "display title": "Solve"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 36,
-                                "has widget": false
-                            },
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 37,
-                                "has widget": false
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 38
-                            }
-                        ],
-                        "GID": 35,
-                        "pos x": 885.1839459089059,
-                        "pos y": 322.570155217133,
-                        "main widget data": "gASVLwAAAAAAAAB9lCiMBHRleHSUjBcgICAtMC45ODY3aiAoMC4wMTMzKzBqKZSMBXNob3dulIh1Lg==",
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "linalg.MatMul",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {
-                            "special actions": {},
-                            "display title": "Mult"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 40,
-                                "has widget": false
-                            },
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 41,
-                                "has widget": false
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 42
-                            }
-                        ],
-                        "GID": 39,
-                        "pos x": 372.26934314274683,
-                        "pos y": 442.4371649448529,
-                        "main widget data": "gASVMwAAAAAAAAB9lCiMBHRleHSUjBsoMSswaikgICAgIDBqCiAgICAwaiAoMSswaimUjAVzaG93bpSIdS4=",
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "std.Eval_Node",
-                        "version": null,
-                        "state data": "gASVPQAAAAAAAAB9lCiMEG51bSBwYXJhbSBpbnB1dHOUSwGMD2V4cHJlc3Npb24gY29kZZSMDihpbnBbMF0pWzBdWzpdlHUu",
-                        "additional data": {
-                            "special actions": {
-                                "add input": {
-                                    "method": "add_param_input"
-                                },
-                                "remove input 0": {
-                                    "method": "remove_param_input",
-                                    "data": 0
-                                }
-                            },
-                            "display title": "eval"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 44,
-                                "has widget": false
-                            }
-                        ],
-                        "outputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 45
-                            }
-                        ],
-                        "GID": 43,
-                        "pos x": 598.3422117376416,
-                        "pos y": 418.5614824529824,
-                        "main widget data": "gASVHAAAAAAAAAB9lIwEdGV4dJSMDihpbnBbMF0pWzBdWzpdlHMu",
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    },
-                    {
-                        "identifier": "built_in.Result_Node",
-                        "version": null,
-                        "state data": "gAR9lC4=",
-                        "additional data": {
-                            "special actions": {},
-                            "display title": "result"
-                        },
-                        "inputs": [
-                            {
-                                "type": "data",
-                                "label": "",
-                                "GID": 47,
-                                "has widget": false
-                            }
-                        ],
-                        "outputs": [],
-                        "GID": 46,
-                        "pos x": 882.6120564064703,
-                        "pos y": 429.0070935431757,
-                        "main widget data": "gAR9lC4=",
-                        "unconnected ports hidden": false,
-                        "collapsed": false
-                    }
-                ],
-                "connections": [
-                    {
-                        "GID": 48,
-                        "parent node index": 0,
-                        "output port index": 0,
-                        "connected node": 1,
-                        "connected input port index": 0
-                    },
-                    {
-                        "GID": 49,
-                        "parent node index": 1,
-                        "output port index": 0,
-                        "connected node": 5,
-                        "connected input port index": 1
-                    },
-                    {
-                        "GID": 50,
-                        "parent node index": 1,
-                        "output port index": 0,
-                        "connected node": 9,
-                        "connected input port index": 0
-                    },
-                    {
-                        "GID": 51,
-                        "parent node index": 3,
-                        "output port index": 0,
-                        "connected node": 4,
-                        "connected input port index": 0
-                    },
-                    {
-                        "GID": 52,
-                        "parent node index": 4,
-                        "output port index": 0,
-                        "connected node": 2,
-                        "connected input port index": 1
-                    },
-                    {
-                        "GID": 53,
-                        "parent node index": 6,
-                        "output port index": 0,
-                        "connected node": 10,
-                        "connected input port index": 0
-                    },
-                    {
-                        "GID": 54,
-                        "parent node index": 6,
-                        "output port index": 0,
-                        "connected node": 7,
-                        "connected input port index": 0
-                    },
-                    {
-                        "GID": 55,
-                        "parent node index": 7,
-                        "output port index": 0,
-                        "connected node": 8,
-                        "connected input port index": 0
-                    },
-                    {
-                        "GID": 56,
-                        "parent node index": 7,
-                        "output port index": 0,
-                        "connected node": 10,
-                        "connected input port index": 1
-                    },
-                    {
-                        "GID": 57,
-                        "parent node index": 10,
-                        "output port index": 0,
-                        "connected node": 11,
-                        "connected input port index": 0
-                    },
-                    {
-                        "GID": 58,
-                        "parent node index": 11,
-                        "output port index": 0,
-                        "connected node": 9,
-                        "connected input port index": 1
-                    },
-                    {
-                        "GID": 59,
-                        "parent node index": 11,
-                        "output port index": 0,
-                        "connected node": 12,
-                        "connected input port index": 0
-                    }
-                ],
-                "GID": 4,
-                "flow view": {
-                    "drawings": [],
-                    "view size": [
-                        6400.0,
-                        4800.0
-                    ]
-                }
-            },
-            "GID": 1
-        }
-    ]
-}
\ No newline at end of file
diff --git a/ryvencore-qt/MANIFEST.in b/ryvencore-qt/MANIFEST.in
new file mode 100644
index 00000000..a1dc1317
--- /dev/null
+++ b/ryvencore-qt/MANIFEST.in
@@ -0,0 +1,10 @@
+recursive-include **/resources *
+recursive-include ryvencore-qt *
+recursive-include docs *
+include LICENSE
+include MANIFEST.in
+include pyproject.toml
+include README.md
+include setup.cfg
+global-exclude *.py[co]
+global-exclude __pycache__
diff --git a/ryvencore-qt/README.md b/ryvencore-qt/README.md
new file mode 100644
index 00000000..fafb947e
--- /dev/null
+++ b/ryvencore-qt/README.md
@@ -0,0 +1,20 @@
+
+<p align="center">
+  <img src="./img/logo.png" alt="drawing" width="70%"/>
+</p>
+
+`ryvencore-qt` provides Qt-based GUI classes for ryvencore, and is itself a Python package. The Ryven editor is built on top of `ryvencore-qt`. `ryvencore-qt` uses the PySide2 Python bindings for Qt. It shouldn't be too much work to get it running with PySide6 but I didn't try it yet. PyQt is not supported (because it doesn't allow for multi-inheritance on QObject derived classes).
+
+### Installation
+
+```
+pip install ryvencore-qt
+```
+
+or build from sources
+
+```
+git clone https://github.com/leon-thomm/ryven
+cd ryven/ryvencore-qt
+pip install .
+```
diff --git a/ryvencore-qt/img/logo.png b/ryvencore-qt/img/logo.png
new file mode 100644
index 00000000..bc9e8712
Binary files /dev/null and b/ryvencore-qt/img/logo.png differ
diff --git a/ryvencore-qt/pyproject.toml b/ryvencore-qt/pyproject.toml
new file mode 100644
index 00000000..f8d89757
--- /dev/null
+++ b/ryvencore-qt/pyproject.toml
@@ -0,0 +1,6 @@
+[build-system]
+requires = [
+    "setuptools",
+    "wheel"
+]
+build-backend = "setuptools.build_meta"
\ No newline at end of file
diff --git a/ryvencore-qt/ryvencore_qt/__init__.py b/ryvencore-qt/ryvencore_qt/__init__.py
new file mode 100644
index 00000000..13e65cd4
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/__init__.py
@@ -0,0 +1,21 @@
+# set package path (for resources etc.)
+import os
+from .src.GlobalAttributes import Location
+Location.PACKAGE_PATH = os.path.normpath(os.path.dirname(__file__))
+
+os.environ['RC_MODE'] = 'gui'  # set ryvencore gui mode
+os.environ['QT_ENABLE_HIGHDPI_SCALING'] = '1'
+
+# expose ryvencore
+import ryvencore
+
+from .src.SessionGUI import SessionGUI
+from .src.flows.nodes.NodeGUI import NodeGUI
+
+# customer base classes
+from ryvencore import Node
+from .src.flows.nodes.WidgetBaseClasses import NodeMainWidget, NodeInputWidget
+
+# gui classes
+from .src.widgets import *
+from .src.flows.FlowTheme import flow_themes
diff --git a/ryvencore-qt/ryvencore_qt/resources/__init__.py b/ryvencore-qt/ryvencore_qt/resources/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/__init__.py b/ryvencore-qt/ryvencore_qt/resources/fonts/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-Bold.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-Bold.ttf
new file mode 100644
index 00000000..127eacbb
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-Bold.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-BoldItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-BoldItalic.ttf
new file mode 100644
index 00000000..69d160c8
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-BoldItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-Italic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-Italic.ttf
new file mode 100644
index 00000000..4039eb9b
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-Italic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-Medium.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-Medium.ttf
new file mode 100644
index 00000000..f70e418e
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-Medium.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-MediumItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-MediumItalic.ttf
new file mode 100644
index 00000000..4cfb4b79
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-MediumItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-Regular.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-Regular.ttf
new file mode 100644
index 00000000..b696fd2b
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-Regular.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-SemiBold.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-SemiBold.ttf
new file mode 100644
index 00000000..3ea7e0db
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-SemiBold.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-SemiBoldItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-SemiBoldItalic.ttf
new file mode 100644
index 00000000..837fdad3
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/Asap-SemiBoldItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/asap/OFL.txt b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/OFL.txt
new file mode 100644
index 00000000..7efcf7a7
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2018 The Asap Project Authors (https://github.com/Omnibus-Type/Asap).
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded, 
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/asap/__init__.py b/ryvencore-qt/ryvencore_qt/resources/fonts/asap/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/OFL.txt b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/OFL.txt
new file mode 100644
index 00000000..d92734d3
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2014-2017 Indian Type Foundry (info@indiantypefoundry.com)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded, 
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Black.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Black.ttf
new file mode 100644
index 00000000..3c8f3637
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Black.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-BlackItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-BlackItalic.ttf
new file mode 100644
index 00000000..f2139088
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-BlackItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Bold.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Bold.ttf
new file mode 100644
index 00000000..6e26de76
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Bold.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-BoldItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-BoldItalic.ttf
new file mode 100644
index 00000000..67d55130
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-BoldItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraBold.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraBold.ttf
new file mode 100644
index 00000000..c2ac5271
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraBold.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraBoldItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraBoldItalic.ttf
new file mode 100644
index 00000000..9b66592a
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraBoldItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraLight.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraLight.ttf
new file mode 100644
index 00000000..24e2d0c5
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraLight.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraLightItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraLightItalic.ttf
new file mode 100644
index 00000000..1bc58aa1
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraLightItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Italic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Italic.ttf
new file mode 100644
index 00000000..0bc0d35f
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Italic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Light.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Light.ttf
new file mode 100644
index 00000000..52d424ba
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Light.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-LightItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-LightItalic.ttf
new file mode 100644
index 00000000..2d98ab53
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-LightItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Medium.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Medium.ttf
new file mode 100644
index 00000000..89aae6b6
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Medium.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-MediumItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-MediumItalic.ttf
new file mode 100644
index 00000000..14704d61
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-MediumItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Regular.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Regular.ttf
new file mode 100644
index 00000000..441d3baa
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Regular.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-SemiBold.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-SemiBold.ttf
new file mode 100644
index 00000000..3b8622f6
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-SemiBold.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-SemiBoldItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-SemiBoldItalic.ttf
new file mode 100644
index 00000000..257a1cf7
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-SemiBoldItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Thin.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Thin.ttf
new file mode 100644
index 00000000..2d62e19e
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-Thin.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-ThinItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-ThinItalic.ttf
new file mode 100644
index 00000000..bf24d1b1
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/Poppins-ThinItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/__init__.py b/ryvencore-qt/ryvencore_qt/resources/fonts/poppins/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/OFL.txt b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/OFL.txt
new file mode 100644
index 00000000..366206f5
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded, 
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Black.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Black.ttf
new file mode 100644
index 00000000..fe62ead2
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Black.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-BlackItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-BlackItalic.ttf
new file mode 100644
index 00000000..a412dd93
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-BlackItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Bold.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Bold.ttf
new file mode 100644
index 00000000..c790e045
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Bold.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-BoldItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-BoldItalic.ttf
new file mode 100644
index 00000000..08781994
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-BoldItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-ExtraLight.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-ExtraLight.ttf
new file mode 100644
index 00000000..ca7bce8f
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-ExtraLight.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-ExtraLightItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-ExtraLightItalic.ttf
new file mode 100644
index 00000000..c33f2840
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-ExtraLightItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Italic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Italic.ttf
new file mode 100644
index 00000000..91a2a44f
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Italic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Light.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Light.ttf
new file mode 100644
index 00000000..7af27ce6
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Light.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-LightItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-LightItalic.ttf
new file mode 100644
index 00000000..330c523a
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-LightItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Medium.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Medium.ttf
new file mode 100644
index 00000000..6450399d
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Medium.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-MediumItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-MediumItalic.ttf
new file mode 100644
index 00000000..50cd774b
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-MediumItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Regular.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Regular.ttf
new file mode 100644
index 00000000..3563e734
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Regular.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-SemiBold.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-SemiBold.ttf
new file mode 100644
index 00000000..526be56c
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-SemiBold.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-SemiBoldItalic.ttf b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-SemiBoldItalic.ttf
new file mode 100644
index 00000000..689c1389
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-SemiBoldItalic.ttf differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/__init__.py b/ryvencore-qt/ryvencore_qt/resources/fonts/source_code_pro/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ryvencore-qt/ryvencore_qt/resources/node_collapse_icon.svg b/ryvencore-qt/ryvencore_qt/resources/node_collapse_icon.svg
new file mode 100644
index 00000000..9e28825b
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/resources/node_collapse_icon.svg
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="63.500004mm"
+   height="31.750082mm"
+   viewBox="0 0 63.500004 31.750083"
+   version="1.1"
+   id="svg8"
+   inkscape:version="1.0.2 (e86c870879, 2021-01-15, custom)"
+   sodipodi:docname="node_collapse_icon.svg">
+  <defs
+     id="defs2" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.979899"
+     inkscape:cx="2.040684"
+     inkscape:cy="-68.588312"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     inkscape:document-rotation="0"
+     showgrid="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1017"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1">
+    <inkscape:grid
+       type="xygrid"
+       id="grid833"
+       originx="-91.281249"
+       originy="-136.26041" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Ebene 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-91.281248,-136.26041)">
+    <path
+       style="fill:none;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 89.164582,134.14374 1.322918,-1.32291 1.322915,1.32291"
+       id="path835"
+       sodipodi:nodetypes="ccc" />
+    <path
+       style="fill:#xxxxxx;fill-opacity:1;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 93.927084,134.40834 0.264583,0.26458 2.38125,-2.38125 2.38125,2.38125 0.264583,-0.26458 -2.645833,-2.64583 z"
+       id="path839" />
+    <path
+       style="fill:#xxxxxx;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="M 91.281249,168.01049 H 154.78125 L 123.03126,136.26041 Z"
+       id="path841" />
+  </g>
+</svg>
diff --git a/ryvencore-qt/ryvencore_qt/resources/node_expand_icon.svg b/ryvencore-qt/ryvencore_qt/resources/node_expand_icon.svg
new file mode 100644
index 00000000..96de138d
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/resources/node_expand_icon.svg
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="63.500004mm"
+   height="31.750082mm"
+   viewBox="0 0 63.500004 31.750083"
+   version="1.1"
+   id="svg8"
+   inkscape:version="1.0.2 (e86c870879, 2021-01-15, custom)"
+   sodipodi:docname="node_expand_icon.svg">
+  <defs
+     id="defs2" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.979899"
+     inkscape:cx="2.040684"
+     inkscape:cy="-68.588312"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     inkscape:document-rotation="0"
+     showgrid="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1017"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1">
+    <inkscape:grid
+       type="xygrid"
+       id="grid833"
+       originx="-91.281249"
+       originy="-136.26041" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Ebene 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-91.281248,-136.26041)">
+    <path
+       style="fill:none;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 89.164582,134.14374 1.322918,-1.32291 1.322915,1.32291"
+       id="path835"
+       sodipodi:nodetypes="ccc" />
+    <path
+       style="fill:#A9D5EF;fill-opacity:1;stroke:#000000;stroke-width:0.265;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 93.927084,134.40834 0.264583,0.26458 2.38125,-2.38125 2.38125,2.38125 0.264583,-0.26458 -2.645833,-2.64583 z"
+       id="path839" />
+    <path
+       style="fill:#A9D5EF;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 91.281249,136.26041 h 63.500001 l -31.74999,31.75008 z"
+       id="path841" />
+  </g>
+</svg>
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/__init__.py b/ryvencore-qt/ryvencore_qt/resources/pics/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/code_block_picture.png b/ryvencore-qt/ryvencore_qt/resources/pics/code_block_picture.png
new file mode 100644
index 00000000..a14a3c95
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/pics/code_block_picture.png differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/code_block_picture2.png b/ryvencore-qt/ryvencore_qt/resources/pics/code_block_picture2.png
new file mode 100644
index 00000000..ebda371a
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/pics/code_block_picture2.png differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/code_block_picture3.png b/ryvencore-qt/ryvencore_qt/resources/pics/code_block_picture3.png
new file mode 100644
index 00000000..4c769164
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/pics/code_block_picture3.png differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/function_picture.png b/ryvencore-qt/ryvencore_qt/resources/pics/function_picture.png
new file mode 100644
index 00000000..786f2fcc
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/pics/function_picture.png differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/function_picture_.png b/ryvencore-qt/ryvencore_qt/resources/pics/function_picture_.png
new file mode 100644
index 00000000..034fb79d
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/pics/function_picture_.png differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/logo.png b/ryvencore-qt/ryvencore_qt/resources/pics/logo.png
new file mode 100644
index 00000000..bc9e8712
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/pics/logo.png differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/macro_icon.svg b/ryvencore-qt/ryvencore_qt/resources/pics/macro_icon.svg
new file mode 100644
index 00000000..999db18c
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/resources/pics/macro_icon.svg
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   id="Layer_1"
+   x="0px"
+   y="0px"
+   width="122.881px"
+   height="122.88px"
+   viewBox="0 0 122.881 122.88"
+   enable-background="new 0 0 122.881 122.88"
+   xml:space="preserve"
+   sodipodi:docname="macro_icon.svg"
+   inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)"
+   inkscape:export-filename="C:\Users\nutri\projects\ryven projects\ryvencore-qt\ryvencore_qt\resources\pics\macro_node_icon.png"
+   inkscape:export-xdpi="390.62183"
+   inkscape:export-ydpi="390.62183"><metadata
+     id="metadata11"><rdf:RDF><cc:Work
+         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+     id="defs9" /><sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1017"
+     id="namedview7"
+     showgrid="false"
+     inkscape:zoom="6.9173179"
+     inkscape:cx="53.516881"
+     inkscape:cy="67.838404"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="g4" /><g
+     id="g4"><path
+       d="m 88.129304,30.22352 c -0.241054,-0.241055 -0.489414,-0.414907 -0.745078,-0.523016 -0.239595,-0.101536 -0.519365,-0.151207 -0.84004,-0.151207 -0.322138,0 -0.607751,0.05113 -0.850268,0.154129 -0.251281,0.105918 -0.493797,0.276848 -0.726086,0.51279 l -0.01533,0.01533 -4.534025,4.520146 c -0.679341,0.675688 -1.729026,0.750926 -2.490906,0.22864 -0.533242,-0.344781 -1.101547,-0.677146 -1.697611,-0.999282 -0.631856,-0.34186 -1.251294,-0.648657 -1.856124,-0.926236 -0.630395,-0.287805 -1.273938,-0.558809 -1.929169,-0.813013 -0.593872,-0.230828 -1.255678,-0.466039 -1.977382,-0.704172 -0.826161,-0.272465 -1.351369,-1.04092 -1.351369,-1.867081 v 0 -6.95261 c 0,-0.333094 -0.05259,-0.627473 -0.15705,-0.883868 -0.104458,-0.253473 -0.2637,-0.489415 -0.483572,-0.708555 -0.214027,-0.214028 -0.446317,-0.37254 -0.70052,-0.475536 -0.255664,-0.104458 -0.550774,-0.15632 -0.884599,-0.15632 h -8.725463 c -0.308989,0 -0.582915,0.04967 -0.822509,0.149015 -0.254204,0.104457 -0.503294,0.275387 -0.749462,0.51352 l -0.0029,0.0022 c -0.216219,0.216219 -0.375461,0.446317 -0.477727,0.691025 -0.105188,0.25055 -0.157781,0.539816 -0.157781,0.868527 v 6.365313 c 0,0.967872 -0.69906,1.772119 -1.619452,1.936475 -0.69906,0.166547 -1.344794,0.338938 -1.932822,0.519364 -0.644275,0.197227 -1.292201,0.421481 -1.941589,0.674954 -0.03214,0.01241 -0.06428,0.0241 -0.09642,0.03434 -0.567575,0.220602 -1.165829,0.476266 -1.794034,0.766262 -0.638429,0.294379 -1.233032,0.593141 -1.783806,0.893365 -0.776489,0.422942 -1.712951,0.265891 -2.311937,-0.323599 l -7.3e-4,7.31e-4 -5.021249,-4.948932 c -0.01533,-0.01533 -0.03068,-0.03068 -0.04602,-0.04602 v 7.31e-4 c -0.213997,-0.226468 -0.438982,-0.392286 -0.674923,-0.495282 -0.226446,-0.09861 -0.494528,-0.149016 -0.807169,-0.149016 -0.310449,0 -0.588028,0.05259 -0.832005,0.157051 -0.267352,0.113953 -0.523016,0.292189 -0.768454,0.532513 l -0.0015,0.0015 -6.091386,6.105266 -0.0037,0.0044 0.0037,0.0037 c -0.249821,0.249821 -0.426594,0.49818 -0.531053,0.745079 -0.101534,0.239594 -0.151936,0.519364 -0.151936,0.83931 0,0.322867 0.05113,0.60775 0.154129,0.851727 0.105917,0.250552 0.276117,0.493067 0.512789,0.725357 l 0.01533,0.01533 4.520147,4.534755 c 0.675685,0.679338 0.750922,1.729023 0.228636,2.490902 -0.344781,0.533974 -0.677144,1.101549 -0.998551,1.697611 -0.34186,0.632588 -0.648657,1.250565 -0.926235,1.856855 -0.288535,0.629665 -0.55954,1.273939 -0.814474,1.92917 -0.230828,0.593142 -0.46531,1.254947 -0.703443,1.976651 -0.272465,0.826892 -1.041649,1.351369 -1.86708,1.351369 v 7.3e-4 h -6.9504 c -0.334555,0 -0.628204,0.05186 -0.884598,0.155591 -0.252744,0.103726 -0.486493,0.261508 -0.699791,0.475536 l -0.008,0.0088 v 0 c -0.214026,0.214027 -0.372538,0.447047 -0.476266,0.699789 -0.104456,0.256395 -0.155589,0.550775 -0.155589,0.884599 v 8.72619 c 0,0.308259 0.04894,0.582915 0.148286,0.821779 0.105186,0.254204 0.276117,0.503293 0.514249,0.750192 l 0.0022,0.0015 c 0.215488,0.21622 0.446316,0.376192 0.690293,0.477728 0.251282,0.105187 0.540547,0.15778 0.869259,0.15778 h 6.364582 c 0.967872,0 1.772119,0.69906 1.936475,1.620182 0.166547,0.69833 0.338938,1.344795 0.519364,1.932823 0.197957,0.644275 0.421481,1.291471 0.674954,1.940858 0.238132,0.618708 0.508406,1.261522 0.809361,1.927709 0.307528,0.681528 0.608481,1.302428 0.900669,1.864159 0.406871,0.778681 0.238132,1.706378 -0.355009,2.295866 l 0.0015,0.0015 -4.949662,4.935053 -0.03141,0.03068 h 7.31e-4 c -0.226446,0.214759 -0.392262,0.439743 -0.494528,0.674954 -0.09934,0.226446 -0.149016,0.495259 -0.149016,0.8079 0,0.310449 0.05259,0.587297 0.15632,0.831275 0.113954,0.267351 0.292189,0.523746 0.533243,0.768454 l 6.069473,6.139598 c 0.237402,0.21987 0.485032,0.380573 0.744348,0.482106 0.260048,0.102999 0.555157,0.153399 0.88533,0.153399 0.333093,0 0.633317,-0.05186 0.897746,-0.156319 0.254205,-0.100073 0.493068,-0.253472 0.714399,-0.460925 l 4.498964,-4.570549 c 0.678606,-0.689564 1.741439,-0.772106 2.511354,-0.240325 0.532512,0.343321 1.098627,0.674224 1.693228,0.994901 0.632588,0.342589 1.250565,0.649387 1.856855,0.926234 0.629665,0.289266 1.273939,0.560271 1.92917,0.814475 0.593142,0.230097 1.254217,0.465308 1.976651,0.703441 0.826892,0.272466 1.351369,1.041651 1.351369,1.86708 h 7.3e-4 v 6.952609 c 0,0.3331 0.05186,0.62748 0.156321,0.88387 0.103726,0.25348 0.261508,0.4865 0.475536,0.70052 l 0.0037,0.005 0.0037,-0.005 c 0.428785,0.42879 0.954724,0.63989 1.584388,0.63989 h 8.72473 c 0.30972,0 0.583645,-0.0489 0.823239,-0.14828 0.254204,-0.10519 0.503294,-0.27612 0.748731,-0.51425 l 0.0029,-0.003 c 0.215489,-0.21622 0.375461,-0.44632 0.477726,-0.69029 0.105919,-0.25128 0.158513,-0.53981 0.158513,-0.86853 v -6.366049 c 0,-0.967137 0.697599,-1.772115 1.61945,-1.93574 0.6976,-0.166548 1.344065,-0.339669 1.932824,-0.519364 0.643544,-0.197958 1.291471,-0.421482 1.940128,-0.674224 0.619437,-0.238863 1.262981,-0.509137 1.93063,-0.810091 0.679338,-0.307527 1.300966,-0.60775 1.860506,-0.899938 0.780142,-0.406872 1.707839,-0.237403 2.297328,0.355008 l 0.0015,-0.0015 4.935054,4.948927 0.02995,0.03142 v -7.31e-4 c 0.21695,0.227907 0.441935,0.392991 0.676415,0.495259 0.229367,0.09862 0.506216,0.149017 0.836387,0.149017 0.323599,0 0.60483,-0.0526 0.847345,-0.157051 0.243977,-0.105919 0.471154,-0.268811 0.677876,-0.487225 0.02265,-0.02486 0.04675,-0.04891 0.07305,-0.07303 l 6.1133,-6.042441 c 0.218411,-0.236673 0.379845,-0.485032 0.48065,-0.744349 0.102999,-0.260046 0.154129,-0.555888 0.154129,-0.88606 0,-0.333093 -0.05112,-0.633316 -0.155589,-0.897746 -0.101537,-0.254205 -0.254204,-0.492337 -0.462389,-0.713669 L 88.14414,80.433189 c -0.690294,-0.678606 -0.772106,-1.740709 -0.241054,-2.511354 0.34405,-0.532512 0.674953,-1.098626 0.99563,-1.692498 0.34259,-0.631857 0.649387,-1.250565 0.926235,-1.856855 0.288536,-0.629664 0.55954,-1.273939 0.814474,-1.929169 0.229367,-0.593142 0.465309,-1.254947 0.703442,-1.977382 0.272465,-0.826891 1.041649,-1.351369 1.867813,-1.351369 v -7.31e-4 h 6.95115 c 0.33383,0 0.62893,-0.05186 0.88461,-0.15632 0.25274,-0.103727 0.48648,-0.261508 0.70051,-0.474805 l 0.007,-0.008 v 0 c 0.21401,-0.214757 0.37399,-0.447777 0.47626,-0.701251 0.10446,-0.255664 0.15558,-0.550043 0.15558,-0.883868 v -8.725981 c 0,-0.308258 -0.0483,-0.582915 -0.14828,-0.822509 -0.10445,-0.253473 -0.27539,-0.503294 -0.51352,-0.750192 l -0.004,-0.0022 c -0.21548,-0.216219 -0.44631,-0.375462 -0.69028,-0.477727 -0.24983,-0.105188 -0.53909,-0.157781 -0.86854,-0.157781 h -6.36458 c -0.980289,0 -1.791841,-0.715131 -1.941587,-1.652322 -0.15413,-0.615787 -0.327981,-1.240338 -0.525208,-1.874386 -0.189923,-0.608482 -0.411985,-1.252025 -0.672032,-1.932092 -0.01314,-0.03288 -0.02411,-0.06647 -0.03433,-0.100075 -0.246899,-0.646465 -0.499642,-1.254216 -0.758959,-1.823253 -0.281231,-0.615785 -0.579262,-1.211118 -0.894825,-1.786728 -0.422211,-0.776489 -0.265161,-1.712951 0.323597,-2.311936 v -7.3e-4 l 4.948935,-5.021249 c 0.01534,-0.01533 0.02996,-0.03068 0.04604,-0.04602 v 0 c 0.227177,-0.214028 0.39226,-0.439012 0.495259,-0.674224 0.09862,-0.226445 0.149747,-0.494527 0.149747,-0.807168 0,-0.31045 -0.0526,-0.588028 -0.15705,-0.832006 -0.113953,-0.267351 -0.292191,-0.523016 -0.533243,-0.768453 l -0.0016,-0.0015 -6.105999,-6.091387 -0.0073,-0.0066 v 0 z m 0.775759,-4.135189 c 0.731931,0.308989 1.396658,0.76115 1.995643,1.360135 l 0.0015,7.31e-4 6.122067,6.108187 0.01533,0.01533 c 0.589498,0.598985 1.040193,1.258599 1.348445,1.981764 0.318492,0.745809 0.479199,1.535448 0.479199,2.368913 0,0.845884 -0.164357,1.636252 -0.487961,2.374756 -0.315562,0.724627 -0.776489,1.368901 -1.372549,1.937937 l -3.920427,3.977407 c 0.115414,0.240325 0.230828,0.483571 0.342591,0.72974 0.314099,0.68518 0.601901,1.371821 0.86341,2.059193 0.0146,0.03068 0.027,0.06209 0.03873,0.09423 0.260779,0.68445 0.510603,1.411999 0.748734,2.177531 0.07672,0.246898 0.151207,0.495258 0.222795,0.745809 h 4.85907 c 0.84443,0 1.6348,0.15486 2.37549,0.46458 0.72755,0.304605 1.38497,0.753843 1.97154,1.344794 l 0.0416,0.04164 c 0.57779,0.597523 1.01608,1.256407 1.31411,1.979573 0.30533,0.735582 0.45654,1.506228 0.45654,2.313397 v 8.725461 c 0,0.839308 -0.14829,1.625293 -0.44777,2.359416 -0.2995,0.732661 -0.74582,1.398119 -1.34261,1.996373 l -0.004,0.0022 -0.004,0.0051 -0.004,0.0029 c -0.59898,0.596795 -1.26371,1.044572 -1.99856,1.344065 -0.73484,0.298762 -1.52083,0.447778 -2.35941,0.447778 h -5.558145 c -0.09715,0.261508 -0.197955,0.531782 -0.306797,0.807899 -0.278307,0.71513 -0.579261,1.43026 -0.905776,2.144659 -0.338212,0.738505 -0.682263,1.436103 -1.032888,2.086952 -0.115414,0.213297 -0.233751,0.425864 -0.355009,0.63843 l 3.469731,3.415676 c 0.0256,0.02411 0.04964,0.04821 0.07598,0.07451 0.569036,0.601906 0.99709,1.270287 1.285626,2.000756 0.289268,0.733392 0.432441,1.510612 0.432441,2.327277 0,0.813743 -0.142443,1.584388 -0.429521,2.314858 -0.287075,0.728278 -0.710746,1.393736 -1.27394,1.997104 v -7.31e-4 c -0.0197,0.02045 -0.04021,0.04237 -0.06138,0.06282 l -6.157861,6.087008 c -0.577801,0.60556 -1.22865,1.068678 -1.95912,1.382771 -0.737774,0.318492 -1.535447,0.479929 -2.398132,0.479929 -0.844423,0 -1.640634,-0.165088 -2.388636,-0.487231 -0.740696,-0.320673 -1.395197,-0.784521 -1.965694,-1.386422 L 76.72782,93.079671 c -0.260778,0.125647 -0.516442,0.244712 -0.764802,0.357204 -0.664727,0.299495 -1.375474,0.595329 -2.130049,0.886789 -0.709286,0.276115 -1.449253,0.531052 -2.216976,0.766262 -0.255664,0.0782 -0.507676,0.152668 -0.756036,0.223525 v 4.848849 c 0,0.8437 -0.15559,1.63479 -0.465309,2.37476 -0.305337,0.72828 -0.753115,1.38497 -1.345525,1.97227 l -0.04164,0.0423 c -0.597525,0.57634 -1.257138,1.0139 -1.978843,1.31338 -0.736313,0.30461 -1.506958,0.45582 -2.314127,0.45582 h -8.725748 c -1.715873,0 -3.165124,-0.59168 -4.366017,-1.79258 l 0.0037,-0.004 c -0.598985,-0.59971 -1.048224,-1.26663 -1.347717,-2.00222 -0.299492,-0.73485 -0.448508,-1.52011 -0.448508,-2.35942 v -5.557736 c -0.261895,-0.09715 -0.531439,-0.198687 -0.808286,-0.306797 -0.7144,-0.278307 -1.42953,-0.579261 -2.143929,-0.906507 -0.738504,-0.337484 -1.436104,-0.682265 -2.087682,-1.033622 -0.212567,-0.114683 -0.425134,-0.233018 -0.6377,-0.354277 l -3.415676,3.469733 c -0.02411,0.0256 -0.04894,0.05038 -0.07451,0.07524 -0.601908,0.570497 -1.270288,0.998551 -2.000757,1.286358 -0.733392,0.289259 -1.510611,0.433162 -2.327276,0.433162 -0.813743,0 -1.58512,-0.142443 -2.314859,-0.430242 -0.728278,-0.287075 -1.393006,-0.710747 -1.997104,-1.273202 l 7.3e-4,-0.0016 c -0.02118,-0.01896 -0.04237,-0.03947 -0.06209,-0.06064 l -6.122065,-6.192922 c -0.588759,-0.598985 -1.039458,-1.25933 -1.347717,-1.982494 -0.317754,-0.746541 -0.479187,-1.535447 -0.479187,-2.368182 0,-0.845885 0.164355,-1.636253 0.487222,-2.375489 0.319216,-0.729738 0.782333,-1.377665 1.385701,-1.950354 l 3.914588,-3.903628 C 29.674931,76.468824 29.555133,76.213159 29.442642,75.962609 29.142419,75.298612 28.847309,74.589326 28.556582,73.83548 28.280465,73.126194 28.0248,72.386229 27.789589,71.617775 27.711426,71.36138 27.636921,71.110098 27.566065,70.861739 h -4.847397 c -0.844422,0 -1.635521,-0.155591 -2.374756,-0.46531 -0.729009,-0.304605 -1.385701,-0.753114 -1.972268,-1.344794 l -0.04237,-0.04383 C 17.752203,68.41101 17.314651,67.751396 17.015889,67.028961 16.711283,66.292648 16.560076,65.522002 16.560076,64.716294 v -8.72619 c 0,-0.83931 0.149016,-1.625296 0.448508,-2.359417 0.299493,-0.734121 0.74581,-1.39958 1.343334,-1.997834 l 7.31e-4,-7.31e-4 0.0044,-0.0037 0.0037,-0.0037 c 0.598986,-0.597444 1.263714,-1.044491 1.998565,-1.343984 0.734124,-0.299492 1.520109,-0.447778 2.359418,-0.447778 h 5.558144 c 0.09642,-0.262238 0.198688,-0.531781 0.306797,-0.808629 0.277579,-0.7144 0.579262,-1.42953 0.906513,-2.14466 0.337477,-0.738505 0.681528,-1.436103 1.033614,-2.086951 0.116875,-0.21695 0.238134,-0.4339 0.362313,-0.650849 l -3.419328,-3.430285 -0.0015,-7.31e-4 c -0.604097,-0.593871 -1.061372,-1.262252 -1.373283,-2.000756 -0.319214,-0.753845 -0.476996,-1.547135 -0.476996,-2.373296 0,-0.824701 0.157051,-1.610686 0.474074,-2.360878 0.314103,-0.742888 0.766994,-1.411268 1.360866,-2.00514 l 0.0037,0.0037 6.103804,-6.118414 0.01533,-0.01533 c 0.598984,-0.588759 1.258599,-1.039459 1.981764,-1.347717 0.74654,-0.317754 1.535448,-0.479918 2.368913,-0.479918 0.845153,0 1.635521,0.164355 2.374757,0.487222 0.724626,0.316294 1.3689,0.776491 1.937936,1.373284 l 3.974485,3.916778 c 0.238864,-0.118336 0.476997,-0.231559 0.714399,-0.341129 0.611403,-0.281961 1.286358,-0.566114 2.024863,-0.852459 0.03141,-0.0146 0.06355,-0.02776 0.09569,-0.04018 0.709287,-0.276118 1.449983,-0.531781 2.218436,-0.766993 0.255665,-0.07816 0.507677,-0.152668 0.756036,-0.223525 v -4.848126 c 0,-0.843693 0.155591,-1.634791 0.46531,-2.374027 0.304605,-0.729008 0.753844,-1.385701 1.344794,-1.972998 l 0.04164,-0.04164 c 0.597524,-0.577071 1.256407,-1.013892 1.978842,-1.312654 0.736313,-0.305336 1.506958,-0.456543 2.314128,-0.456543 h 8.724729 c 0.838579,0 1.625294,0.148285 2.359417,0.447777 0.737774,0.300954 1.405424,0.750193 2.007331,1.351369 0.594602,0.596064 1.043111,1.262252 1.344064,1.999295 0.299492,0.734123 0.447778,1.520108 0.447778,2.358687 v 5.558875 c 0.262238,0.09715 0.531051,0.199418 0.808629,0.306797 0.713669,0.277579 1.428069,0.579262 2.143198,0.905782 0.739235,0.337477 1.436835,0.682259 2.086952,1.034345 0.21841,0.117606 0.43536,0.238133 0.65231,0.362313 l 3.429554,-3.419328 0.0015,-7.31e-4 c 0.593871,-0.604098 1.262982,-1.061372 2.000026,-1.373283 0.754575,-0.319215 1.546404,-0.476997 2.372565,-0.476997 0.826162,0 1.612147,0.156321 2.362339,0.473345 z M 61.440135,42.379266 c 1.300966,0 2.570522,0.12418 3.808669,0.371078 1.216963,0.243247 2.423698,0.616517 3.618016,1.120541 l 0.01314,0.0058 0.0029,-0.0058 c 1.140263,0.492336 2.221358,1.080364 3.244016,1.765545 1.013161,0.678606 1.951815,1.444869 2.812307,2.297327 l 0.0073,0.008 v 0 c 0.853189,0.861222 1.619452,1.799145 2.298058,2.813038 0.685181,1.022657 1.272478,2.103753 1.764815,3.244015 0.02264,0.05405 0.0431,0.10811 0.06136,0.162895 0.471883,1.146107 0.826161,2.302441 1.05845,3.470461 0.24836,1.238877 0.371078,2.507703 0.371078,3.807939 0,1.300966 -0.122718,2.570522 -0.371078,3.808669 -0.242515,1.217693 -0.616517,2.422968 -1.119809,3.618016 l -0.0058,0.01314 0.0058,0.0029 c -0.492337,1.140263 -1.080365,2.220628 -1.764816,3.243285 -0.679337,1.013892 -1.445599,1.951816 -2.298057,2.813038 l -0.0073,0.008 v 0 c -0.861953,0.853188 -1.799147,1.619451 -2.813769,2.298058 -1.021196,0.68518 -2.103022,1.272478 -3.242554,1.764814 -0.05405,0.02265 -0.10811,0.04383 -0.164356,0.06136 -1.145376,0.472614 -2.30244,0.826162 -3.46973,1.059181 -1.238148,0.247629 -2.508433,0.371079 -3.80867,0.371079 -1.300967,0 -2.569792,-0.12345 -3.807938,-0.371079 -1.217693,-0.243246 -2.423699,-0.617246 -3.618747,-1.12054 l -0.01314,-0.0058 -0.0022,0.0058 C 52.857858,78.517835 51.776032,77.929807 50.754105,77.245357 49.740214,76.566751 48.802291,75.800488 47.941798,74.9473 l -0.0073,-0.008 v 0 c -0.853189,-0.861954 -1.620183,-1.799147 -2.298058,-2.813769 -0.684451,-1.021927 -1.272479,-2.103753 -1.764085,-3.242555 -0.02338,-0.05405 -0.04383,-0.10811 -0.06209,-0.163625 -0.472614,-1.146107 -0.826161,-2.30244 -1.05918,-3.470461 -0.2469,-1.238147 -0.37108,-2.507703 -0.37108,-3.80867 0,-1.300966 0.12345,-2.569792 0.37108,-3.807938 0.243245,-1.217693 0.617245,-2.423699 1.121269,-3.618017 l 0.0051,-0.01314 -0.0051,-0.0022 c 0.491607,-1.140263 1.080365,-2.221358 1.764085,-3.243285 0.678606,-1.014622 1.444869,-1.951815 2.298058,-2.813769 l 0.008,-0.0073 v 0 c 0.860492,-0.852458 1.798415,-1.61872 2.811577,-2.297327 1.022658,-0.684451 2.104484,-1.273209 3.244746,-1.765545 0.05405,-0.02338 0.10811,-0.04383 0.163625,-0.06136 1.146108,-0.472613 2.301711,-0.82543 3.469731,-1.05845 1.238146,-0.248359 2.506972,-0.37254 3.807939,-0.37254 z m 3.040214,4.227958 c -0.981751,-0.196497 -1.994182,-0.294379 -3.040214,-0.294379 -1.045303,0 -2.057733,0.09788 -3.040215,0.294379 -0.953994,0.190653 -1.864889,0.463848 -2.732687,0.819587 -0.03799,0.01899 -0.0767,0.03799 -0.116875,0.05478 -0.94815,0.409793 -1.82033,0.880217 -2.615081,1.411998 -0.800595,0.535434 -1.541291,1.141724 -2.22355,1.816678 -0.675684,0.68299 -1.281974,1.425146 -1.818139,2.226472 -0.531781,0.79402 -1.002205,1.66547 -1.411268,2.61362 l -0.0051,-0.0022 c -0.380575,0.903591 -0.669841,1.854663 -0.869989,2.852485 -0.195766,0.981751 -0.293649,1.994181 -0.293649,3.039483 0,1.045303 0.09788,2.057733 0.293649,3.040215 0.190652,0.953994 0.464579,1.86489 0.820317,2.732687 0.01899,0.03872 0.03799,0.0767 0.05478,0.116876 0.409062,0.948149 0.879485,1.818869 1.410536,2.612889 0.536165,0.801326 1.142455,1.543483 1.81887,2.226472 0.682258,0.675685 1.423685,1.281243 2.22428,1.816679 0.79402,0.531781 1.665471,1.002203 2.614351,1.411997 l -0.0022,0.0051 c 0.904321,0.380575 1.854662,0.670572 2.852484,0.86926 0.981751,0.196495 1.994913,0.293649 3.040215,0.293649 1.045302,0 2.058463,-0.09788 3.040215,-0.293649 0.954724,-0.190653 1.864888,-0.46385 2.733417,-0.819588 0.03726,-0.01899 0.0767,-0.03799 0.116874,-0.05478 0.94742,-0.409795 1.81887,-0.880217 2.613622,-1.411999 0.800594,-0.535434 1.542021,-1.141723 2.224279,-1.816677 0.676415,-0.68299 1.282705,-1.424416 1.81887,-2.225741 0.531051,-0.794021 1.001474,-1.665472 1.409806,-2.613621 l 0.0058,0.0022 c 0.380574,-0.903592 0.670571,-1.854663 0.869989,-2.851754 0.195765,-0.982482 0.293648,-1.994913 0.293648,-3.040215 0,-1.045302 -0.09861,-2.057733 -0.293648,-3.039484 -0.191383,-0.954724 -0.463848,-1.864889 -0.820317,-2.733418 -0.01972,-0.03799 -0.03726,-0.0767 -0.05551,-0.116874 -0.408332,-0.948151 -0.878755,-1.8196 -1.411267,-2.613621 -0.534704,-0.801325 -1.141724,-1.543483 -1.817409,-2.225741 -0.682259,-0.674954 -1.423685,-1.281244 -2.22428,-1.816678 -0.794751,-0.531782 -1.666201,-1.002204 -2.613621,-1.411998 l 0.0015,-0.0051 c -0.90359,-0.380576 -1.853931,-0.670572 -2.852484,-0.86999 z"
+       id="path2"
+       style="fill:#448aff;fill-opacity:0.98431373;stroke-width:0.803517;stroke-miterlimit:4;stroke-dasharray:none"
+       inkscape:export-xdpi="390.62183"
+       inkscape:export-ydpi="390.62183" /></g></svg>
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/macro_node_icon.png b/ryvencore-qt/ryvencore_qt/resources/pics/macro_node_icon.png
new file mode 100644
index 00000000..51f81101
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/pics/macro_node_icon.png differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/macro_script_picture.png b/ryvencore-qt/ryvencore_qt/resources/pics/macro_script_picture.png
new file mode 100644
index 00000000..89e5f8ee
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/pics/macro_script_picture.png differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/opencv_example.png b/ryvencore-qt/ryvencore_qt/resources/pics/opencv_example.png
new file mode 100644
index 00000000..973d2245
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/pics/opencv_example.png differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/script_picture.png b/ryvencore-qt/ryvencore_qt/resources/pics/script_picture.png
new file mode 100644
index 00000000..05e10ee6
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/pics/script_picture.png differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/script_picture.svg b/ryvencore-qt/ryvencore_qt/resources/pics/script_picture.svg
new file mode 100644
index 00000000..102ff4e7
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/resources/pics/script_picture.svg
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   id="Layer_1"
+   x="0px"
+   y="0px"
+   viewBox="0 0 117.12 117.12"
+   xml:space="preserve"
+   sodipodi:docname="script_picture.svg"
+   inkscape:export-filename="C:\Users\nutri\projects\ryven projects\ryvencore-qt\ryvencore_qt\resources\pics\script_picture.png"
+   inkscape:export-xdpi="507.73843"
+   inkscape:export-ydpi="507.73843"
+   width="1.22in"
+   height="1.22in"
+   inkscape:version="1.0.2-2 (e86c870879, 2021-01-15)"><metadata
+     id="metadata13"><rdf:RDF><cc:Work
+         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+     id="defs11" /><sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1017"
+     id="namedview9"
+     showgrid="false"
+     units="in"
+     width="122in"
+     inkscape:zoom="3.4847491"
+     inkscape:cx="56.845784"
+     inkscape:cy="55.28112"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="Layer_1" /><style
+     type="text/css"
+     id="style2">.st0{fill-rule:evenodd;clip-rule:evenodd;}</style><g
+     id="g6"
+     style="fill:#448aff;fill-opacity:0.984314"
+     transform="matrix(0.76940556,0,0,0.76940556,13.305675,10.905415)"><path
+       class="st0"
+       d="m 107.73,1.31 c -0.96,-0.89 -2.06,-1.37 -3.29,-1.3 -1.23,0 -2.33,0.48 -3.22,1.44 l -7.27,7.54 20.36,19.67 7.33,-7.68 c 0.89,-0.89 1.23,-2.06 1.23,-3.29 0,-1.23 -0.48,-2.4 -1.37,-3.22 z M 8.35,5.09 h 50.2 V 18.13 H 14.58 c -0.42,0 -0.81,0.18 -1.09,0.46 -0.28,0.28 -0.46,0.67 -0.46,1.09 v 87.71 c 0,0.42 0.18,0.81 0.46,1.09 0.28,0.28 0.67,0.46 1.09,0.46 h 87.71 c 0.42,0 0.81,-0.18 1.09,-0.46 0.28,-0.28 0.46,-0.67 0.46,-1.09 V 65.1 h 13.04 v 48.51 c 0,2.31 -0.95,4.38 -2.46,5.89 -1.51,1.51 -3.61,2.46 -5.89,2.46 H 8.35 c -2.32,0 -4.38,-0.95 -5.89,-2.46 C 0.95,118 0,115.89 0,113.61 V 13.44 C 0,11.12 0.95,9.06 2.46,7.55 3.96,6.04 6.07,5.09 8.35,5.09 Z m 61.27,69.98 c -2.67,0.89 -5.42,1.71 -8.09,2.61 -2.67,0.89 -5.35,1.78 -8.09,2.67 -6.38,2.06 -9.87,3.22 -10.63,3.43 -0.75,0.21 -0.27,-2.74 1.3,-8.91 l 5.07,-19.4 0.42,-0.43 20.02,20.02 z M 57.01,47.34 88.44,14.7 108.8,34.3 77.02,67.35 Z"
+       id="path4"
+       style="fill:#448aff;fill-opacity:0.984314" /></g></svg>
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/script_picture_old.png b/ryvencore-qt/ryvencore_qt/resources/pics/script_picture_old.png
new file mode 100644
index 00000000..e476b414
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/pics/script_picture_old.png differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/variable_picture.png b/ryvencore-qt/ryvencore_qt/resources/pics/variable_picture.png
new file mode 100644
index 00000000..0feb1944
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/pics/variable_picture.png differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/pics/warning.png b/ryvencore-qt/ryvencore_qt/resources/pics/warning.png
new file mode 100644
index 00000000..a6557bef
Binary files /dev/null and b/ryvencore-qt/ryvencore_qt/resources/pics/warning.png differ
diff --git a/ryvencore-qt/ryvencore_qt/resources/warning.svg b/ryvencore-qt/ryvencore_qt/resources/warning.svg
new file mode 100644
index 00000000..4cb0a02e
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/resources/warning.svg
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="20.107mm"
+   height="16.165001mm"
+   viewBox="0 0 20.107 16.165001"
+   version="1.1"
+   id="svg5"
+   inkscape:version="1.1.2 (76b9e6a115, 2022-02-25)"
+   sodipodi:docname="warning.svg"
+   inkscape:export-filename="/home/leon/projects/ryven_projects/ryvencore-qt/ryvencore_qt/resources/pics/warning.png"
+   inkscape:export-xdpi="619.08533"
+   inkscape:export-ydpi="619.08533"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview7"
+     pagecolor="#505050"
+     bordercolor="#eeeeee"
+     borderopacity="1"
+     inkscape:pageshadow="0"
+     inkscape:pageopacity="0"
+     inkscape:pagecheckerboard="0"
+     inkscape:document-units="mm"
+     showgrid="false"
+     showguides="false"
+     inkscape:zoom="8.7679434"
+     inkscape:cx="28.34188"
+     inkscape:cy="28.455932"
+     inkscape:window-width="1920"
+     inkscape:window-height="1026"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1"
+     width="20.107mm" />
+  <defs
+     id="defs2">
+    <filter
+       style="color-interpolation-filters:sRGB;"
+       inkscape:label="Blur"
+       id="filter10145"
+       x="-0.037085046"
+       y="-0.046829941"
+       width="1.0741701"
+       height="1.0936633">
+      <feGaussianBlur
+         stdDeviation="0.05 0.05"
+         result="blur"
+         id="feGaussianBlur10143" />
+    </filter>
+    <filter
+       style="color-interpolation-filters:sRGB;"
+       inkscape:label="Blur"
+       id="filter10149"
+       x="-0.42208318"
+       y="-0.10964406"
+       width="1.844166"
+       height="1.2192303">
+      <feGaussianBlur
+         stdDeviation="0.05 0.05"
+         result="blur"
+         id="feGaussianBlur10147" />
+    </filter>
+    <filter
+       style="color-interpolation-filters:sRGB;"
+       inkscape:label="Blur"
+       id="filter10153"
+       x="-0.32602975"
+       y="-0.33282494"
+       width="1.6520263"
+       height="1.6656443">
+      <feGaussianBlur
+         stdDeviation="0.05 0.05"
+         result="blur"
+         id="feGaussianBlur10151" />
+    </filter>
+  </defs>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <path
+       style="fill:#172ec1;stroke:#448aff;stroke-width:1.165;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter10145);fill-opacity:0.43838"
+       d="M 0.5944035,15.594628 H 19.53607 L 10.065237,0.59462737 Z"
+       id="path857"
+       sodipodi:nodetypes="cccc"
+       inkscape:export-filename="/home/leon/projects/ryven_projects/ryvencore-qt/ryvencore_qt/resources/pics/warning.png"
+       inkscape:export-xdpi="619.08533"
+       inkscape:export-ydpi="619.08533" />
+    <path
+       style="fill:#448aff;fill-opacity:0.97647059;stroke:#448aff;stroke-width:0.865;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter10149)"
+       d="m 9.4346169,5.6529604 0.6306201,5.0390326 0.67835,-5.0390326 z"
+       id="path1735"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="opacity:1;fill:#448aff;fill-opacity:0.976471;stroke:#448aff;stroke-width:0.403;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter10153)"
+       id="path9573"
+       sodipodi:type="arc"
+       sodipodi:cx="10.069466"
+       sodipodi:cy="12.862857"
+       sodipodi:rx="0.49021924"
+       sodipodi:ry="0.4821817"
+       sodipodi:start="2.4387311"
+       sodipodi:end="2.3927417"
+       sodipodi:open="true"
+       sodipodi:arc-type="arc"
+       d="m 9.6954305,13.174541 a 0.49021924,0.4821817 0 0 1 0.052876,-0.675979 0.49021924,0.4821817 0 0 1 0.6878093,0.04423 0.49021924,0.4821817 0 0 1 -0.03706,0.676996 0.49021924,0.4821817 0 0 1 -0.688661,-0.02867" />
+  </g>
+</svg>
diff --git a/ryvencore-qt/ryvencore_qt/src/Design.py b/ryvencore-qt/ryvencore_qt/src/Design.py
new file mode 100644
index 00000000..0385d97d
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/Design.py
@@ -0,0 +1,114 @@
+import json
+
+from qtpy.QtCore import QObject, Signal
+from qtpy.QtGui import QFontDatabase
+
+from .flows.FlowTheme import FlowTheme, flow_themes
+from .GlobalAttributes import Location
+
+
+class Design(QObject):
+    """Design serves as a container for the stylesheet and flow themes, and sends signals to notify GUI elements
+    on change of the flow theme. A configuration for the flow themes can be loaded from a json file."""
+
+    global_stylesheet = ''
+
+    flow_theme_changed = Signal(str)
+    performance_mode_changed = Signal(str)
+
+    def __init__(self):
+        super().__init__()
+
+        self.flow_themes = flow_themes
+        self.flow_theme: FlowTheme = None
+        self.default_flow_size = None
+        self.performance_mode: str = None
+        self.node_item_shadows_enabled: bool = None
+        self.animations_enabled: bool = None
+        self.node_selection_stylesheet: str = None
+
+        # load standard default values
+        self._default_flow_theme = self.flow_themes[-1]
+        self.set_performance_mode('pretty')
+        self.set_animations_enabled(True)
+        self.default_flow_size = [1000, 700]
+        self.set_flow_theme(self._default_flow_theme)
+
+    @staticmethod
+    def register_fonts():
+        db = QFontDatabase()
+        db.addApplicationFont(
+            Location.PACKAGE_PATH + '/resources/fonts/poppins/Poppins-Medium.ttf'
+        )
+        db.addApplicationFont(
+            Location.PACKAGE_PATH + '/resources/fonts/source_code_pro/SourceCodePro-Regular.ttf'
+        )
+        db.addApplicationFont(
+            Location.PACKAGE_PATH + '/resources/fonts/asap/Asap-Regular.ttf'
+        )
+
+    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)
+
+        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)
+
+        if 'init performance mode' in IMPORT_DATA:
+            self.set_performance_mode(IMPORT_DATA['init performance mode'])
+
+        if 'init animations enabled' in IMPORT_DATA:
+            self.set_animations_enabled(IMPORT_DATA['init animations enabled'])
+
+        if 'default flow size' in IMPORT_DATA:
+            self.default_flow_size = IMPORT_DATA['default flow size']
+
+    def available_flow_themes(self) -> dict:
+        return {theme.name: theme for theme in self.flow_themes}
+
+    def flow_theme_by_name(self, name: str) -> FlowTheme:
+        for theme in self.flow_themes:
+            if theme.name.casefold() == name.casefold():
+                return theme
+        return None
+
+    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 and name != '':
+            self.flow_theme = self.flow_theme_by_name(name)
+        else:
+            return
+
+        self.node_selection_stylesheet = self.flow_theme.build_node_selection_stylesheet()
+
+        self.flow_theme_changed.emit(self.flow_theme.name)
+
+
+    def set_performance_mode(self, new_mode: str):
+        self.performance_mode = new_mode
+        if new_mode == 'fast':
+            self.node_item_shadows_enabled = False
+        else:
+            self.node_item_shadows_enabled = True
+
+        self.performance_mode_changed.emit(self.performance_mode)
+
+    def set_animations_enabled(self, b: bool):
+        self.animations_enabled = b
+
+    def set_node_item_shadows(self, b: bool):
+        self.node_item_shadows_enabled = b
diff --git a/ryvencore-qt/ryvencore_qt/src/GUIBase.py b/ryvencore-qt/ryvencore_qt/src/GUIBase.py
new file mode 100644
index 00000000..8029be66
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/GUIBase.py
@@ -0,0 +1,53 @@
+from ryvencore.Base import Base
+
+
+class GUIBase:
+    """Base class for GUI items that represent specific core components"""
+
+    # every frontend GUI object that represents some specific component from the core
+    # is stored there under the the global id of the represented component.
+    # used for completing data (serialization)
+    FRONTEND_COMPONENT_ASSIGNMENTS = {}  # component global id : GUI object
+
+    @staticmethod
+    def get_complete_data_function(session):
+        """
+        generates a function that searches through generated data by the core and calls
+        complete_data() on frontend components that represent them to add frontend data
+        """
+
+        def analyze(obj):
+            """Searches recursively through obj and calls complete_data(obj) on associated
+            frontend components (instances of GUIBase)"""
+
+            if isinstance(obj, dict):
+                GID = obj.get('GID')
+                if GID is not None:
+                    # find representative
+                    comp = GUIBase.FRONTEND_COMPONENT_ASSIGNMENTS.get(GID)
+                    if comp:
+                        obj = comp.complete_data(obj)
+
+                # look for child objects
+                for key, value in obj.items():
+                    obj[key] = analyze(value)
+
+            elif isinstance(obj, list):
+                for i in range(len(obj)):
+                    item = obj[i]
+                    item = analyze(item)
+                    obj[i] = item
+
+            return obj
+
+        return analyze
+
+    def __init__(self, representing_component: Base = None):
+        """parameter `representing` indicates representation of a specific core component"""
+        if representing_component is not None:
+            GUIBase.FRONTEND_COMPONENT_ASSIGNMENTS[representing_component.global_id] = self
+
+    # OVERRIDE
+    def complete_data(self, data: dict) -> dict:
+        """completes the data dict of the represented core component by adding all frontend data"""
+        return data
diff --git a/ryvencore-qt/ryvencore_qt/src/GlobalAttributes.py b/ryvencore-qt/ryvencore_qt/src/GlobalAttributes.py
new file mode 100644
index 00000000..8df020f9
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/GlobalAttributes.py
@@ -0,0 +1,2 @@
+class Location:
+    PACKAGE_PATH = None
diff --git a/ryvencore-qt/ryvencore_qt/src/SessionGUI.py b/ryvencore-qt/ryvencore_qt/src/SessionGUI.py
new file mode 100644
index 00000000..80f080ef
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/SessionGUI.py
@@ -0,0 +1,82 @@
+from typing import List
+
+from qtpy.QtCore import QObject, Signal, Qt
+from qtpy.QtWidgets import QWidget, QApplication
+
+import ryvencore
+
+from .flows.FlowView import FlowView
+from .Design import Design
+from .GUIBase import GUIBase
+
+
+class SessionGUI(GUIBase, QObject):
+    """
+    ryvencore-qt's Session wrapper class, implementing the GUI.
+    Any session with a GUI must be created through this class.
+    Access the ryvencore session through the :code:`session`
+    attribute, and the GUI from the ryvencore session through the
+    :code:`gui` attribute. Once instantiated, you can simply use
+    the :code:`session` directly to create, rename, delete flows,
+    register nodes, etc.
+    """
+
+    flow_created = Signal(object)
+    flow_deleted = Signal(object)
+    flow_renamed = Signal(object, str)
+    flow_view_created = Signal(object, object)
+
+    def __init__(self, gui_parent: QWidget):
+        GUIBase.__init__(self)
+        QObject.__init__(self)
+
+        self.core_session = ryvencore.Session(gui=True, load_addons=True)
+        setattr(self.core_session, 'gui', self)
+
+        self.gui_parent = gui_parent
+
+        # flow views
+        self.flow_views = {}  # {Flow : FlowView}
+
+        # register complete_data function
+        ryvencore.set_complete_data_func(self.get_complete_data_function(self))
+
+        # load design
+        app = QApplication.instance()
+        app.setAttribute(Qt.AA_UseHighDpiPixmaps)
+        Design.register_fonts()
+        self.design = Design()
+
+        # connect to session
+        self.core_session.flow_created.sub(self._flow_created)
+        self.core_session.flow_deleted.sub(self._flow_deleted)
+        self.core_session.flow_renamed.sub(self._flow_renamed)
+
+    def _flow_created(self, flow: ryvencore.Flow):
+        """
+        Builds the flow view for a newly created flow, saves it in
+        self.flow_views, and emits the flow_view_created signal.
+        """
+        self.flow_created.emit(flow)
+
+        self.flow_views[flow] = FlowView(
+            session_gui=self,
+            flow=flow,
+            parent=self.gui_parent,
+        )
+        self.flow_view_created.emit(flow, self.flow_views[flow])
+
+        return flow
+
+    def _flow_deleted(self, flow: ryvencore.Flow):
+        """
+        Removes the flow view for a deleted flow from self.flow_views.
+        """
+        self.flow_views.pop(flow)
+        self.flow_deleted.emit(flow)
+
+    def _flow_renamed(self, flow: ryvencore.Flow, new_name: str):
+        """
+        Renames the flow view for a renamed flow.
+        """
+        self.flow_renamed.emit(flow, new_name)
\ No newline at end of file
diff --git a/ryvencore-qt/ryvencore_qt/src/__init__.py b/ryvencore-qt/ryvencore_qt/src/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/FlowCommands.py b/ryvencore-qt/ryvencore_qt/src/flows/FlowCommands.py
new file mode 100644
index 00000000..bd01a08e
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/FlowCommands.py
@@ -0,0 +1,317 @@
+"""
+This file contains the implementations of undoable actions for FlowView.
+"""
+
+
+from qtpy.QtCore import QObject, QPointF
+from qtpy.QtWidgets import QUndoCommand
+
+from .drawings.DrawingObject import DrawingObject
+from .nodes.NodeItem import NodeItem
+from typing import Tuple
+from ryvencore.NodePort import NodePort, NodeInput, NodeOutput
+
+
+class FlowUndoCommand(QObject, QUndoCommand):
+    """
+    The main difference to normal QUndoCommands is the activate 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(), so to prevent those commands to be added to the
+    undo stack before the parent command, it is here blocked at first.
+    """
+
+    def __init__(self, flow_view):
+
+        self.flow_view = flow_view
+        self.flow = flow_view.flow
+        self._activated = False
+
+        QObject.__init__(self)
+        QUndoCommand.__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.items_list = items_list
+        self.p_from = p_from
+        self.p_to = p_to
+        self.last_item_group_pos = p_to
+
+    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):
+        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_view.scene().createItemGroup(self.items_list)
+
+    def destroy_items_group(self, items_group):
+        self.flow_view.scene().destroyItemGroup(items_group)
+
+
+class PlaceNode_Command(FlowUndoCommand):
+
+    def __init__(self, flow_view, node_class, pos):
+        super().__init__(flow_view)
+
+        self.node_class = node_class
+        self.node = None
+        self.item_pos = pos
+
+    def undo_(self):
+        self.flow.remove_node(self.node)
+
+    def redo_(self):
+        if self.node:
+            self.flow.add_node(self.node)
+        else:
+            self.node = self.flow.create_node(self.node_class)
+
+
+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):
+        # 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.flow_view.remove_component(self.drawing)
+
+    def redo_(self):
+        self.flow_view.add_drawing(self.drawing, self.drawing_obj_pos)
+
+
+class RemoveComponents_Command(FlowUndoCommand):
+
+    def __init__(self, flow_view, items):
+        super().__init__(flow_view)
+
+        self.items = items
+        self.broken_connections = []  # the connections that go beyond the removed nodes and need to be restored in undo
+        self.internal_connections = set()
+
+        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)
+
+        for n in self.nodes:
+            for i in n.inputs:
+                cp = n.flow.connected_output(i)
+                if cp is not None:
+                    cn = cp.node
+                    if cn not in self.nodes:
+                        self.broken_connections.append((cp, i))
+                    else:
+                        self.internal_connections.add((cp, i))
+            for o in n.outputs:
+                for cp in n.flow.connected_inputs(o):
+                    cn = cp.node
+                    if cn not in self.nodes:
+                        self.broken_connections.append((o, cp))
+                    else:
+                        self.internal_connections.add((o, cp))
+
+    def undo_(self):
+
+        # add nodes
+        for n in self.nodes:
+            self.flow.add_node(n)
+
+        # add drawings
+        for d in self.drawings:
+            self.flow_view.add_drawing(d)
+
+        # add connections
+        self.restore_broken_connections()
+        self.restore_internal_connections()
+
+    def redo_(self):
+
+        # remove connections
+        self.remove_broken_connections()
+        self.remove_internal_connections()
+
+        # remove nodes
+        for n in self.nodes:
+            self.flow.remove_node(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.add_connection(c)
+
+    def remove_internal_connections(self):
+        for c in self.internal_connections:
+            self.flow.remove_connection(c)
+
+    def restore_broken_connections(self):
+        for c in self.broken_connections:
+            self.flow.add_connection(c)
+
+    def remove_broken_connections(self):
+        for c in self.broken_connections:
+            self.flow.remove_connection(c)
+
+
+class ConnectPorts_Command(FlowUndoCommand):
+
+    def __init__(self, flow_view, out, inp):
+        super().__init__(flow_view)
+
+        # CAN ALSO LEAD TO DISCONNECT INSTEAD OF CONNECT!!
+
+        self.out = out
+        self.inp = inp
+        self.connection = None
+        self.connecting = True
+
+        for i in flow_view.flow.connected_inputs(out):
+            if i == self.inp:
+                self.connection = (out, i)
+                self.connecting = False
+
+
+    def undo_(self):
+        if self.connecting:
+            # remove connection
+            self.flow.remove_connection(self.connection)
+        else:
+            # recreate former connection
+            self.flow.add_connection(self.connection)
+
+    def redo_(self):
+        if self.connecting:
+            if self.connection:
+                self.flow.add_connection(self.connection)
+            else:
+                # connection hasn't been created yet
+                self.connection = self.flow.connect_nodes(self.out, self.inp)
+        else:
+            # remove existing connection
+            self.flow.remove_connection(self.connection)
+
+
+
+
+class Paste_Command(FlowUndoCommand):
+
+    def __init__(self, flow_view, data, offset_for_middle_pos):
+        super().__init__(flow_view)
+
+        self.data = data
+        self.modify_data_positions(offset_for_middle_pos)
+        self.pasted_components = None
+
+
+    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 redo_(self):
+        if self.pasted_components is None:
+            self.pasted_components = {}
+
+            # create components
+            self.create_drawings()
+
+            self.pasted_components['nodes'], self.pasted_components['connections'] = \
+                self.flow.load_components(
+                    nodes_data=self.data['nodes'],
+                    conns_data=self.data['connections'],
+                    output_data=self.data['output data'],
+                )
+
+            self.select_new_components_in_view()
+        else:
+            self.add_existing_components()
+
+    def undo_(self):
+        # remove components and their items from flow
+        for c in self.pasted_components['connections']:
+            self.flow.remove_connection(c)
+        for n in self.pasted_components['nodes']:
+            self.flow.remove_node(n)
+        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.flow.add_node(n)
+        for c in self.pasted_components['connections']:
+            self.flow.add_connection(c)
+        for d in self.pasted_components['drawings']:
+            self.flow_view.add_drawing(d)
+
+        self.select_new_components_in_view()
+
+    def select_new_components_in_view(self):
+        self.flow_view.clear_selection()
+        for d in self.pasted_components['drawings']:
+            d: DrawingObject
+            d.setSelected(True)
+        for n in self.pasted_components['nodes']:
+            n: NodeItem
+            ni: NodeItem = self.flow_view.node_items[n]
+            ni.setSelected(True)
+
+    def create_drawings(self):
+        drawings = []
+        for d in self.data['drawings']:
+            new_drawing = self.flow_view.create_drawing(d)
+            self.flow_view.add_drawing(new_drawing, posF=QPointF(d['pos x'], d['pos y']))
+            drawings.append(new_drawing)
+        self.pasted_components['drawings'] = drawings
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/FlowTheme.py b/ryvencore-qt/ryvencore_qt/src/flows/FlowTheme.py
new file mode 100644
index 00000000..5362e2a0
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/FlowTheme.py
@@ -0,0 +1,1627 @@
+from qtpy.QtCore import Qt, QPointF, QPoint, QRectF, QMargins, QMarginsF
+from qtpy.QtGui import QColor, QPainter, QBrush, QRadialGradient, QLinearGradient, QPen, QPainterPath, QFont, QPolygon
+from qtpy.QtWidgets import QStyle, QStyleOption
+
+from ..utils import pythagoras
+
+
+#
+#   notice: available themes are hardcoded in Ryven for CLI; make sure to update those
+#   in case of changes affecting it
+#
+
+
+class FlowTheme:
+
+    name = ''
+    type_ = 'dark'
+
+    node_selection_stylesheet__base = '''
+QScrollArea {
+    border: 0px solid grey;
+    border-radius: 0px;
+}
+NodeWidget {
+
+}
+'''
+    node_selection_stylesheet = ''
+
+    header_padding = (10, 2, 10, 2)  # (left, top, right, botton)
+
+    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):
+        for k, v in imported.items():
+
+            if k == 'exec connection color':
+                self.exec_conn_color = self.hex_to_col(v)
+            elif k == 'exec connection width':
+                self.exec_conn_width = v
+            elif k == 'exec connection pen style':
+                self.exec_conn_pen_style = self._parse_pen_style(v)
+
+            elif k == 'data connection color':
+                self.data_conn_color = self.hex_to_col(v)
+            elif k == 'data connection width':
+                self.data_conn_width = v
+            elif k == 'data connection pen style':
+                self.data_conn_pen_style = self._parse_pen_style(v)
+
+            elif k == 'flow background color':
+                self.flow_background_brush.setColor(self.hex_to_col(v))
+
+    def build_node_selection_stylesheet(self):
+        return self.node_selection_stylesheet__base + '\n' + self.node_selection_stylesheet
+
+    def paint_NI_title_label(self, node_gui, selected: bool, hovering: bool, painter: QPainter, option: QStyleOption,
+                             node_style: str, node_title: str, node_color: QColor, node_item_bounding_rect):
+        pass
+
+    def paint_PI_label(self, node_gui, painter: QPainter, option: QStyleOption, type_: str, connected: bool, label_str: str,
+                       node_color: QColor, bounding_rect: QRectF):
+        pass
+
+    def paint_PI(self, node_gui, painter: QPainter, option: QStyleOption, node_color: QColor, type_: str, connected: bool,
+                 rect: QRectF):  # padding, w, h):
+        pass
+
+    def paint_NI(self, node_gui,
+                 selected: bool, hovered: bool, node_style: str,
+                 painter: QPainter, option: QStyleOption,
+                 color: QColor, w, h, bounding_rect, title_rect):
+
+        painter.setRenderHint(QPainter.Antialiasing)
+
+        if node_style == 'normal':
+            self.draw_NI_normal(node_gui, selected, hovered, painter, color, w, h, bounding_rect, title_rect)
+        elif node_style == 'small':
+            self.draw_NI_small(node_gui, selected, hovered, painter, color, w, h, bounding_rect)
+
+    def draw_NI_normal(self, node_gui, selected: bool, hovered: bool,
+                       painter: QPainter, c: QColor, w, h, bounding_rect, title_rect):
+        pass
+
+    def draw_NI_small(self, node_gui, selected: bool, hovered: bool,
+                      painter: QPainter, c: QColor, w, h, bounding_rect, background_color=None):
+        pass
+
+    def paint_NI_selection_border(self, ni, painter: QPainter, color: QColor, w, h, bounding_rect):
+        pen = QPen(self.flow_highlight_pen_color)
+        pen.setWidth(3)
+        painter.setPen(pen)
+        painter.setBrush(Qt.NoBrush)
+
+        size_factor = 1.2
+
+        rect = QRectF(bounding_rect)
+        rect.setWidth(w*size_factor)
+        rect.setHeight(h*size_factor)
+        rect.setX(rect.x() - w*size_factor/2)
+        rect.setY(rect.y() - h*size_factor/2)
+
+        painter.drawRoundedRect(rect, 10, 10)
+
+    @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())
+
+        if node_style == 'normal':
+            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.0 * 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
+
+    @staticmethod
+    def col(c: QColor, alpha=255):
+        return QColor(c.red(), c.green(), c.blue(), alpha)
+
+    @staticmethod
+    def _parse_pen_style(s: str):
+        if s == 'solid line':
+            return Qt.SolidLine
+        elif s == 'dash line':
+            return Qt.DashLine
+        elif s == 'dash dot line':
+            return Qt.DashDotLine
+        elif s == 'dash dot dot line':
+            return Qt.DashDotDotLine
+        elif s == 'dot line':
+            return Qt.DotLine
+
+
+class FlowTheme_Toy(FlowTheme):
+    name = 'Toy'
+
+    node_selection_stylesheet = ''
+
+    header_padding = (12, 5, 10, 2)
+
+    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_gui, selected, hovering, painter, option, node_style, node_title, node_color,
+                             node_item_bounding_rect):
+
+        if node_style == 'normal':
+            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_gui, 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_gui, painter, option, node_color, type_, connected, rect):
+
+        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(rect)
+
+    def draw_NI_normal(self, node_gui, selected: bool, hovered: bool,
+                       painter: QPainter, c: QColor, w, h, bounding_rect, title_rect):
+
+        # main rect
+        header_color = QColor(c.red() / 10 + 100, c.green() / 10 + 100, c.blue() / 10 + 100)
+        if selected:
+            header_color = header_color.lighter()
+        body_gradient = QRadialGradient(bounding_rect.topLeft(), pythagoras(h, w))
+        body_gradient.setColorAt(0, self.col(header_color, alpha=200))
+        body_gradient.setColorAt(1, self.col(header_color, alpha=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_gui, selected: bool, hovered: bool,
+                      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'
+
+    node_selection_stylesheet = ''
+
+    header_padding = (12, 5, 10, 2)
+
+    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_gui, selected, hovering, painter, option, node_style, node_title, node_color,
+                             node_item_bounding_rect):
+        if node_style == 'normal':
+            self.paint_NI_title_label_default(
+                painter=painter,
+                node_style=node_style,
+                title=node_title,
+                color=node_color if not (hovering or selected) else node_color.lighter().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_gui, 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_gui, painter, option, node_color, type_, connected, rect):
+
+        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(rect)
+
+    # 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 == 'normal':
+    #         self.draw_NI_normal(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_normal(self, node_gui, selected: bool, hovered: bool,
+                       painter, c: QColor, w: int, h: int, bounding_rect, title_rect):
+
+        background_color = QColor('#212224')
+        painter.setBrush(background_color)
+        pen = QPen(c if not selected else c.lighter())
+        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_gui, selected: bool, hovered: bool,
+                      painter: QPainter, c: QColor, w, h, bounding_rect, background_color=None):
+
+        if hovered:
+            background_color = c.darker()
+        else:
+            background_color = QColor('#212429')
+
+        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'
+
+    node_selection_stylesheet = ''
+
+    header_padding = (12, 6, 10, 2)
+
+    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):
+        super()._load(imported)
+
+        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_gui, selected, hovering, painter, option, node_style, node_title, node_color,
+                             node_item_bounding_rect):
+
+        if node_style == 'normal':
+            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_gui, 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_gui, painter, option, node_color, type_, connected, rect):
+
+        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(rect.marginsRemoved(QMarginsF(3, 3, 3, 3)))
+
+    # 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 == 'normal':
+    #         self.draw_NI_normal(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_normal(self, node_gui, selected: bool, hovered: bool,
+                       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 if not selected else 5)
+        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_gui, selected: bool, hovered: bool,
+                      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'
+
+    node_selection_stylesheet = ''
+
+    header_padding = (5, 0, 0, 0)
+
+    exec_conn_color = QColor(0, 17, 25)
+    exec_conn_width = 3
+    exec_conn_pen_style = Qt.SolidLine
+
+    data_conn_color = QColor(200, 200, 200)
+    data_conn_width = 2.5
+    data_conn_pen_style = Qt.SolidLine
+
+    flow_background_color = QColor('#232323')
+    flow_background_brush = QBrush(flow_background_color)
+
+    node_color = QColor('#3f3f3f')
+    corner_radius_normal = 5
+    corner_radius_small = 10
+
+    EXPORT = [
+        'nodes color',
+        'flow background color'
+    ]
+
+    def _load(self, imported: dict):
+        super()._load(imported)
+
+        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_gui, selected, hovering, painter, option, node_style, node_title, node_color,
+                             node_item_bounding_rect):
+        if node_style == 'normal':
+            self.paint_NI_title_label_default(
+                painter=painter,
+                node_style=node_style,
+                title=node_title,
+                color=QColor('#FFFFFF'),
+                pen_w=2,
+                font=QFont('Segoe UI', 11),
+                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('Segoe UI', 15, QFont.Bold, True),
+                node_item_bounding_rect=node_item_bounding_rect
+            )
+
+    def paint_PI_label(self, node_gui, 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_gui, painter, option, node_color, type_, connected, rect):
+
+        color = QColor('#FFFFFF') if type_ == 'exec' else node_color
+
+        painter.setBrush(QBrush(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(rect.marginsRemoved(QMarginsF(2, 2, 2, 2)))
+
+    def draw_NI_normal(self, node_gui, selected: bool, hovered: bool,
+                       painter, c, w, h, bounding_rect, title_rect):
+
+        background_color = self.node_color
+        header_color = QColor(c.red(), c.green(), c.blue(), 180)
+        if selected:
+            header_color = header_color.lighter()
+
+
+        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
+            if not selected else
+            QPen(QColor(200, 200, 200))
+        )
+        painter.drawRoundedRect(bounding_rect, self.corner_radius_normal, self.corner_radius_normal)
+
+    def draw_NI_small(self, node_gui, selected: bool, hovered: bool,
+                      painter, c, w, h, bounding_rect, background_color=None):
+
+        background_color = QColor('#212429')
+        painter.setBrush(self.interpolate_color(c, background_color, 0.97))
+        painter.setPen(
+            QPen(c)
+            if not selected else
+            QPen(QColor(200, 200, 200))
+        )
+        painter.drawRoundedRect(bounding_rect, self.corner_radius_small, self.corner_radius_small)
+
+
+class FlowTheme_Simple(FlowTheme):
+    name = 'Simple'
+    type_ = 'dark'
+
+    node_selection_stylesheet = ''
+
+    header_padding = (10, 2, 10, 2)
+
+    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):
+        super()._load(imported)
+
+        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_gui, selected, hovering, painter, option, node_style, node_title, node_color,
+                             node_item_bounding_rect):
+        if node_style == 'normal':
+            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_gui, 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_gui, painter, option, node_color, type_, connected, rect):
+
+        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(rect.marginsRemoved(QMarginsF(2, 2, 2, 2)))
+        # painter.drawEllipse(QRectF(padding+w/8, padding+h/8, 3*w/4, 3*h/4))
+
+    def draw_NI_normal(self, node_gui, selected: bool, hovered: bool,
+                       painter, c, w, h, bounding_rect, title_rect):
+
+        background_color = self.node_background_color
+
+        if selected:
+            header_color = c.lighter()
+        else:
+            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_gui, selected: bool, hovered: bool,
+                      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'
+
+    node_selection_stylesheet = ''
+
+    header_padding = (5, 2, 10, 0)
+
+    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):
+        super()._load(imported)
+
+        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_gui, selected, hovering, painter, option, node_style, node_title, node_color,
+                             node_item_bounding_rect):
+        if node_style == 'normal':
+            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_gui, 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_gui, painter, option, node_color, type_, connected, rect):
+
+        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(rect.marginsRemoved(QMarginsF(2, 2, 2, 2)))
+        # painter.drawEllipse(QRectF(padding+w/8, padding+h/8, 3*w/4, 3*h/4))
+
+    def draw_NI_normal(self, node_gui, selected: bool, hovered: bool,
+                       painter, c, w, h, bounding_rect: QRectF, title_rect):
+
+        if selected:
+            background_color = self.interpolate_color(self.nodes_background_color, c.darker(), 0.18)
+        else:
+            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_gui, selected: bool, hovered: bool,
+                      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_PureDark(FlowTheme):
+    name = 'pure dark'
+
+    node_selection_stylesheet = ''
+
+    header_padding = (4, 2, 2, 2)
+
+    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_item_shadow_color = QColor('#101010')
+
+    node_normal_bg_col = QColor('#0C1116')
+    node_small_bg_col = 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):
+        super()._load(imported)
+
+        for k, v in imported.items():
+            if k == 'extended node background color':
+                self.node_normal_bg_col = self.hex_to_col(v)
+            elif k == 'small node background color':
+                self.node_small_bg_col = 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_gui, selected, hovering, painter, option, node_style, node_title, node_color,
+                             node_item_bounding_rect):
+
+        painter.setPen(QPen(self.node_title_color))
+
+        if node_style == 'normal':
+            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_gui, 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_gui, painter, option, node_color, type_, connected, rect):
+
+        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)
+
+        # rect = QRectF(padding + w / 8, padding + h / 8, 3 * w / 4, 3 * h / 4)
+        if type_ == 'exec':
+            painter.setBrush(QBrush(QColor('white')))
+        # painter.drawEllipse(rect)
+        painter.drawEllipse(rect.marginsRemoved(QMarginsF(2, 2, 2, 2)))
+
+    def draw_NI_normal(self, node_gui, selected: bool, hovered: bool,
+                       painter, c, w, h, bounding_rect: QRectF, title_rect):
+
+        if selected:
+            background_color = self.interpolate_color(self.node_normal_bg_col, c.darker(), 0.18)
+        else:
+            background_color = self.node_normal_bg_col
+
+        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_gui, selected: bool, hovered: bool,
+                      painter, c, w, h, bounding_rect, background_color=None):
+
+        painter.setBrush(QBrush(self.node_small_bg_col))
+        painter.setPen(Qt.NoPen)
+        painter.drawRoundedRect(bounding_rect, 4, 4)
+
+
+class FlowTheme_PureLight(FlowTheme_PureDark):
+    name = 'pure light'
+    type_ = 'light'
+
+    header_padding = (2, 2, 2, 2)
+
+    node_selection_stylesheet = '''
+NodeSelectionWidget {
+    background-color: white;
+}
+NodeWidget {
+    background-color: white;
+}
+    '''
+
+    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_normal_bg_col = QColor('#cdcfd1')
+    node_small_bg_col = QColor('#bebfc1')
+    node_title_color = QColor('#1f1f1f')
+    port_pin_pen_color = QColor('#1f1f1f')
+
+    node_item_shadow_color = QColor('#cccccc')
+
+
+class FlowTheme_Colorful(FlowTheme):
+    name = 'colorful dark'
+
+    header_padding = (12, 0, 2, 2)
+
+    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):
+        super()._load(imported)
+
+        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_gui, selected, hovering, painter, option, node_style, node_title, node_color,
+                             node_item_bounding_rect):
+
+        painter.setPen(QPen(self.node_title_color))
+
+        if node_style == 'normal':
+            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_gui, 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_gui, painter, option, node_color, type_, connected, rect):
+
+        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(rect.marginsRemoved(QMarginsF(2, 2, 2, 2)))
+        # painter.drawEllipse(QRectF(padding + w / 8, padding + h / 8, 3 * w / 4, 3 * h / 4))
+
+    def draw_NI_normal(self, node_gui, selected: bool, hovered: bool,
+                       painter, c, w, h, bounding_rect: QRectF, title_rect):
+
+        background_color = c
+        background_color.setAlpha(150)
+
+        if selected:
+            header_color = QColor(c.red(), c.green(), c.blue(), 130)
+        else:
+            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_gui, selected: bool, hovered: bool,
+                      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_ColorfulLight(FlowTheme_Colorful):
+    name = 'colorful light'
+    type_ = 'light'
+
+    header_padding = (12, 0, 2, 2)
+
+    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')
+
+    node_item_shadow_color = QColor('#cccccc')
+
+    def paint_PI_label(self, node_gui, 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_normal(self, node_gui, selected: bool, hovered: bool, painter, c, w, h, bounding_rect: QRectF, title_rect):
+
+        background_color = c.lighter()
+        background_color.setAlpha(150)
+
+        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_gui, selected: bool, hovered: bool,
+                      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_Industrial(FlowTheme):
+    name = 'Industrial'
+
+    header_padding = (8, 0, 8, 0)  # (8, 6, 10, 2)
+
+    exec_conn_color = QColor(255, 255, 255)
+    exec_conn_width = 2
+    exec_conn_pen_style = Qt.SolidLine
+
+    data_conn_color = QColor(255, 194, 45)
+    data_conn_width = 2
+    data_conn_pen_style = Qt.DashLine
+
+    flow_background_brush = QBrush(QColor(19, 19, 19))
+    flow_background_grid = ('points', QColor(80, 80, 80), 2, 30, 30)
+
+    node_color = QColor(10, 10, 10, 250)
+    node_item_shadow_color = QColor(0, 0, 0)
+
+    def paint_NI_title_label(self, node_gui, selected, hovering, painter, option, node_style, node_title, node_color,
+                             node_item_bounding_rect):
+        if node_style == 'normal':
+            self.paint_NI_title_label_default(
+                painter=painter,
+                node_style=node_style,
+                title=node_title,
+                color=QColor(200, 200, 200),
+                pen_w=1,
+                font=QFont('Segoe UI', 11, QFont.Normal if not (hovering or selected) else 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('Segoe UI', 15, QFont.Bold),
+                node_item_bounding_rect=node_item_bounding_rect
+            )
+
+    def paint_PI_label(self, node_gui, painter, option, type_, connected, label_str, node_color, bounding_rect):
+        c = QColor('#FFFFFF')
+        self.paint_PI_label_default(painter, label_str, c, QFont("Segoe UI", 8, QFont.Normal), bounding_rect)
+
+    def paint_PI(self, node_gui, painter, option, node_color, type_, connected, rect):
+
+        color = QColor('#FFFFFF') if type_ == 'exec' else node_color
+
+        # add = 2
+        # padd = padding + add
+        outer_ellipse_rect = rect.marginsRemoved(QMarginsF(2, 2, 2, 2))  # QRectF(padd, padd, w - 2*padd, h - 2*padd)
+        # add = 4
+        # padd = padding + add
+        inner_ellipse_rect = rect.marginsRemoved(QMarginsF(4, 4, 4, 4))  # QRectF(padd, padd, w - 2*padd, h - 2*padd)
+
+        if type_ == 'exec':
+
+            if connected:
+                brush = QBrush(QColor(255, 255, 255, 200))
+                painter.setBrush(brush)
+            else:
+                painter.setBrush(Qt.NoBrush)
+
+            rect_ = rect.marginsRemoved(QMarginsF(2, 2, 2, 2))
+
+            painter.setPen(QPen(QColor(255, 255, 255)))
+
+            painter.drawPolygon(QPolygon([
+                rect_.topLeft().toPoint(),
+                QPoint(rect_.right(), rect_.center().toPoint().y()),
+                rect_.bottomLeft().toPoint(),
+            ]))
+
+        elif type_ == 'data':
+
+            pen = QPen(color)
+            pen.setWidth(1)
+            painter.setPen(pen)
+
+            if connected:
+                # draw inner ellipse
+                brush = QBrush(QColor(node_color.red(), node_color.green(), node_color.blue(), 200))
+                painter.setBrush(brush)
+                painter.drawEllipse(inner_ellipse_rect)
+
+            # draw outer ellipse
+            painter.setBrush(Qt.NoBrush)
+            painter.drawEllipse(outer_ellipse_rect)
+
+    def draw_NI_normal(self, node_gui, selected: bool, hovered: bool,
+                       painter, c, w, h, bounding_rect, title_rect):
+
+        background_color = QColor(14, 14, 14)
+        header_color = QColor(105, 105, 105, 150)
+
+        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, 2, 2)
+
+    def draw_NI_small(self, node_gui, selected: bool, hovered: bool,
+                      painter, c, w, h, bounding_rect, background_color=None):
+
+        background_color = QColor(217, 217, 217, 50)
+        c_s = 2
+        painter.setBrush(self.interpolate_color(c, background_color, 0.97))
+        pen = QPen(QColor(130, 130, 130))
+        painter.setPen(pen)
+        painter.drawRoundedRect(bounding_rect, c_s, c_s)
+
+
+class FlowTheme_Fusion(FlowTheme):
+    name = 'Fusion'
+    type_ = 'light'
+
+    node_selection_stylesheet = '''
+    NodeSelectionWidget {
+        background-color: white;
+    }
+    NodeWidget {
+        background-color: white;
+    }
+        '''
+
+    exec_conn_color = QColor('#1f1f1f')
+    exec_conn_width = 2
+
+    data_conn_color = QColor('#1f1f1f')
+    data_conn_width = 2
+
+    flow_background_brush = QBrush(QColor('#ffffff'))
+
+    node_normal_bg_col = QColor('#ebeced')
+    node_small_bg_col = QColor('#cccdcf')
+    node_title_color = QColor('#1f1f1f')
+    port_pin_pen_color = QColor('#1f1f1f')
+
+    node_item_shadow_color = QColor('#cccccc')
+
+
+    def paint_NI_title_label(self, node_gui, selected, hovering, painter, option, node_style, node_title, node_color,
+                             node_item_bounding_rect):
+
+        painter.setPen(QPen(self.node_title_color))
+
+        if node_style == 'normal':
+            painter.setFont(QFont('Segoe UI', 10))
+            align = Qt.AlignLeft | Qt.AlignVCenter
+        else:
+            painter.setFont(QFont('Segoe UI', 12))
+            align = Qt.AlignCenter
+
+        painter.drawText(node_item_bounding_rect, align, node_title)
+
+
+    def paint_PI_label(self, node_gui, painter, option, type_, connected, label_str, node_color, bounding_rect):
+        pen = QPen(QColor('#000000'))
+        pen.setWidthF(1.2)
+        painter.setPen(pen)
+
+        self.paint_PI_label_default(painter, label_str, QColor(0, 0, 0), QFont("Segoe UI", 8), bounding_rect)
+
+
+    def paint_PI(self, node_gui, painter, option, node_color, type_, connected, rect):
+
+        painter.setBrush(QColor('#000000'))
+        painter.setPen(Qt.NoPen)
+
+        if type_ == 'data':
+            painter.drawEllipse(rect.marginsRemoved(QMarginsF(3, 3, 3, 3)))
+        else:
+            draw_rect = rect.marginsRemoved(QMarginsF(3, 3, 3, 3))
+            path = QPainterPath(draw_rect.topLeft())
+            path.lineTo(QPointF(draw_rect.right(), draw_rect.center().y()))
+            path.lineTo(draw_rect.bottomLeft())
+            path.closeSubpath()
+
+            painter.drawPath(path)
+
+
+    def draw_NI_normal(self, node_gui, selected: bool, hovered: bool,
+                       painter, c, w, h, bounding_rect: QRectF, title_rect):
+
+        pen = QPen(c)
+        col_top = self.node_normal_bg_col.lighter(105)
+        col_bottom = self.node_normal_bg_col
+
+        if not selected:
+            pen.setWidthF(1)
+        else:
+            pen.setWidthF(2.5)
+            col_bottom = QColor(255, 255, 255)
+
+        header_height = self.get_header_rect(w, h, title_rect).height()
+        header_fraction = header_height / bounding_rect.height()
+
+        gradient = QLinearGradient(bounding_rect.topLeft(), bounding_rect.bottomLeft())
+        gradient.setColorAt(0, col_top)
+        gradient.setColorAt(header_fraction, self.interpolate_color(col_top, col_bottom, 0.7))
+        gradient.setColorAt(1, col_bottom)
+
+        painter.setBrush(QBrush(gradient))
+        painter.setPen(pen)
+        painter.drawRoundedRect(bounding_rect, 3, 3)
+
+
+    def draw_NI_small(self, node_gui, selected: bool, hovered: bool,
+                      painter, c, w, h, bounding_rect, background_color=None):
+
+        painter.setBrush(QBrush(self.node_small_bg_col))
+        painter.setPen(Qt.NoPen)
+        painter.drawRoundedRect(bounding_rect, 4, 4)
+
+
+flow_themes = [
+    FlowTheme_Toy(),
+    FlowTheme_DarkTron(),
+    FlowTheme_Ghost(),
+    FlowTheme_Blender(),
+    FlowTheme_Simple(),
+    FlowTheme_Ueli(),
+    FlowTheme_PureDark(),
+    FlowTheme_Colorful(),
+    FlowTheme_PureLight(),
+    FlowTheme_ColorfulLight(),
+    FlowTheme_Industrial(),
+    FlowTheme_Fusion(),
+]
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/FlowView.py b/ryvencore-qt/ryvencore_qt/src/flows/FlowView.py
new file mode 100644
index 00000000..da6bc42c
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/FlowView.py
@@ -0,0 +1,1202 @@
+import json
+
+from typing import Tuple
+
+from qtpy.QtCore import Qt, QPointF, QPoint, QRectF, QSizeF, Signal, QTimer, QTimeLine, QEvent
+from qtpy.QtGui import QPainter, QPen, QColor, QKeySequence, QTabletEvent, QImage, QGuiApplication, QFont, QTouchEvent
+from qtpy.QtWidgets import QGraphicsView, QGraphicsScene, QShortcut, QMenu, QGraphicsItem, QUndoStack
+
+from ryvencore.Flow import Flow
+from ryvencore.Node import Node
+from ryvencore.NodePort import NodePort, NodeInput, NodeOutput
+#from ryvencore.Connection import Connection, DataConnection
+from ryvencore.InfoMsgs import InfoMsgs
+from ryvencore.RC import PortObjPos
+from ryvencore.utils import node_from_identifier
+
+from ..GUIBase import GUIBase
+from ..utils import *
+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 .node_list_widget.NodeListWidget import NodeListWidget
+from .nodes.NodeGUI import NodeGUI
+from .nodes.NodeItem import NodeItem
+from .nodes.PortItem import PortItemPin, PortItem
+from .connections.ConnectionItem import default_cubic_connection_path, ConnectionItem, DataConnectionItem, \
+    ExecConnectionItem
+from .drawings.DrawingObject import DrawingObject
+
+
+class FlowView(GUIBase, 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((NodeOutput, NodeInput), bool)
+    connect_request = Signal(NodePort, NodePort)
+
+    #get_nodes_data_request = Signal(list)
+    #get_connections_data_request = Signal(list)
+    get_flow_data_request = Signal()
+
+    viewport_update_mode_changed = Signal(str)
+
+    def __init__(self, session_gui, flow, parent=None):
+        GUIBase.__init__(self, representing_component=flow)
+        QGraphicsView.__init__(self, parent=parent)
+
+        # UNDO STACK
+        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_gui = session_gui
+
+        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._tmp_data = None
+        self._selected_pin: PortItemPin = None
+        self._dragging_connection = False
+        self._temp_connection_ports = None
+        self._waiting_for_connection_request: bool = False
+        self.mouse_event_taken = False  # for stylus - see tablet event
+        self._last_mouse_move_pos: QPointF = None
+        self._node_place_pos = QPointF()
+        self._left_mouse_pressed_in_flow = False
+        self._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
+        self._zoom_data = {
+            'viewport pos': None,
+            'scene pos': None,
+            'delta': 0,
+        }
+
+        # CONNECTIONS TO FLOW
+        self.create_node_request.connect(self.flow.create_node)
+        self.remove_node_request.connect(self.flow.remove_node)
+        self.check_connection_validity_request.connect(self.flow.check_connection_validity)
+        # TODO: need to check if the 2 lines below are used
+        #self.get_nodes_data_request.connect(self.flow.gen_nodes_data)
+        #self.get_connections_data_request.connect(self.flow.gen_conns_data)
+        self.get_flow_data_request.connect(self.flow.data)
+
+        # CONNECTIONS FROM FLOW
+        self.flow.node_added.sub(self.add_node)
+        self.flow.node_removed.sub(self.remove_node)
+        self.flow.connection_added.sub(self.add_connection)
+        self.flow.connection_removed.sub(self.remove_connection)
+        self.flow.connection_request_valid.sub(self.connection_request_valid)
+
+        # CREATE UI
+        scene = QGraphicsScene(self)
+        scene.setItemIndexMethod(QGraphicsScene.NoIndex)
+        scene.setSceneRect(0, 0, 10000, 7000)
+
+        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))
+
+        self.scene_rect_width = self.mapFromScene(self.sceneRect()).boundingRect().width()
+        self.scene_rect_height = self.mapFromScene(self.sceneRect()).boundingRect().height()
+
+        # NODE LIST WIDGET
+        self._node_list_widget = NodeListWidget(self.session_gui)
+        self._node_list_widget.setMinimumWidth(260)
+        self._node_list_widget.setFixedHeight(300)
+        self._node_list_widget.escaped.connect(self.hide_node_list_widget)
+        self._node_list_widget.node_chosen.connect(self.create_node__cmd)
+
+        self._node_list_widget_proxy = FlowViewProxyWidget(self)
+        self._node_list_widget_proxy.setZValue(1000)
+        self._node_list_widget_proxy.setWidget(self._node_list_widget)
+        self.scene().addItem(self._node_list_widget_proxy)
+
+        self.hide_node_list_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)
+        self.viewport().setAttribute(Qt.WA_AcceptTouchEvents)
+        self.last_pinch_points_dist = 0
+
+        # DESIGN
+        self.session_gui.design.flow_theme_changed.connect(self._theme_changed)
+        self.session_gui.design.performance_mode_changed.connect(self._perf_mode_changed)
+
+        # DATA
+        data = self.flow.load_data
+        if data is not None:
+            view_data = data['flow view']
+            if 'drawings' in view_data:  # not all (old) project files have drawings arr
+                self.place_drawings_from_data(view_data['drawings'])
+            if 'view size' in view_data:
+                self.setSceneRect(0, 0, view_data['view size'][0], view_data['view size'][1])
+
+            self._undo_stack.clear()
+
+        # CATCH UP ON FLOW
+        for node in self.flow.nodes:
+            self.add_node(node)
+        for c in [(o, i) for o, conns in self.flow.graph_adj.items() for i in conns]:
+            self.add_connection(c)
+
+    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):
+        self._node_list_widget.setStyleSheet(self.session_gui.design.node_selection_stylesheet)
+        for n, ni in self.node_items.items():
+            ni.widget.rebuild_ui()
+
+        # TODO: repaint background. how?
+        self.viewport().update()
+        self.scene().update(self.sceneRect())
+
+    def _perf_mode_changed(self, mode):
+
+        # set widget value update rule for port inputs, because this takes up quite a bit of performance
+        update_widget_value = (mode == 'pretty')
+        for n, ni in self.node_items.items():
+            for inp in ni.inputs:
+                inp.update_widget_value = (update_widget_value and inp.widget)
+
+        self.viewport().update()
+        self.scene().update(self.sceneRect())
+
+    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):
+        # InfoMsgs.write('mouse press event received, point:', event.pos())
+
+        # to catch tablet events (for some reason, it results in a mousePrEv too)
+        if self.mouse_event_taken:
+            self.mouse_event_taken = False
+            return
+
+        QGraphicsView.mousePressEvent(self, event)
+
+        # don't process the event if it has been processed by a specific component in the scene
+        # this includes node items, node port pins, proxy widgets etc.
+        if self.mouse_event_taken:
+            self.mouse_event_taken = False
+            return
+
+        if event.button() == Qt.LeftButton:
+            if self._node_list_widget_proxy.isVisible():
+                self.hide_node_list_widget()
+
+        elif event.button() == Qt.RightButton:
+            self._right_mouse_pressed_in_flow = True
+            event.accept()
+
+        self._mouse_press_pos = self.mapToScene(event.pos())
+
+    def mouseMoveEvent(self, event):
+
+        QGraphicsView.mouseMoveEvent(self, event)
+
+        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()
+
+        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.mouse_event_taken:
+            self.mouse_event_taken = False
+            self.viewport().repaint()
+            return
+
+        elif self._panning:
+            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:
+                self.show_place_node_widget(event.pos())
+                return
+
+        if self._dragging_connection:
+
+            # connection dropped over port item
+            port_items = {i: isinstance(i, PortItem) for i in self.items(event.pos())}
+            if any(port_items.values()):
+
+                p_i = list(port_items.keys())[list(port_items.values()).index(True)]  # gets the first PortItem
+
+                # the validity of the connection is checked in connect_node_ports__cmd
+                self.connect_node_ports__cmd(
+                    self._selected_pin.port,
+                    p_i.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):
+
+        if event.modifiers() & Qt.ControlModifier:
+            event.accept()
+
+            self._zoom_data['viewport pos'] = event.posF()
+            self._zoom_data['scene pos'] = pointF_mapped(self.mapToScene(event.pos()), event.posF())
+
+            self._zoom_data['delta'] += event.delta()
+
+            if self._zoom_data['delta'] * event.delta() < 0:
+                self._zoom_data['delta'] = event.delta()
+
+            anim = QTimeLine(100, self)
+            anim.setUpdateInterval(10)
+            anim.valueChanged.connect(self._scaling_time)
+            anim.start()
+
+        else:
+            super().wheelEvent(event)
+
+    def _scaling_time(self, x):
+        delta = self._zoom_data['delta'] / 8
+        if abs(delta) <= 5:
+            delta = self._zoom_data['delta']
+        self._zoom_data['delta'] -= delta
+
+        self.zoom(self._zoom_data['viewport pos'], self._zoom_data['scene pos'], delta)
+
+    def viewportEvent(self, event: QEvent) -> bool:
+        """handling some touch features here"""
+
+        if event.type() == QEvent.TouchBegin:
+            self.setDragMode(QGraphicsView.NoDrag)
+            return True
+        elif event.type() == QEvent.TouchUpdate:
+            event: QTouchEvent
+            if len(event.touchPoints()) == 2:
+
+                tp0, tp1 = event.touchPoints()[0], event.touchPoints()[1]
+
+                p0, p1 = tp0.pos(), tp1.pos()
+                # sp0, sp1 = tp0.scenePos(), tp1.scenePos()
+
+                pinch_points_dist = points_dist(p0, p1)
+
+                if self.last_pinch_points_dist == 0:
+                    self.last_pinch_points_dist = pinch_points_dist
+
+                center = middle_point(p0, p1)
+                self.zoom(
+                    p_abs=center,
+                    p_mapped=self.mapToScene(center.toPoint()),
+                    angle=((pinch_points_dist / self.last_pinch_points_dist)**10 - 1) * 100,
+                )
+
+                self.last_pinch_points_dist = pinch_points_dist
+
+            return True
+
+        elif event.type() == QEvent.TouchEnd:
+            self.last_pinch_points_dist = 0
+            self.setDragMode(QGraphicsView.RubberBandDrag)
+            return True
+
+        else:
+            return super().viewportEvent(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.mouse_event_taken = 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,
+                        data={**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.mouse_event_taken = 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._stylus_modes_widget.pen_width())
+                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:
+                self._current_drawing.finish()
+                InfoMsgs.write('drawing finished')
+                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('application/json'):
+            event.acceptProposedAction()
+
+    def dragMoveEvent(self, event):
+        if event.mimeData().hasFormat('application/json'):
+            event.acceptProposedAction()
+
+    def dropEvent(self, event):
+
+        try:
+            text = str(event.mimeData().data('application/json'), 'utf-8')
+            data: dict = json.loads(text)
+
+            if data['type'] == 'node':
+                self._node_place_pos = self.mapToScene(event.pos())
+                self.create_node__cmd(
+                    node_from_identifier(data['node identifier'], self.session_gui.core_session.nodes)
+                )
+        except Exception:
+            pass
+
+    # PAINTING
+    def drawBackground(self, painter, rect):
+
+        painter.setBrush(self.session_gui.design.flow_theme.flow_background_brush)
+        painter.drawRect(rect.intersected(self.sceneRect()))
+        painter.setPen(Qt.NoPen)
+        painter.drawRect(self.sceneRect())
+
+        if self.session_gui.design.performance_mode == 'pretty':
+            theme = self.session_gui.design.flow_theme
+            if theme.flow_background_grid and self._current_scale >= 0.7:
+                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):
+
+        # DRAW CURRENTLY DRAGGED CONNECTION
+        if self._dragging_connection:
+            pen = QPen(QColor('#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(), 0))
+            # 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_list_widget stays in the viewport
+        if dialog_pos.x() + self._node_list_widget.width() / self._total_scale_div > self.viewport().width():
+            dialog_pos.setX(dialog_pos.x() - (
+                    dialog_pos.x() + self._node_list_widget.width() / self._total_scale_div - self.viewport().width()))
+        if dialog_pos.y() + self._node_list_widget.height() / self._total_scale_div > self.viewport().height():
+            dialog_pos.setY(dialog_pos.y() - (
+                    dialog_pos.y() + self._node_list_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__cmd
+        self._node_list_widget.update_list(
+            nodes if nodes is not None else self.session_gui.core_session.nodes
+        )
+        self._node_list_widget_proxy.setPos(dialog_pos)
+        self._node_list_widget_proxy.show()
+        self._node_list_widget.refocus()
+
+    def hide_node_list_widget(self):
+        self._node_list_widget_proxy.hide()
+        self._node_list_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.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):
+        # print(f'zooming to {p_abs} / {p_mapped} with angle: {angle}')
+
+        by = 0
+        velocity = 2 * (1 / self._current_scale) + 0.5
+        if velocity > 3:
+            velocity = 3
+        if self._current_scale < 1:
+            velocity *= self._current_scale
+
+        zoom_dir_IN = angle>0
+        if zoom_dir_IN:
+            by = 1 + (angle / 4000)*velocity
+        else:
+            by = 1 - (-angle / 4000)*velocity
+
+        if zoom_dir_IN:
+            if self._current_scale * by < 3:
+                self.scale(by, by)
+                self._current_scale *= by
+        else:
+            if self.scene_rect_width * by >= self.viewport().size().width() and self.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 = NodeItem(
+                node=node,
+                node_gui=
+                    (node.GUI if hasattr(node, 'GUI') else NodeGUI)     # use custom GUI class if available
+                    ((node, self.session_gui)),                         # calls __init__ of NodeGUI class with tuple arg
+                flow_view=self,
+                design=self.session_gui.design,
+            )
+            item.initialize()
+
+            self.node_placed.emit(node)
+
+            item_data = node.load_data
+            if item_data is not None and 'pos x' in item_data:
+                pos = QPointF(item_data['pos x'], item_data['pos 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: NodePort, p2: NodePort):
+        # Need to check order of ports since Flow.check_connection_validity needs (NodeOutput, NodeInput)
+        if isinstance(p1, NodeOutput) and isinstance(p2, NodeInput):
+            self._temp_connection_ports = (p1, p2)
+            self._waiting_for_connection_request = True
+            self.check_connection_validity_request.emit((p1, p2), True)
+
+        elif isinstance(p1, NodeInput) and isinstance(p2, NodeOutput):
+            self._temp_connection_ports = (p2, p1)
+            self._waiting_for_connection_request = True
+            self.check_connection_validity_request.emit((p2, p1), True)
+
+        else:
+            self.connection_request_valid(False)
+
+    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 self._waiting_for_connection_request:
+            self._waiting_for_connection_request = False
+        else:
+            return
+
+        if valid:
+            out, inp = self._temp_connection_ports
+            if out.io_pos == PortObjPos.INPUT:
+                out, inp = inp, out
+
+            if self.flow.connected_output(inp) == out:
+                # if the exact connection exists, we want to remove it by command
+                self._push_undo(
+                    ConnectPorts_Command(self, out=self.flow.connected_output(inp), inp=inp)
+                )
+            else:
+                self._push_undo(
+                    ConnectPorts_Command(self, out=out, inp=inp)
+                )
+
+    def add_connection(self, c: Tuple[NodeOutput, NodeInput]):
+        out, inp = c
+
+        #TODO: need to verify that connection_items_cache still works fine with new connection object
+        item: ConnectionItem = None
+        if c in self.connection_items__cache.keys():
+            item = self.connection_items__cache[c]
+
+        else:
+            if inp.type_ == 'data':
+                # item = self.CLASSES['data conn item'](c, self.session.design)
+                item = DataConnectionItem(c, self.session_gui.design)
+            else:
+                # item = self.CLASSES['exec conn item'](c, self.session.design)
+                item = ExecConnectionItem(c, self.session_gui.design)
+
+        self._add_connection_item(item)
+
+        item.out_item.port_connected()
+        item.inp_item.port_connected()
+
+    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: Tuple[NodeOutput, NodeInput]):
+        item = self.connection_items[c]
+        self._remove_connection_item(item)
+
+        item.out_item.port_disconnected()
+        item.inp_item.port_disconnected()
+
+        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: NodePort, 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
+
+    def update_conn_item(self, c: Tuple[NodeOutput, NodeInput]):
+        if c in self.connection_items:
+            self.connection_items[c].changed = True
+            self.connection_items[c].update()
+
+    # DRAWINGS
+    def create_drawing(self, data=None) -> DrawingObject:
+        """Creates and returns a new DrawingObject."""
+
+        new_drawing = DrawingObject(self, data)
+        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: DrawingObject):
+        """Removes a drawing from the scene."""
+
+        # TODO https://github.com/leon-thomm/ryvencore/issues/4
+
+        self.scene().removeItem(drawing)
+        self.drawings.remove(drawing)
+
+    def place_drawings_from_data(self, drawings_data: list, offset_pos=QPoint(0, 0)):
+        """Creates and places drawings from drawings. The same list is returned by the data_() method
+        at 'drawings'."""
+
+        new_drawings = []
+        for d_data in drawings_data:
+            x = d_data['pos x'] + offset_pos.x()
+            y = d_data['pos y'] + offset_pos.y()
+            new_drawing = self.create_drawing(data=d_data)
+            self.add_drawing(new_drawing, QPointF(x, y))
+            new_drawings.append(new_drawing)
+
+        return new_drawings
+
+    def _create_and_place_drawing__cmd(self, posF, data=None):
+        new_drawing_obj = self.create_drawing(data)
+        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_data(self.selected_nodes()),
+            'connections': self._get_connections_data(self.selected_nodes()),
+            'output data': self._get_output_data(self.selected_nodes()),
+            'drawings': self._get_drawings_data(self.selected_drawings())
+        }
+        QGuiApplication.clipboard().setText(json.dumps(data))
+
+    def _cut(self):  # ctrl+x
+        data = {
+            'nodes': self._get_nodes_data(self.selected_nodes()),
+            'connections': self._get_connections_data(self.selected_nodes()),
+            'drawings': self._get_drawings_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)
+        )
+
+    # DATA
+    def complete_data(self, data: dict):
+
+        data['flow view'] = {
+            'drawings': self._get_drawings_data(self.drawings),
+            'view size': [self.sceneRect().size().width(), self.sceneRect().size().height()]
+        }
+
+        return data
+
+    def _get_nodes_data(self, nodes):
+        """generates the data for the specified list of nodes"""
+        f_complete_data = self.session_gui.core_session.complete_data
+        return f_complete_data(self.flow._gen_nodes_data(nodes))
+
+    def _get_connections_data(self, nodes):
+        """generates the connections data for connections between a specified list of nodes"""
+        f_complete_data = self.session_gui.core_session.complete_data
+        return f_complete_data(self.flow._gen_conns_data(nodes))
+
+    def _get_output_data(self, nodes):
+        """generates the serialized data of output ports of the specified nodes"""
+        f_complete_data = self.session_gui.core_session.complete_data
+        return f_complete_data(self.flow._gen_output_data(nodes))
+
+    def _get_drawings_data(self, drawings):
+        """generates the data for a list of drawings"""
+
+        return [d.data() for d in drawings]
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/FlowViewProxyWidget.py b/ryvencore-qt/ryvencore_qt/src/flows/FlowViewProxyWidget.py
new file mode 100644
index 00000000..a422fa7a
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/FlowViewProxyWidget.py
@@ -0,0 +1,28 @@
+from qtpy.QtWidgets import QGraphicsProxyWidget
+
+
+class FlowViewProxyWidget(QGraphicsProxyWidget):
+    """Ensures easy controls event handling for QProxyWidgets in the flow."""
+
+    def __init__(self, flow_view, parent=None):
+        super(FlowViewProxyWidget, self).__init__(parent)
+
+        self.flow_view = flow_view
+
+
+    def mousePressEvent(self, arg__1):
+        QGraphicsProxyWidget.mousePressEvent(self, arg__1)
+        if arg__1.isAccepted():
+            self.flow_view.mouse_event_taken = True
+
+    def mouseReleaseEvent(self, arg__1):
+        self.flow_view.mouse_event_taken = True
+        QGraphicsProxyWidget.mouseReleaseEvent(self, arg__1)
+
+    def wheelEvent(self, event):
+        QGraphicsProxyWidget.wheelEvent(self, event)
+
+    def keyPressEvent(self, arg__1):
+        QGraphicsProxyWidget.keyPressEvent(self, arg__1)
+        if arg__1.isAccepted():
+            self.flow_view.ignore_key_event = True
\ No newline at end of file
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/FlowViewStylusModesWidget.py b/ryvencore-qt/ryvencore_qt/src/flows/FlowViewStylusModesWidget.py
new file mode 100644
index 00000000..94aa83f6
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/FlowViewStylusModesWidget.py
@@ -0,0 +1,127 @@
+from qtpy.QtWidgets import QWidget, QPushButton, QHBoxLayout, QSlider, QColorDialog
+from qtpy.QtCore import Qt
+from qtpy.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 pen_width(self):
+        return self.pen_width_slider.value()/20
+
+    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/ryvencore-qt/ryvencore_qt/src/flows/FlowViewZoomWidget.py b/ryvencore-qt/ryvencore_qt/src/flows/FlowViewZoomWidget.py
new file mode 100644
index 00000000..9237eb9e
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/FlowViewZoomWidget.py
@@ -0,0 +1,34 @@
+from qtpy.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/ryvencore-qt/ryvencore_qt/src/flows/README.md b/ryvencore-qt/ryvencore_qt/src/flows/README.md
new file mode 100644
index 00000000..a60250cc
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/README.md
@@ -0,0 +1,9 @@
+# GUI for flows
+
+The frontend for flows mainly consists of the classes
+
+- `FlowView` - representing `ryvencore.Flow`
+- `NodeItem` - representing `ryvencore.Node`
+- `ConnectionItem` - representing `ryvencore.Connection`
+
+Furthermore, there are drawing objects (which is the stylus drawings in the scene), a nodes list widget with drag&drop support (which can also be used outside of `FlowView`), all the flow themes in `FlowTheme.py` and lots of classes around `NodeItem` implementing specific parts. Notice that, besides `FlowView.py` there is also `Flowcommands.py` where all abstract undoable actions you can perform in the flow are implemented for the `FlowView` in a reactive way.
\ No newline at end of file
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/__init__.py b/ryvencore-qt/ryvencore_qt/src/flows/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/connections/ConnectionItem.py b/ryvencore-qt/ryvencore_qt/src/flows/connections/ConnectionItem.py
new file mode 100644
index 00000000..b81b6d66
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/connections/ConnectionItem.py
@@ -0,0 +1,214 @@
+# import math
+from qtpy.QtCore import QMarginsF
+from qtpy.QtCore import QRectF, QPointF, Qt
+from qtpy.QtGui import QPainter, QColor, QRadialGradient, QPainterPath, QPen
+from qtpy.QtWidgets import QGraphicsPathItem, QGraphicsItem, QStyleOptionGraphicsItem
+
+from ...GUIBase import GUIBase
+from ...utils import sqrt
+from ...utils import pythagoras
+
+
+class ConnectionItem(GUIBase, QGraphicsPathItem):
+    """The GUI representative for a connection. The classes ExecConnectionItem and DataConnectionItem will be ready
+    for reimplementation later, so users can add GUI for the enhancements of DataConnection and ExecConnection,
+    like input fields for weights."""
+
+    def __init__(self, connection, session_design):
+        QGraphicsPathItem.__init__(self)
+
+        self.setAcceptHoverEvents(True)
+
+        self.connection = connection
+        out, inp = self.connection
+
+        out_port_index = out.node.outputs.index(out)
+        inp_port_index = inp.node.inputs.index(inp)
+        self.out_item = out.node.gui.item.outputs[out_port_index]
+        self.inp_item = inp.node.gui.item.inputs[inp_port_index]
+
+        self.session_design = session_design
+        self.session_design.flow_theme_changed.connect(self.recompute)
+        self.session_design.performance_mode_changed.connect(self.recompute)
+
+        # for rendering flow pictures
+        self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
+
+        self.recompute()
+
+    def recompute(self):
+        """Updates scene position and recomputes path, pen and gradient"""
+
+        # position
+        self.setPos(self.out_pos())
+
+        # path
+        self.setPath(
+            self.connection_path(
+                QPointF(0, 0),
+                self.inp_pos()-self.scenePos()
+            )
+        )
+
+        # pen
+        pen = self.get_pen()
+
+        # brush
+        self.setBrush(Qt.NoBrush)
+
+        #   gradient
+        if self.session_design.performance_mode == 'pretty':
+            c = pen.color()
+            w = self.path().boundingRect().width()
+            h = self.path().boundingRect().height()
+            gradient = QRadialGradient(
+                self.boundingRect().center(),
+                pythagoras(w, h) / 2
+            )
+
+            c_r = c.red()
+            c_g = c.green()
+            c_b = c.blue()
+
+            # this offset will be 1 if inp.x >> out.x and 0 if inp.x < out.x
+            # hence, no fade for the gradient if the connection goes backwards
+            offset_mult: float = max(
+                0,
+                min(
+                    (self.inp_pos().x() - self.out_pos().x()) / 200,
+                    1
+                )
+            )
+
+            # and if the input is very far away from the output, decrease the gradient fade so the connection
+            # doesn't fully disappear at the ends and stays visible
+            if self.inp_pos().x() > self.out_pos().x():
+                offset_mult = min(
+                    offset_mult,
+                    2000 / (self.dist(self.inp_pos(), self.out_pos()))
+                )
+                # zucker.
+
+            gradient.setColorAt(0.0, QColor(c_r, c_g, c_b, 255))
+            gradient.setColorAt(0.75, QColor(c_r, c_g, c_b, 255 - round(55 * offset_mult)))
+            gradient.setColorAt(0.95, QColor(c_r, c_g, c_b, 255 - round(255 * offset_mult)))
+
+            pen.setBrush(gradient)
+
+        self.setPen(pen)
+    
+    def out_pos(self) -> QPointF:
+        """The current global scene position of the pin of the output port"""
+
+        return self.out_item.pin.get_scene_center_pos()
+    
+    def inp_pos(self) -> QPointF:
+        """The current global scene position of the pin of the input port"""
+
+        return self.inp_item.pin.get_scene_center_pos()
+
+    def set_highlighted(self, b: bool):
+        pen: QPen = self.pen()
+
+        if b:
+            pen.setWidthF(self.pen_width() * 2)
+        else:
+            pen.setWidthF(self.pen_width())
+            self.recompute()
+
+        self.setPen(pen)
+
+    def get_pen(self) -> QPen:
+        pass
+
+    def pen_width(self) -> int:
+        pass
+
+    def flow_theme(self):
+        return self.session_design.flow_theme
+
+    def hoverEnterEvent(self, event):
+        self.set_highlighted(True)
+        super().hoverEnterEvent(event)
+
+    def hoverLeaveEvent(self, event):
+        self.set_highlighted(False)
+        super().hoverLeaveEvent(event)
+
+    @staticmethod
+    def dist(p1: QPointF, p2: QPointF) -> float:
+        """Returns the diagonal distance between the points using pythagoras"""
+
+        dx = p2.x()-p1.x()
+        dy = p2.y()-p1.y()
+        return sqrt((dx**2) + (dy**2))
+
+
+    @staticmethod
+    def connection_path(p1: QPointF, p2: QPointF) -> QPainterPath:
+        """Returns the painter path for drawing the connection, using the usual cubic connection path by default"""
+
+        return default_cubic_connection_path(p1, p2)
+
+
+class ExecConnectionItem(ConnectionItem):
+
+    def pen_width(self):
+        return self.flow_theme().exec_conn_width
+
+    def get_pen(self):
+        theme = self.flow_theme()
+        pen = QPen(theme.exec_conn_color, theme.exec_conn_width)
+        pen.setStyle(theme.exec_conn_pen_style)
+        pen.setCapStyle(Qt.RoundCap)
+        return pen
+
+
+class DataConnectionItem(ConnectionItem):
+
+    def pen_width(self):
+        return self.flow_theme().data_conn_width
+
+    def get_pen(self):
+        theme = self.flow_theme()
+        pen = QPen(theme.data_conn_color, theme.data_conn_width)
+        pen.setStyle(theme.data_conn_pen_style)
+        pen.setCapStyle(Qt.RoundCap)
+        return pen
+
+
+def default_cubic_connection_path(p1: QPointF, p2: QPointF):
+    """Returns the nice looking QPainterPath from p1 to p2"""
+
+    path = QPainterPath()
+
+    path.moveTo(p1)
+
+    dx = p2.x() - p1.x()
+    adx = abs(dx)
+    dy = p2.y() - p1.y()
+    ady = abs(dy)
+    distance = sqrt((dx ** 2) + (dy ** 2))
+    x1, y1 = p1.x(), p1.y()
+    x2, y2 = p2.x(), p2.y()
+
+    if ((x1 < x2 - 30) or distance < 100) and (x1 < x2):
+        # STANDARD FORWARD
+        path.cubicTo(x1 + ((x2 - x1) / 2), y1,
+                     x1 + ((x2 - x1) / 2), y2,
+                     x2, y2)
+    elif x2 < x1 - 100 and adx > ady * 2:
+        # STRONG BACKWARDS
+        path.cubicTo(x1 + 100 + (x1 - x2) / 10, y1,
+                     x1 + 100 + (x1 - x2) / 10, y1 + (dy / 2),
+                     x1 + (dx / 2), y1 + (dy / 2))
+        path.cubicTo(x2 - 100 - (x1 - x2) / 10, y2 - (dy / 2),
+                     x2 - 100 - (x1 - x2) / 10, y2,
+                     x2, y2)
+    else:
+        # STANDARD BACKWARDS
+        path.cubicTo(x1 + 100 + (x1 - x2) / 3, y1,
+                     x2 - 100 - (x1 - x2) / 3, y2,
+                     x2, y2)
+    
+    return path
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/connections/__init__.py b/ryvencore-qt/ryvencore_qt/src/flows/connections/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/drawings/DrawingObject.py b/ryvencore-qt/ryvencore_qt/src/flows/drawings/DrawingObject.py
new file mode 100644
index 00000000..3d5bc83a
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/drawings/DrawingObject.py
@@ -0,0 +1,234 @@
+from qtpy.QtWidgets import QGraphicsItem
+from qtpy.QtGui import QPen, QPainter, QColor, QPainterPath
+from qtpy.QtCore import Qt, QRectF, QPointF, QLineF
+
+from ...utils import MovementEnum
+
+
+class DrawingObject(QGraphicsItem):
+    """GUI implementation for 'drawing objects' in the scene, written by hand using a stylus pen"""
+
+    def __init__(self, flow_view, load_data=None):
+        super(DrawingObject, self).__init__()
+
+        self.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable |
+                      QGraphicsItem.ItemSendsScenePositionChanges)
+        self.setAcceptHoverEvents(True)
+        self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)  # for rendering flow pictures
+
+        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
+        self.points = []
+        self.stroke_weights = []
+        self.pen_stroke_weight = 0  # approx. avg of self.stroke_weights
+        self.rect = None
+        self.path: QPainterPath = None
+        self.width = -1
+        self.height = -1
+        self.finished = False
+
+        # viewport_pos enables global floating points for precise pen positions
+        self.viewport_pos: QPointF = load_data['viewport pos'] if 'viewport pos' in load_data else None
+        # if the drawing gets loaded, its correct global floating pos is already correct (gets set by flow then)
+
+        self.movement_state = None  # ugly - should get replaced later, see NodeItem, same issue
+        self.movement_pos_from = None
+
+        if 'points' in load_data:
+            p_c = load_data['points']
+            for p in p_c:
+                if type(p) == list:
+                    x = p[0]
+                    y = p[1]
+                    w = p[2]
+                    self.points.append(QPointF(x, y))
+                    self.stroke_weights.append(w)
+                elif type(p) == dict:  # backwards compatibility
+                    x = p['x']
+                    y = p['y']
+                    w = p['w']
+                    self.points.append(QPointF(x, y))
+                    self.stroke_weights.append(w)
+            self.finished = True
+
+        self.color = QColor(load_data['color'])
+        self.base_stroke_weight = load_data['base stroke weight']
+
+
+    def paint(self, painter, option, widget=None):
+
+        if not self.finished:
+            for i in range(1, len(self.points)):
+                pen = QPen()
+                pen.setColor(self.color)
+                pen_width = (self.stroke_weights[i] + 0.2) * self.base_stroke_weight
+                pen.setWidthF(pen_width)
+                if i == 1 or i == len(self.points) - 1:
+                    pen.setCapStyle(Qt.RoundCap)
+                painter.setPen(pen)
+                painter.setRenderHint(QPainter.Antialiasing)
+                painter.setRenderHint(QPainter.HighQualityAntialiasing)
+                painter.drawLine(self.points[i - 1], self.points[i])
+            return
+
+        if not self.path and self.finished:
+            if len(self.points) == 0:
+                return
+
+            self.path = QPainterPath()
+            self.path.moveTo(self.points[0])
+            avg_weight = self.stroke_weights[0]
+            for i in range(1, len(self.points)):
+                self.path.lineTo(self.points[i])
+                avg_weight += self.stroke_weights[i]
+            self.pen_stroke_weight = (avg_weight/len(self.points) + 0.2)*self.base_stroke_weight
+
+        pen = QPen()
+        pen.setColor(self.color)
+        pen.setWidthF(self.pen_stroke_weight)
+        painter.setPen(pen)
+        painter.setRenderHint(QPainter.Antialiasing)
+        # painter.setRenderHint(QPainter.HighQualityAntialiasing)
+        painter.drawPath(self.path)
+
+    def append_point(self, posF_in_view: QPointF) -> bool:
+        """
+        Only used for active drawing.
+        Appends a point (floating, in viewport coordinates) only if the distance to the last one isn't too small
+        """
+
+        p: QPointF = (self.viewport_pos + posF_in_view) - self.pos()
+        p.setX(round(p.x(), 2))
+        p.setY(round(p.y(), 2))
+
+        if len(self.points) > 0:
+            line = QLineF(self.points[-1], p)
+            if line.length() < 0.5:
+                return False
+
+        self.points.append(p)
+        return True
+
+    def finish(self):
+        """
+        Computes the correct center position and updates the relative position for all points.
+        """
+
+        # Correct bounding rect (so far (0,0) is at the start of the line, but it should be in the middle)
+
+        rect_center = self.get_points_rect_center()
+        for p in self.points:
+            p.setX(p.x()-rect_center.x())
+            p.setY(p.y()-rect_center.y())
+        self.setPos(self.pos()+rect_center)
+
+        self.rect = self.get_points_rect()
+
+        self.finished = True
+
+    def get_points_rect(self):
+        """Computes the 'bounding rect' for all points"""
+
+        if len(self.points) == 0:
+            return QRectF(0, 0, 0, 0)
+        x_coords = [p.x() for p in self.points]
+        y_coords = [p.y() for p in self.points]
+        left = min(x_coords)
+        right = max(x_coords)
+        up = min(y_coords)
+        down = max(y_coords)
+
+        rect = QRectF(left, up, right - left, down - up)
+
+        self.width = rect.width()
+        self.height = rect.height()
+        return rect
+
+    def get_points_rect_center(self):
+        """Returns the center point for the 'bounding rect' for all points"""
+
+        return self.get_points_rect().center()
+
+    def boundingRect(self):
+        if self.rect:
+            return self.rect
+        else:
+            return self.get_points_rect()
+
+    def itemChange(self, change, value):
+        if change == QGraphicsItem.ItemPositionChange:
+            self.flow_view.viewport().update()
+            if self.movement_state == MovementEnum.mouse_clicked:
+                self.movement_state = MovementEnum.position_changed
+
+        return QGraphicsItem.itemChange(self, change, value)
+
+    def mousePressEvent(self, event):
+        """Used for Moving-Commands in Flow - may be replaced later with a nicer determination of a move 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 move action."""
+
+        if self.movement_state == MovementEnum.position_changed:
+            self.flow_view.selected_components_moved(self.pos() - self.movement_pos_from)
+        self.movement_state = None
+        return QGraphicsItem.mouseReleaseEvent(self, event)
+
+    def data_(self):
+        drawing_dict = {
+            'pos x': self.pos().x(),
+            'pos y': self.pos().y(),
+            'color': self.color.name(),
+            'type': self.type,
+            'base stroke weight': self.base_stroke_weight
+        }
+        points_list = []
+        for i in range(len(self.points)):
+            p = self.points[i]
+            points_list.append([p.x(), p.y(), self.stroke_weights[i]])
+        drawing_dict['points'] = points_list
+        return drawing_dict
+
+
+# alternative QGraphcsItem-based implementation:
+# 
+#   ...
+#     def paint(self, painter, option, widget=None):
+#
+#         self.setBrush(Qt.NoBrush)
+#
+#         if self.finished:
+#             super().paint(painter, option,widget)
+#         else:
+#             if len(self.stroke_weights) == 0:
+#                 return
+#
+#             # pen
+#             pen = QPen(self.color)
+#             pen.setWidthF(self.stroke_weights[-1])
+#             pen.setCapStyle(Qt.RoundCap)
+#             self.setPen(pen)
+#
+#             # path
+#             path = QPainterPath(self.points[0])
+#             for i, p in enumerate(self.points, start=1):
+#                 path.lineTo(p)
+#             self.setPath(path)
+#             self.path_generated = True
+#
+#             super().paint(painter, option, widget)
+#
+#   ...
+#
+#     def finish(self):
+#
+#         self.finished = True
+#         self.update()
+# 
+# it is a bit worse in appearance but might be a lot faster
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/drawings/__init__.py b/ryvencore-qt/ryvencore_qt/src/flows/drawings/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/node_list_widget/NodeListWidget.py b/ryvencore-qt/ryvencore_qt/src/flows/node_list_widget/NodeListWidget.py
new file mode 100644
index 00000000..e9a0249c
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/node_list_widget/NodeListWidget.py
@@ -0,0 +1,193 @@
+from qtpy.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QScrollArea
+from qtpy.QtCore import Qt, Signal
+
+from ryvencore import Node
+from .utils import search, sort_nodes, inc, dec
+from ..node_list_widget.NodeWidget import NodeWidget
+
+from statistics import median
+
+
+class NodeListWidget(QWidget):
+
+    # SIGNALS
+    escaped = Signal()
+    node_chosen = Signal(object)
+
+    def __init__(self, session):
+        super().__init__()
+
+        self.session = session
+        self.nodes: list[type[Node]] = []
+
+        self.current_nodes = []             # currently selectable nodes
+        self.active_node_widget_index = -1  # index of focused node widget
+        self.active_node_widget = None      # focused node widget
+        self.node_widgets = {}              # Node-NodeWidget assignments
+        self._node_widget_index_counter = 0
+
+        self._setup_UI()
+
+
+    def _setup_UI(self):
+
+        self.main_layout = QVBoxLayout(self)
+        self.main_layout.setAlignment(Qt.AlignTop)
+        self.setLayout(self.main_layout)
+
+        # adding all stuff to the layout
+        self.search_line_edit = QLineEdit(self)
+        self.search_line_edit.setPlaceholderText('search for node...')
+        self.search_line_edit.textChanged.connect(self._update_view)
+        self.layout().addWidget(self.search_line_edit)
+
+
+        self.list_scroll_area = QScrollArea(self)
+        self.list_scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+        self.list_scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+        self.list_scroll_area.setWidgetResizable(True)
+        self.list_scroll_area.setContentsMargins(0, 0, 0, 0)
+
+        self.list_scroll_area_widget = QWidget()
+        self.list_scroll_area_widget.setContentsMargins(0, 0, 0, 0)
+        self.list_scroll_area.setWidget(self.list_scroll_area_widget)
+
+        self.list_layout = QVBoxLayout()
+        self.list_layout.setContentsMargins(0, 0, 0, 0)
+        self.list_layout.setAlignment(Qt.AlignTop)
+        self.list_scroll_area_widget.setLayout(self.list_layout)
+
+        self.layout().addWidget(self.list_scroll_area)
+
+        self._update_view('')
+
+        self.setStyleSheet(self.session.design.node_selection_stylesheet)
+
+        self.search_line_edit.setFocus()
+
+
+    def mousePressEvent(self, event):
+        # need to accept the event, so the scene doesn't process it further
+        QWidget.mousePressEvent(self, event)
+        event.accept()
+
+
+    def keyPressEvent(self, event):
+        """key controls"""
+
+        num_items = len(self.current_nodes)
+
+        if event.key() == Qt.Key_Escape:
+            self.escaped.emit()
+
+        elif event.key() == Qt.Key_Down:
+            self._set_active_node_widget_index(
+                inc(self.active_node_widget_index, length=num_items)
+            )
+        elif event.key() == Qt.Key_Up:
+            self._set_active_node_widget_index(
+                dec(self.active_node_widget_index, num_items)
+            )
+
+        elif event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter:
+            if len(self.current_nodes) > 0:
+                self._place_node(self.active_node_widget_index)
+        else:
+            event.setAccepted(False)
+
+
+    def wheelEvent(self, event):
+        # need to accept the event, so the scene doesn't process it further
+        QWidget.wheelEvent(self, event)
+        event.accept()
+
+
+    def refocus(self):
+        """focuses the search line edit and selects the text"""
+        self.search_line_edit.setFocus()
+        self.search_line_edit.selectAll()
+
+
+    def update_list(self, nodes):
+        """update the list of available nodes"""
+        self.nodes = sort_nodes(nodes)
+        self._update_view('')
+
+
+    def _update_view(self, search_text=''):
+        if len(self.nodes) == 0:
+            return
+
+        search_text = search_text.lower()
+
+        # remove all node widgets
+
+        for i in reversed(range(self.list_layout.count())):
+            self.list_layout.itemAt(i).widget().setParent(None)
+
+        self.current_nodes.clear()
+
+        self._node_widget_index_counter = 0
+
+        # search
+        sorted_distances = search(
+            items={
+                n: [n.title.lower()] + n.tags
+                for n in self.nodes
+            },
+            text=search_text
+        )
+
+        # create node widgets
+        cutoff = median(sorted_distances.values())
+        for n, dist in sorted_distances.items():
+            if search_text != '' and dist > cutoff:
+                continue
+
+            self.current_nodes.append(n)
+
+            if self.node_widgets.get(n) is None:
+                self.node_widgets[n] = self._create_node_widget(n)
+
+            self.list_layout.addWidget(self.node_widgets[n])
+
+        # focus on first result
+        if len(self.current_nodes) > 0:
+            self._set_active_node_widget_index(0)
+
+
+    def _create_node_widget(self, node):
+        node_widget = NodeWidget(self, node)
+        node_widget.custom_focused_from_inside.connect(self._node_widget_focused_from_inside)
+        node_widget.setObjectName('node_widget_' + str(self._node_widget_index_counter))
+        self._node_widget_index_counter += 1
+        node_widget.chosen.connect(self._node_widget_chosen)
+
+        return node_widget
+
+    def _node_widget_focused_from_inside(self):
+        index = self.list_layout.indexOf(self.sender())
+        self._set_active_node_widget_index(index)
+
+    def _set_active_node_widget_index(self, index):
+        self.active_node_widget_index = index
+        node_widget = self.list_layout.itemAt(index).widget()
+
+        if self.active_node_widget:
+            self.active_node_widget.set_custom_focus(False)
+
+        node_widget.set_custom_focus(True)
+        self.active_node_widget = node_widget
+        self.list_scroll_area.ensureWidgetVisible(self.active_node_widget)
+
+
+    def _node_widget_chosen(self):
+        index = int(self.sender().objectName()[self.sender().objectName().rindex('_')+1:])
+        self._place_node(index)
+
+
+    def _place_node(self, index):
+        node_index = index
+        node = self.current_nodes[node_index]
+        self.node_chosen.emit(node)
+        self.escaped.emit()
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/node_list_widget/NodeWidget.py b/ryvencore-qt/ryvencore_qt/src/flows/node_list_widget/NodeWidget.py
new file mode 100644
index 00000000..01556923
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/node_list_widget/NodeWidget.py
@@ -0,0 +1,116 @@
+import json
+
+from qtpy.QtWidgets import QLineEdit, QWidget, QLabel, QGridLayout, QHBoxLayout, QSpacerItem, QSizePolicy, QStyleOption, QStyle
+from qtpy.QtGui import QFont, QPainter, QColor, QDrag
+from qtpy.QtCore import Signal, Qt, QMimeData
+
+
+class NodeWidget(QWidget):
+
+    chosen = Signal()
+    custom_focused_from_inside = Signal()
+
+    def __init__(self, parent, node):
+        super(NodeWidget, self).__init__(parent)
+
+        self.custom_focused = False
+        self.node = node
+
+        self.left_mouse_pressed_on_me = False
+
+        # UI
+        main_layout = QGridLayout()
+        main_layout.setContentsMargins(0, 0, 0, 0)
+
+        self_ = self
+        class NameLabel(QLineEdit):
+            def __init__(self, text):
+                super().__init__(text)
+
+                self.setReadOnly(True)
+                self.setFont(QFont('Source Code Pro', 8))
+            def mouseMoveEvent(self, ev):
+                self_.custom_focused_from_inside.emit()
+                ev.ignore()
+            def mousePressEvent(self, ev):
+                ev.ignore()
+            def mouseReleaseEvent(self, ev):
+                ev.ignore()
+
+        name_label = NameLabel(node.title)
+
+        type_layout = QHBoxLayout()
+
+        #type_label = QLabel(node.type_)
+        #type_label.setFont(QFont('Segoe UI', 8, italic=True))
+        # type_label.setStyleSheet('color: white;')
+
+        main_layout.addWidget(name_label, 0, 0)
+        #main_layout.addWidget(type_label, 0, 1)
+
+        self.setLayout(main_layout)
+        self.setContentsMargins(0, 0, 0, 0)
+        self.setMaximumWidth(250)
+
+        self.setToolTip(node.__doc__)
+        self.update_stylesheet()
+
+
+    def mousePressEvent(self, event):
+        self.custom_focused_from_inside.emit()
+        if event.button() == Qt.LeftButton:
+            self.left_mouse_pressed_on_me = True
+
+    def mouseMoveEvent(self, event):
+        if self.left_mouse_pressed_on_me:
+            drag = QDrag(self)
+            mime_data = QMimeData()
+            mime_data.setData('application/json', bytes(json.dumps(
+                {
+                    'type': 'node',
+                    'node identifier': self.node.identifier,
+                }
+            ), encoding='utf-8'))
+            drag.setMimeData(mime_data)
+            drop_action = drag.exec_()
+
+    def mouseReleaseEvent(self, event):
+        self.left_mouse_pressed_on_me = False
+        if self.geometry().contains(self.mapToParent(event.pos())):
+            self.chosen.emit()
+
+    def set_custom_focus(self, new_focus):
+        self.custom_focused = new_focus
+        self.update_stylesheet()
+
+    def update_stylesheet(self):
+        color = self.node.GUI.color if hasattr(self.node, 'GUI') else '#888888'
+
+        r, g, b = QColor(color).red(), QColor(color).green(), QColor(color).blue()
+
+        new_style_sheet = f'''
+NodeWidget {{
+    border: 1px solid rgba(255,255,255,150);
+    border-radius: 2px;
+    {(
+        f'background-color: rgba(255,255,255,80);'
+    ) if self.custom_focused else ''}
+}}
+QLabel {{
+    background: transparent;
+}}
+QLineEdit {{
+    color: white;
+    background: transparent;
+    border: none;
+    padding: 2px;
+}}
+        '''
+
+        self.setStyleSheet(new_style_sheet)
+
+    def paintEvent(self, event):  # just to enable stylesheets
+        o = QStyleOption()
+        o.initFrom(self)
+        p = QPainter(self)
+        self.style().drawPrimitive(QStyle.PE_Widget, o, p, self)
\ No newline at end of file
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/node_list_widget/__init__.py b/ryvencore-qt/ryvencore_qt/src/flows/node_list_widget/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/node_list_widget/utils.py b/ryvencore-qt/ryvencore_qt/src/flows/node_list_widget/utils.py
new file mode 100644
index 00000000..35f5a6e5
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/node_list_widget/utils.py
@@ -0,0 +1,46 @@
+import textdistance
+
+
+def dec(i: int, length: int) -> int:
+    if i != 0:
+        return i - 1
+    else:
+        return length - 1
+
+def inc(i: int, length: int) -> int:
+    if i != length - 1:
+        return i + 1
+    else:
+        return 0
+
+
+def sort_nodes(nodes):
+    return sorted(nodes, key=lambda x: x.title.lower())
+
+
+def sort_by_val(d: dict) -> dict:
+    return {
+        k: v
+        for k, v in sorted(
+            d.items(),
+
+            # x: (key, value); sort by value
+            key=lambda x: x[1]
+        )
+    }
+
+
+def search(items: dict, text: str) -> dict:
+    """performs the search on `items` under search string `text`"""
+    dist = textdistance.sorensen_dice.distance
+
+    distances = {}
+
+    for item, tags in items.items():
+        min_dist = 1.0
+        for tag in tags:
+            min_dist = min(min_dist, dist(text, tag))
+
+        distances[item] = min_dist
+
+    return sort_by_val(distances)
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeErrorIndicator.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeErrorIndicator.py
new file mode 100644
index 00000000..6eebda50
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeErrorIndicator.py
@@ -0,0 +1,32 @@
+import traceback
+
+from qtpy.QtWidgets import QGraphicsPixmapItem
+from qtpy.QtGui import QPixmap, QImage
+
+from ...GUIBase import GUIBase
+from ...utils import get_resource
+
+
+class NodeErrorIndicator(GUIBase, QGraphicsPixmapItem):
+
+    def __init__(self, node_item):
+        GUIBase.__init__(self)
+        QGraphicsPixmapItem.__init__(self, parent=node_item)
+
+        self.node = node_item
+        self.pix = QPixmap(str(get_resource('pics/warning.png')))
+        self.setPixmap(self.pix)
+        self.setScale(0.1)
+        self.setOffset(-self.boundingRect().width()/2, -self.boundingRect().width()/2)
+
+    def set_error(self, e):
+        error_msg = ''.join([
+            f'<p>{line}</p>'
+            for line in traceback.format_exc().splitlines()
+        ])
+
+        self.setToolTip(
+            f'<html><head/><body>'
+            f'{error_msg}'
+            f'</body></html>'
+        )
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeGUI.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeGUI.py
new file mode 100644
index 00000000..fc9a1a75
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeGUI.py
@@ -0,0 +1,265 @@
+from queue import Queue
+from typing import List, Dict, Tuple, Optional, Union
+
+from qtpy.QtCore import QObject, Signal
+
+
+class NodeGUI(QObject):
+    """
+    Interface class between nodes and their GUI representation.
+    """
+
+    # customizable gui attributes
+    description_html: str = None
+    main_widget_class: list = None
+    main_widget_pos: str = 'below ports'
+    input_widget_classes: dict = {}
+    init_input_widgets: dict = {}
+    style: str = 'normal'
+    color: str = '#c69a15'
+    display_title: str = None
+    icon: str = None
+
+    # qt signals
+    updating = Signal()
+    update_error = Signal(object)
+    input_added = Signal(int, object)
+    output_added = Signal(int, object)
+    input_removed = Signal(int, object)
+    output_removed = Signal(int, object)
+    update_shape_triggered = Signal()
+    hide_unconnected_ports_triggered = Signal()
+    show_unconnected_ports_triggered = Signal()
+
+    def __init__(self, params):
+        QObject.__init__(self)
+
+        node, session_gui = params
+        self.node = node
+        self.item = None   # set by the node item directly after this __init__ call
+        self.session_gui = session_gui
+        setattr(node, 'gui', self)
+
+        self.actions = self._init_default_actions()
+
+        if self.display_title is None:
+            self.display_title = self.node.title
+
+        self.input_widgets = {}     # {input: widget name}
+        for i, widget_data in self.init_input_widgets.items():
+            self.input_widgets[self.node.inputs[i]] = widget_data
+        # using attach_input_widgets() one can buffer input widget
+        # names for inputs that are about to get created
+        self._next_input_widgets = Queue()
+
+        self.error_during_update = False
+
+        # turn ryvencore signals into Qt signals
+        self.node.updating.sub(self._on_updating)
+        self.node.update_error.sub(self._on_update_error)
+        self.node.input_added.sub(self._on_new_input_added)
+        self.node.output_added.sub(self._on_new_output_added)
+        self.node.input_removed.sub(self._on_input_removed)
+        self.node.output_removed.sub(self._on_output_removed)
+
+    def initialized(self):
+        """
+        *VIRTUAL*
+
+        Called after the node GUI has been fully initialized.
+        The Node has been created already (including all ports) and loaded.
+        No connections have been made to ports of the node yet.
+        """
+        pass
+
+    """
+    slots
+    """
+
+    # TODO: displaying update errors is currently prevented by the
+    #   lack of an appropriate updated event in ryvencore.
+    #   Update: there is an updating event now.
+
+    # def on_updated(self, inp):
+    #     if self.error_during_update:
+    #         # an error should prevent an update event, so if we
+    #         # are here, the update was successful
+    #         self.self.error_during_update = False
+    #         self.item.remove_error_message()
+    #     self.updated.emit()
+    #
+    def _on_update_error(self, e):
+    #     self.item.display_error(e)
+    #     self.error_during_update = True
+        self.update_error.emit(e)
+
+    def _on_updating(self, inp: int):
+        # update input widget
+        if inp != -1 and self.item.inputs[inp].widget is not None:
+            o = self.node.flow.connected_output(self.node.inputs[inp])
+            if o is not None:
+                self.item.inputs[inp].widget.val_update_event(o.val)
+
+        self.updating.emit()
+
+    def _on_new_input_added(self, _, index, inp):
+        if not self._next_input_widgets.empty():
+            self.input_widgets[inp] = self._next_input_widgets.get()
+        self.input_added.emit(index, inp)
+
+    def _on_new_output_added(self, _, index, out):
+        self.output_added.emit(index, out)
+
+    def _on_input_removed(self, _, index, inp):
+        self.input_removed.emit(index, inp)
+
+    def _on_output_removed(self, _, index, out):
+        self.output_removed.emit(index, out)
+
+    """
+    actions
+    
+    TODO: move actions to ryvencore?
+    """
+
+    def _init_default_actions(self):
+        """
+        Returns the default actions every node should have
+        """
+        return {
+            'update shape': {'method': self.update_shape},
+            'hide unconnected ports': {'method': self.hide_unconnected_ports},
+            'change title': {'method': self.change_title},
+        }
+
+    def _deserialize_actions(self, actions_data):
+        """
+        Recursively reconstructs the actions dict from the serialized version
+        """
+
+        def _transform(actions_data: dict):
+            """
+            Mutates the actions_data argument by replacing the method names
+            with the actual methods. Doesn't modify the original dict.
+            """
+            new_actions = {}
+            for key, value in actions_data.items():
+                if key == 'method':
+                    try:
+                        value = getattr(self, value)
+                    except AttributeError:
+                        print(f'Warning: action method "{value}" not found in node "{self.node.title}", skipping.')
+                elif isinstance(value, dict):
+                    value = _transform(value)
+                new_actions[key] = value
+            return new_actions
+
+        return _transform(actions_data)
+
+    def _serialize_actions(self, actions):
+        """
+        Recursively transforms the actions dict into a JSON-compatible dict
+        by replacing methods with their name. Doesn't modify the original dict.
+        """
+
+        def _transform(actions: dict):
+            new_actions = {}
+            for key, value in actions.items():
+                if key == 'method':
+                    new_actions[key] = value.__name__
+                elif isinstance(value, dict):
+                    new_actions[key] = _transform(value)
+                else:
+                    new_actions[key] = value
+            return new_actions
+
+        return _transform(actions)
+
+    """
+    serialization
+    """
+
+    def data(self):
+        return {
+            'actions': self._serialize_actions(self.actions),
+            'display title': self.display_title,
+        }
+
+    def load(self, data):
+        if 'actions' in data:   # otherwise keep default
+            self.actions = self._deserialize_actions(data['actions'])
+        if 'display title' in data:
+            self.display_title = data['display title']
+
+        if 'special actions' in data:   # backward compatibility
+            self.actions = self._deserialize_actions(data['special actions'])
+
+    """
+    GUI access methods
+    """
+
+    def set_display_title(self, t: str):
+        self.display_title = t
+        self.update_shape()
+
+    def flow_view(self):
+        return self.item.flow_view
+
+    def main_widget(self):
+        """Returns the main_widget object, or None if the item doesn't exist (yet)"""
+
+        return self.item.main_widget
+
+    def attach_input_widgets(self, widget_names: List[str]):
+        """Attaches the input widget to the next created input."""
+
+        for w in widget_names:
+            self._next_input_widgets.queue(w)
+
+    def input_widget(self, index: int):
+        """Returns a reference to the widget of the corresponding input"""
+
+        return self.item.inputs[index].widget
+
+    def session_stylesheet(self):
+        return self.session_gui.design.global_stylesheet
+
+    def update_shape(self):
+        """Causes recompilation of the whole shape of the GUI item."""
+
+        self.update_shape_triggered.emit()
+
+    def hide_unconnected_ports(self):
+        """Hides all ports that are not connected to anything."""
+
+        del self.actions['hide unconnected ports']
+        self.actions['show unconnected ports'] = {'method': self.show_unconnected_ports}
+        self.hide_unconnected_ports_triggered.emit()
+
+    def show_unconnected_ports(self):
+        """Shows all ports that are not connected to anything."""
+
+        del self.actions['show unconnected ports']
+        self.actions['hide unconnected ports'] = {'method': self.hide_unconnected_ports}
+        self.show_unconnected_ports_triggered.emit()
+
+    def change_title(self):
+        from qtpy.QtWidgets import QDialog, QVBoxLayout, QLineEdit
+
+        class ChangeTitleDialog(QDialog):
+            def __init__(self, title):
+                super().__init__()
+                self.new_title = None
+                self.setLayout(QVBoxLayout())
+                self.line_edit = QLineEdit(title)
+                self.layout().addWidget(self.line_edit)
+                self.line_edit.returnPressed.connect(self.return_pressed)
+
+            def return_pressed(self):
+                self.new_title = self.line_edit.text()
+                self.accept()
+
+        d = ChangeTitleDialog(self.display_title)
+        d.exec_()
+        if d.new_title:
+            self.set_display_title(d.new_title)
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItem.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItem.py
new file mode 100644
index 00000000..7f04e959
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItem.py
@@ -0,0 +1,523 @@
+import traceback
+from typing import Optional, Tuple
+
+from qtpy.QtWidgets import QGraphicsItem, QGraphicsObject, QMenu, QGraphicsDropShadowEffect
+from qtpy.QtCore import Qt, QRectF, QObject, QPointF
+from qtpy.QtGui import QColor
+
+from .NodeErrorIndicator import NodeErrorIndicator
+from .NodeGUI import NodeGUI
+from ...GUIBase import GUIBase
+from ryvencore.NodePort import NodeInput, NodeOutput
+from .NodeItemAction import NodeItemAction
+from .NodeItemAnimator import NodeItemAnimator
+from .NodeItemWidget import NodeItemWidget
+from .PortItem import InputPortItem, OutputPortItem
+from ...utils import serialize, deserialize
+from ...utils import MovementEnum
+
+
+class NodeItem(GUIBase, QGraphicsObject):  # QGraphicsItem, QObject):
+    """The GUI representative for nodes. Unlike the Node class, this class is not subclassed individually and works
+    the same for every node."""
+
+    def __init__(self, node, node_gui, flow_view, design):
+        GUIBase.__init__(self, representing_component=node)
+        QGraphicsObject.__init__(self)
+
+        self.node = node
+        self.node_gui = node_gui
+        self.node_gui.item = self
+        self.flow_view = flow_view
+        self.session_design = design
+        self.movement_state = None
+        self.movement_pos_from = None
+        self.painted_once = False
+        self.inputs = []
+        self.outputs = []
+        self.color = QColor(self.node_gui.color)  # manipulated by self.animator
+
+        self.collapsed = False
+        self.hovered = False
+        self.hiding_unconnected_ports = False
+        self.displaying_error = False
+
+        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_data = self.node.load_data
+
+        # CONNECT TO NODE
+        self.node_gui.updating.connect(self.node_updating)
+        self.node_gui.update_shape_triggered.connect(self.update_shape)
+        self.node_gui.hide_unconnected_ports_triggered.connect(self.hide_unconnected_ports_triggered)
+        self.node_gui.show_unconnected_ports_triggered.connect(self.show_unconnected_ports_triggered)
+        self.node_gui.input_added.connect(self.on_node_input_added)
+        self.node_gui.output_added.connect(self.on_node_output_added)
+        self.node_gui.input_removed.connect(self.on_node_input_removed)
+        self.node_gui.output_removed.connect(self.on_node_output_removed)
+
+        # 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.node_gui.main_widget_class is not None:
+            self.main_widget = self.node_gui.main_widget_class((self.node, self, self.node_gui))
+        self.widget = NodeItemWidget(self.node_gui, self)  # QGraphicsWidget(self)
+        self.animator = NodeItemAnimator(self)  # needs self.title_label
+        self.error_indicator = NodeErrorIndicator(self)
+        self.error_indicator.hide()
+
+        # TOOLTIP
+        self.tooltip_descr_html_content = \
+            self.node_gui.description_html \
+            if self.node_gui.description_html is not None \
+            else \
+            f'<p>{self.node.__doc__}</p>'
+
+        self.setToolTip(f'<html><head/><body>{self.tooltip_descr_html_content}</body></html>')
+
+        # DESIGN THEME
+        self.session_design.flow_theme_changed.connect(self.update_design)
+        self.session_design.performance_mode_changed.connect(self.update_design)
+
+    def initialize(self):
+        """All ports and the main widget get finally created here."""
+
+        # LOADING DATA
+        if self.init_data is not None:
+            if self.main_widget:
+                try:
+                    self.main_widget.set_state(deserialize(self.init_data['main widget data']))
+                except Exception as e:
+                    print('Exception while setting data in', self.node.title, 'Node\'s main widget:', e,
+                          ' (was this intended?)')
+
+        # catch up on init ports
+        for inp in self.node.inputs:
+            self.add_new_input(inp)
+
+        for out in self.node.outputs:
+            self.add_new_output(out)
+
+        if self.init_data is not None:
+            if self.init_data.get('unconnected ports hidden'):
+                self.hide_unconnected_ports_triggered()
+            if self.init_data.get('collapsed'):
+                self.collapse()
+
+        if self.init_data is not None:
+            self.node_gui.load(self.init_data)
+
+        self.node_gui.initialized()
+
+        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)
+        # 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()  # ... not sure if I need that
+
+    # UI STUFF
+
+    def node_updating(self):
+        if self.session_design.animations_enabled:
+            if not self.animator.running():
+                self.animator.start()
+            elif self.animator.fading_out():
+                self.animator.set_animation_max()
+
+        self.update()
+
+    def display_error(self, e):
+        self.error_indicator.set_error(e)
+        self.error_indicator.show()
+        self.displaying_error = True
+
+    def remove_error_message(self):
+        self.error_indicator.hide()
+        self.setToolTip(f'<html><head/><body>{self.tooltip_descr_html_content}</body></html>')
+
+    def set_tooltip(self, error_msg=None):
+
+        if error_msg is not None:
+            err = f'<p style="background: red; color: white">{error_msg}</p>'
+        else:
+            err = ''
+
+        if self.node.description_html:
+            html = self.node.description_html + f'<html><head/><body>{err}</body></html>'
+        elif self.node.__doc__:
+            html = f'<html><head/><body><p>{self.node.__doc__}</p>{err}</body></html>'
+
+        self.setToolTip(html)
+        self.setCursor(Qt.SizeAllCursor)
+
+    def on_node_input_added(self, index, inp: NodeInput):
+        insert = index if index == len(self.node.inputs) - 1 else None
+        self.add_new_input(inp, insert)
+
+    def add_new_input(self, inp: NodeInput, insert: int = None):
+
+        if inp in self.node_gui.input_widgets:
+            widget_name = self.node_gui.input_widgets[inp]['name']
+            widget_class = self.node_gui.input_widget_classes[widget_name]
+            widget_pos = self.node_gui.input_widgets[inp]['pos']
+            widget = (widget_class, widget_pos)
+        else:
+            widget = None
+
+        # create item
+        item = InputPortItem(self.node_gui, self, inp, input_widget=widget)
+
+        if insert is not None:
+            self.inputs.insert(insert, item)
+            self.widget.insert_input_into_layout(insert, item)
+        else:
+            self.inputs.append(item)
+            self.widget.add_input_to_layout(item)
+
+        if not self.initializing:
+            self.update_shape()
+            self.update()
+
+    def on_node_input_removed(self, index, inp: NodeInput):
+        self.remove_input(inp)
+
+    def remove_input(self, inp: NodeInput):
+        item = None
+        for inp_item in self.inputs:
+            if inp_item.port == inp:
+                item = inp_item
+                break
+
+        # index = self.node.inputs.index(inp)
+        # item = self.inputs[index]
+
+        # 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(item.pin)
+        self.scene().removeItem(item.label)
+        if item.proxy is not None:
+            self.scene().removeItem(item.proxy)
+
+        self.inputs.remove(item)
+        self.widget.remove_input_from_layout(item)
+
+        if not self.initializing:
+            self.update_shape()
+            self.update()
+
+    def on_node_output_added(self, index, out: NodeOutput):
+        insert = index if index == len(self.node.outputs) - 1 else None
+        self.add_new_output(out, insert)
+
+    def add_new_output(self, out: NodeOutput, insert: int = None):
+
+        # create item
+        # out.item = OutputPortItem(out.node, self, out)
+        item = OutputPortItem(self.node_gui, self, out)
+
+        if insert is not None:
+            self.outputs.insert(insert, item)
+            self.widget.insert_output_into_layout(insert, item)
+        else:
+            self.outputs.append(item)
+            self.widget.add_output_to_layout(item)
+
+        if not self.initializing:
+            self.update_shape()
+            self.update()
+
+    def on_node_output_removed(self, index, out: NodeOutput):
+        self.remove_output(out)
+
+    def remove_output(self, out: NodeOutput):
+        item = None
+        for out_item in self.outputs:
+            if out_item.port == out:
+                item = out_item
+                break
+
+        # index = self.node.outputs.index(out)
+        # item = self.outputs[index]
+
+        # see remove_input() for info!
+        self.scene().removeItem(item.pin)
+        self.scene().removeItem(item.label)
+
+        self.outputs.remove(item)
+        self.widget.remove_output_from_layout(item)
+
+        if not self.initializing:
+            self.update_shape()
+            self.update()
+
+    def update_shape(self):
+        self.widget.update_shape()
+        self.update_conn_pos()
+        self.flow_view.viewport().update()
+
+    def update_design(self):
+        """Loads the shadow effect option and causes redraw with active theme."""
+
+        if self.session_design.node_item_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(self.session_design.flow_theme.node_item_shadow_color)
+            self.setGraphicsEffect(self.shadow_effect)
+        else:
+            self.setGraphicsEffect(None)
+
+        self.widget.update_shape()
+        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
+
+    def get_left_body_header_vertex_scene_pos(self):
+        return self.mapToScene(
+            QPointF(
+                -self.boundingRect().width() / 2,
+                -self.boundingRect().height() / 2 + self.widget.header_widget.rect().height()
+            )
+        )
+
+    def get_right_body_header_vertex_scene_pos(self):
+        return self.mapToScene(
+            QPointF(
+                +self.boundingRect().width() / 2,
+                -self.boundingRect().height() / 2 + self.widget.header_widget.rect().height()
+            )
+        )
+
+    def hide_unconnected_ports_triggered(self):
+        self.widget.hide_unconnected_ports()
+        self.hiding_unconnected_ports = True
+        self.update_shape()
+
+    def show_unconnected_ports_triggered(self):
+        self.widget.show_unconnected_ports()
+        self.hiding_unconnected_ports = False
+        self.update_shape()
+
+    def expand(self):
+        self.collapsed = False
+        self.widget.expand()
+        self.update_shape()
+
+    def collapse(self):
+        self.collapsed = True
+        self.widget.collapse()
+        self.update_shape()
+
+    #   PAINTING
+
+    def paint(self, painter, option, widget=None):
+        """All painting is done by NodeItemPainter"""
+
+        # in order to access a meaningful geometry of GraphicsWidget contents in update_shape(), the paint event
+        # has to be called once.
+        # https://forum.qt.io/topic/117179/force-qgraphicsitem-to-update-immediately-wait-for-update-event/4
+        if not self.painted_once:
+            # Since I am using a NodeItemWidget, calling self.update_design() here (again)
+            # leads to a QT crash without error, which is strange. In pronciple, calling update_design multiple times
+            # shouldn't be a problem. It's not necessary anymore, so I removed it.
+            # self.update_design()
+
+            self.update_shape()
+            self.update_conn_pos()
+            self.error_indicator.setPos(self.boundingRect().bottomRight())
+
+        self.session_design.flow_theme.paint_NI(
+            node_gui=self.node_gui,
+            selected=self.isSelected(),
+            hovered=self.hovered,
+            node_style=self.node_gui.style,
+            painter=painter,
+            option=option,
+            color=self.color,
+            w=self.boundingRect().width(),
+            h=self.boundingRect().height(),
+            bounding_rect=self.boundingRect(),
+            title_rect=self.widget.header_widget.boundingRect()
+            if self.widget.header_widget
+            else self.widget.title_label.boundingRect()
+        )
+
+        # useful for widget development:
+
+        # painter.setBrush(Qt.NoBrush)
+        # painter.setPen(QPen(QColor('black')))
+        #
+        # painter.drawRect(
+        #         self.boundingRect()
+        # )
+        #
+        # header_rect = QRectF(
+        #         -self.boundingRect().width()/2, -self.boundingRect().height()/2,
+        #         self.widget.header_widget.geometry().width(), self.widget.header_widget.geometry().height()
+        #     )
+        # painter.drawRect(header_rect)
+        #
+        # # note that the widget's layout has spacing 0
+        # body_rect = QRectF(
+        #     -self.boundingRect().width()/2, -self.boundingRect().height()/2 + header_rect.height(),
+        #     self.widget.body_widget.geometry().width(), self.widget.body_widget.geometry().height()
+        # )
+        # painter.drawRect(body_rect)
+
+        self.painted_once = True
+
+    # MOUSE INTERACTION
+
+    def get_context_menu(self):
+        menu = QMenu(self.flow_view)
+
+        actions = self.get_actions(self.node_gui.actions, menu)
+        for a in actions:  # menu needed for 'parent'
+            if type(a) == NodeItemAction:
+                menu.addAction(a)
+            elif type(a) == QMenu:
+                menu.addMenu(a)
+
+        return menu
+
+    def itemChange(self, change, value):
+        """Ensures that all connections, selection borders etc. that get drawn in the FlowView get
+        constantly redrawn during a drag of the item"""
+
+        if change == QGraphicsItem.ItemPositionChange:
+            if self.session_design.performance_mode == 'pretty':
+                self.flow_view.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 scene positions of connections"""
+
+        for o in self.node.outputs:
+            for i in self.node.flow.connected_inputs(o):
+                # c.item.recompute()
+
+                if (o, i) 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[(o,i)]
+                item.recompute()
+        for i in self.node.inputs:
+            o = self.node.flow.connected_output(i)
+            # c.item.recompute()
+
+            if (o, i) 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[(o,i)]
+            item.recompute()
+
+    def hoverEnterEvent(self, event):
+        self.hovered = True
+        self.widget.title_label.set_NI_hover_state(hovering=True)
+        QGraphicsItem.hoverEnterEvent(self, event)
+
+    def hoverLeaveEvent(self, event):
+        self.hovered = False
+        self.widget.title_label.set_NI_hover_state(hovering=False)
+        QGraphicsItem.hoverLeaveEvent(self, event)
+
+    def mousePressEvent(self, event):
+        """Used for Moving-Commands in FlowView - may be replaced later by a nicer determination of a moving action."""
+
+        self.flow_view.mouse_event_taken = True
+
+        if event.button() == Qt.LeftButton:
+            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 FlowView - may be replaced later by a nicer determination of a moving action."""
+
+        self.flow_view.mouse_event_taken = True
+
+        if self.movement_state == MovementEnum.position_changed:
+            self.flow_view.selected_components_moved(self.pos() - self.movement_pos_from)
+        self.movement_state = None
+        return QGraphicsItem.mouseReleaseEvent(self, event)
+
+    # ACTIONS
+
+    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 = NodeItemAction(node_gui=self.node_gui, text=k, method=method, menu=menu, data=data)
+                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
+
+    # DATA
+
+    def complete_data(self, data: dict) -> dict:
+        """completes the node's data by adding all frontend info"""
+
+        data['pos x'] = self.pos().x()
+        data['pos y'] = self.pos().y()
+        if self.main_widget:
+            data['main widget data'] = serialize(self.main_widget.get_state())
+
+        data['unconnected ports hidden'] = self.hiding_unconnected_ports
+        data['collapsed'] = self.collapsed
+
+        data = {**data, **self.node_gui.data()}
+
+        return data
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItemAction.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItemAction.py
new file mode 100644
index 00000000..30f0ee22
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItemAction.py
@@ -0,0 +1,33 @@
+from qtpy.QtCore import Signal
+from qtpy.QtWidgets import QAction
+
+
+class NodeItemAction(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, object)
+    triggered_without_data = Signal(object)
+
+    def __init__(self, node_gui, text, method, menu, data=None):
+        super(NodeItemAction, self).__init__(text=text, parent=menu)
+
+        self.node_gui = node_gui
+        self.data = data
+        self.method = method
+        self.triggered.connect(self.triggered_)
+
+    def triggered_(self):
+        if self.data is not None:
+            self.grab_method()(self.data)
+        else:
+            self.grab_method()()
+
+    def grab_method(self):
+        # the method object could have changed since the action was created
+        return getattr(self.node_gui, self.method.__name__)
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItemAnimator.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItemAnimator.py
new file mode 100644
index 00000000..879e5ff2
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItemAnimator.py
@@ -0,0 +1,75 @@
+from qtpy.QtCore import QObject, QPropertyAnimation, Property
+from qtpy.QtGui import QColor
+from qtpy.QtWidgets import QGraphicsItem
+
+
+class NodeItemAnimator(QObject):
+
+    def __init__(self, node_item):
+        super(NodeItemAnimator, self).__init__()
+
+        self.node_item = node_item
+        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_item.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 fading_out(self):
+        return self.title_activation_animation.currentTime()/self.title_activation_animation.duration() >= 0.3
+
+    def set_animation_max(self):
+        self.title_activation_animation.setCurrentTime(0.3*self.title_activation_animation.duration())
+        self.body_activation_animation.setCurrentTime(0.3*self.body_activation_animation.duration())
+
+    def get_body_color(self):
+        return self.node_item.color
+
+    def set_body_color(self, val):
+        self.node_item.color = val
+        QGraphicsItem.update(self.node_item)
+
+    p_body_color = Property(QColor, get_body_color, set_body_color)
+
+
+    def get_title_color(self):
+        return self.node_item.widget.title_label.color
+
+    def set_title_color(self, val):
+        self.node_item.widget.title_label.color = val
+        # QGraphicsItem.update(self.node_item)
+
+    p_title_color = Property(QColor, get_title_color, set_title_color)
\ No newline at end of file
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItemWidget.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItemWidget.py
new file mode 100644
index 00000000..f54e35c3
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItemWidget.py
@@ -0,0 +1,262 @@
+from qtpy.QtCore import QPointF, QRectF, Qt, QSizeF
+from qtpy.QtWidgets import QGraphicsWidget, QGraphicsLinearLayout, QSizePolicy
+
+from .NodeItem_CollapseButton import NodeItem_CollapseButton
+from ..FlowViewProxyWidget import FlowViewProxyWidget
+# from .Node import Node
+from .NodeItem_Icon import NodeItem_Icon
+from .NodeItem_TitleLabel import TitleLabel
+from .PortItem import InputPortItem, OutputPortItem
+
+
+class NodeItemWidget(QGraphicsWidget):
+    """The QGraphicsWidget managing all GUI components of a NodeItem in widgets and layouts."""
+
+    def __init__(self, node_gui, node_item):
+        super().__init__(parent=node_item)
+
+        self.node_gui = node_gui
+        self.node_item = node_item
+        self.flow_view = self.node_item.flow_view
+        self.flow = self.flow_view.flow
+
+        self.body_padding = 6
+        self.header_padding = (0, 0, 0, 0)  # theme dependent and hence updated in setup_layout()!
+
+        self.icon = NodeItem_Icon(node_gui, node_item) if node_gui.icon else None
+        self.collapse_button = NodeItem_CollapseButton(node_gui, node_item) if node_gui.style == 'normal' else None
+        self.title_label = TitleLabel(node_gui, 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.header_layout: QGraphicsWidget = None
+        self.header_widget: QGraphicsWidget = None
+        self.body_layout: QGraphicsLinearLayout = None
+        self.body_widget: QGraphicsWidget = None
+        self.inputs_layout: QGraphicsLinearLayout = None
+        self.outputs_layout: QGraphicsLinearLayout = None
+        self.setLayout(self.setup_layout())
+
+    def setup_layout(self) -> QGraphicsLinearLayout:
+
+        self.header_padding = self.node_item.session_design.flow_theme.header_padding
+
+        #   main layout
+        layout = QGraphicsLinearLayout(Qt.Vertical)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.setSpacing(0)
+
+        if self.node_gui.style == 'normal':
+            self.header_widget = QGraphicsWidget()
+            # self.header_widget.setContentsMargins(0, 0, 0, 0)
+            self.header_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+            self.header_layout = QGraphicsLinearLayout(Qt.Horizontal)
+            self.header_layout.setSpacing(5)
+            self.header_layout.setContentsMargins(
+                *self.header_padding
+            )
+            if self.icon:
+                self.header_layout.addItem(self.icon)
+                self.header_layout.setAlignment(self.icon, Qt.AlignVCenter | Qt.AlignLeft)
+
+            self.header_layout.addItem(self.title_label)
+
+            self.header_layout.addItem(self.collapse_button)
+            self.header_layout.setAlignment(self.collapse_button, Qt.AlignVCenter | Qt.AlignRight)
+
+            self.header_widget.setLayout(self.header_layout)
+            # layout.addItem(self.header_layout)
+            layout.addItem(self.header_widget)
+            # 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_widget = QGraphicsWidget()
+        # self.body_widget.setContentsMargins(0, 0, 0, 0)
+        self.body_layout = QGraphicsLinearLayout(Qt.Horizontal)
+        self.body_layout.setContentsMargins(
+            self.body_padding,
+            self.body_padding,
+            self.body_padding,
+            self.body_padding
+        )
+
+        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)
+
+        self.body_widget.setLayout(self.body_layout)
+
+        # layout.addItem(self.body_layout)
+        layout.addItem(self.body_widget)
+
+        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, inp in enumerate(self.node_item.inputs):
+            self.inputs_layout.removeAt(0)
+            # inp.rebuild_ui()
+        for i, out in enumerate(self.node_item.outputs):
+            self.outputs_layout.removeAt(0)
+            # out.rebuild_ui()
+
+        self.setLayout(None)
+        self.resize(self.minimumSize())
+        self.setLayout(self.setup_layout())
+
+        if self.node_item.collapsed:
+            return
+
+        for inp_item in self.node_item.inputs:
+            self.add_input_to_layout(inp_item)
+        for out_item in self.node_item.outputs:
+            self.add_output_to_layout(out_item)
+
+        if self.node_item.main_widget:
+            self.add_main_widget_to_layout()
+
+    def update_shape(self):
+
+        self.title_label.update_shape()
+
+        # 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 normal 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())
+
+            # self.main_widget_proxy.setMaximumSize(mw.maximumSize())
+
+            self.main_widget_proxy.setMaximumSize(QSizeF(mw.size()))
+            self.main_widget_proxy.setMinimumSize(QSizeF(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_gui.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_gui.style == 'normal':
+            if self.icon:
+                self.icon.setPos(
+                    QPointF(-self.icon.boundingRect().width() / 2,
+                            -self.icon.boundingRect().height() / 2)
+                )
+                self.title_label.hide()
+            else:
+                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_gui.main_widget_pos == 'between ports':
+            self.body_layout.insertItem(1, self.main_widget_proxy)
+            self.body_layout.insertStretch(2)
+
+        elif self.node_gui.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()
+        self.inputs_layout.addItem(inp)
+        self.inputs_layout.setAlignment(inp, Qt.AlignLeft)
+
+    def insert_input_into_layout(self, index: int, inp: InputPortItem):
+        self.inputs_layout.insertItem(index * 2 + 1, inp)  # *2 bcs of the stretches
+        self.inputs_layout.setAlignment(inp, Qt.AlignLeft)
+        if len(self.node_gui.node.inputs) > 1:
+            self.inputs_layout.insertStretch(index * 2 + 1)  # *2+1 because of the stretches, too
+
+    def remove_input_from_layout(self, inp: InputPortItem):
+        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: OutputPortItem):
+        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: OutputPortItem):
+        self.outputs_layout.insertItem(index * 2 + 1, out)  # *2 because of the stretches
+        self.outputs_layout.setAlignment(out, Qt.AlignRight)
+        if len(self.node_gui.node.outputs) > 1:
+            self.outputs_layout.insertStretch(index * 2 + 1)  # *2+1 because of the stretches, too
+
+    def remove_output_from_layout(self, out: OutputPortItem):
+        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()
+
+    def collapse(self):
+        self.body_widget.hide()
+        if self.main_widget_proxy:
+            self.main_widget_proxy.hide()
+
+    def expand(self):
+        self.body_widget.show()
+        if self.main_widget_proxy:
+            self.main_widget_proxy.show()
+
+    def hide_unconnected_ports(self):
+        for inp in self.node_item.node.inputs:
+            if self.flow.connected_output(inp) is None:
+                inp.hide()
+        for out in self.node_item.node.outputs:
+            if len(self.flow.connected_inputs(out)):
+                out.hide()
+
+    def show_unconnected_ports(self):
+        for inp in self.node_item.inputs:
+            inp.show()
+        for out in self.node_item.outputs:
+            out.show()
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItem_CollapseButton.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItem_CollapseButton.py
new file mode 100644
index 00000000..6548f5e7
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItem_CollapseButton.py
@@ -0,0 +1,73 @@
+from qtpy.QtCore import QSize, QRectF, QPointF, QSizeF, Qt
+from qtpy.QtWidgets import QGraphicsWidget, QGraphicsLayoutItem
+from qtpy.QtGui import QColor
+
+from ...GlobalAttributes import Location
+from ...utils import change_svg_color, get_resource
+
+
+class NodeItem_CollapseButton(QGraphicsWidget):
+    def __init__(self, node_gui, node_item):
+        super().__init__(parent=node_item)
+
+        self.node_gui = node_gui
+        self.node_item = node_item
+
+        self.size = QSizeF(14, 7)
+
+        self.setGraphicsItem(self)
+        self.setCursor(Qt.PointingHandCursor)
+
+
+        self.collapse_pixmap = change_svg_color(
+            get_resource('node_collapse_icon.svg'),
+            self.node_gui.color
+        )
+        self.expand_pixmap = change_svg_color(
+            get_resource('node_expand_icon.svg'),
+            self.node_gui.color
+        )
+
+
+    def boundingRect(self):
+        return QRectF(QPointF(0, 0), self.size)
+
+    def setGeometry(self, rect):
+        self.prepareGeometryChange()
+        QGraphicsLayoutItem.setGeometry(self, rect)
+        self.setPos(rect.topLeft())
+
+    def sizeHint(self, which, constraint=...):
+        return QSizeF(self.size.width(), self.size.height())
+
+    def mousePressEvent(self, event):
+        event.accept()  # make sure the event doesn't get passed on
+        self.node_item.flow_view.mouse_event_taken = True
+
+        if self.node_item.collapsed:
+            self.node_item.expand()
+        else:
+            self.node_item.collapse()
+
+    # def hoverEnterEvent(self, event):
+
+    def paint(self, painter, option, widget=None):
+
+        # doesn't work: ...
+        # painter.setRenderHint(QPainter.Antialiasing, True)
+        # painter.setRenderHint(QPainter.HighQualityAntialiasing, True)
+        # painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
+
+        if not self.node_item.hovered:
+            return
+
+        if self.node_item.collapsed:
+            pixmap = self.expand_pixmap
+        else:
+            pixmap = self.collapse_pixmap
+
+        painter.drawPixmap(
+            0, 0,
+            self.size.width(), self.size.height(),
+            pixmap
+        )
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItem_Icon.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItem_Icon.py
new file mode 100644
index 00000000..dd1d4663
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItem_Icon.py
@@ -0,0 +1,50 @@
+from qtpy.QtCore import QSize, QRectF, QPointF, QSizeF
+from qtpy.QtGui import QPixmap, QImage, QPainter, QIcon, QPicture
+from qtpy.QtWidgets import QGraphicsPixmapItem, QGraphicsWidget, QGraphicsLayoutItem
+
+from ...utils import change_svg_color
+
+
+class NodeItem_Icon(QGraphicsWidget):
+    def __init__(self, node_gui, node_item):
+        super().__init__(parent=node_item)
+
+        if node_gui.style == 'normal':
+            self.size = QSize(20, 20)
+        else:
+            self.size = QSize(50, 50)
+
+        self.setGraphicsItem(self)
+
+        image = QImage(node_gui.icon)
+        self.pixmap = QPixmap.fromImage(image)
+        # self.pixmap = change_svg_color(node.icon, node.color)
+
+
+    def boundingRect(self):
+        return QRectF(QPointF(0, 0), self.size)
+
+    def setGeometry(self, rect):
+        self.prepareGeometryChange()
+        QGraphicsLayoutItem.setGeometry(self, rect)
+        self.setPos(rect.topLeft())
+
+    def sizeHint(self, which, constraint=...):
+        return QSizeF(self.size.width(), self.size.height())
+
+
+    def paint(self, painter, option, widget=None):
+
+        # TODO: anti aliasing for node icons
+
+        # this doesn't work: ...
+        # painter.setRenderHint(QPainter.Antialiasing, True)
+        # painter.setRenderHint(QPainter.HighQualityAntialiasing, True)
+        # painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
+
+
+        painter.drawPixmap(
+            0, 0,
+            self.size.width(), self.size.height(),
+            self.pixmap
+        )
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItem_TitleLabel.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItem_TitleLabel.py
new file mode 100644
index 00000000..97a17ea5
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/nodes/NodeItem_TitleLabel.py
@@ -0,0 +1,72 @@
+from qtpy.QtCore import QRectF, QPointF, QSizeF, Property
+from qtpy.QtGui import QFont, QFontMetricsF, QColor
+from qtpy.QtWidgets import QGraphicsWidget, QGraphicsLayoutItem, QGraphicsItem
+
+from ...utils import get_longest_line
+
+
+class TitleLabel(QGraphicsWidget):
+
+    def __init__(self, node_gui, node_item):
+        super(TitleLabel, self).__init__(parent=node_item)
+
+        self.setGraphicsItem(self)
+
+        self.node_gui = node_gui
+        self.node_item = node_item
+
+        font = QFont('Poppins', 15) if self.node_gui.style == 'normal' else \
+            QFont('K2D', 20, QFont.Bold, True)  # should be quite similar to every specific font chosen by the painter
+        self.fm = QFontMetricsF(font)
+        self.title_str, self.width, self.height = None, None, None
+        self.update_shape()
+
+        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 update_shape(self):
+        self.title_str = self.node_gui.display_title
+
+        # approximately!
+        self.width = self.fm.width(get_longest_line(self.title_str)+'___')
+        self.height = self.fm.height() * 0.7 * (self.title_str.count('\n') + 1)
+
+    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.node_item.session_design.flow_theme.paint_NI_title_label(
+            self.node_gui, self.node_item.isSelected(), self.hovering, painter, option,
+            self.design_style(), self.title_str,
+            self.node_item.color, self.boundingRect()
+        )
+
+    def design_style(self):
+        return self.node_gui.style
+
+    def set_NI_hover_state(self, hovering: bool):
+        self.hovering = hovering
+        # self.update_design()
+        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/ryvencore-qt/ryvencore_qt/src/flows/nodes/PortItem.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/PortItem.py
new file mode 100644
index 00000000..eb63b6c5
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/nodes/PortItem.py
@@ -0,0 +1,350 @@
+from typing import Tuple
+
+from qtpy.QtWidgets import QGraphicsGridLayout, QGraphicsWidget, QGraphicsLayoutItem
+from qtpy.QtCore import Qt, QRectF, QPointF, QSizeF
+from qtpy.QtGui import QFontMetricsF, QFont
+
+from ...GUIBase import GUIBase
+from .PortItemInputWidgets import Data_IW_S, Data_IW_M, Data_IW_L, Float_IW, Integer_IW, \
+    Choice_IW, Boolean_IW, String_IW_S, String_IW_M, String_IW_L
+from ryvencore import serialize, Data
+from ryvencore.NodePort import NodeOutput, NodeInput, NodePort
+from ryvencore.utils import deserialize
+from ...utils import get_longest_line, shorten
+
+from ..FlowViewProxyWidget import FlowViewProxyWidget
+
+
+# utils
+
+
+def is_connected(port):
+    if isinstance(port, NodeOutput):
+        is_connected = len(port.node.flow.connected_inputs(port)) > 0
+    else:
+        is_connected = port.node.flow.connected_output(port) is not None
+    return is_connected
+
+def val(port):
+    if isinstance(port, NodeOutput):
+        return port.val.payload if isinstance(port.val, Data) else None
+    else:
+        conn_out = port.node.flow.connected_output(port)
+        if conn_out:
+            return conn_out.val.payload if conn_out.val is not None else None
+        else:
+            return None
+
+def connections(port):
+    if isinstance(port, NodeOutput):
+        return [(port, i) for i in port.node.flow.connected_inputs(port)]
+    else:
+        conn_out = port.node.flow.connected_output(port)
+        if conn_out:
+            return [(port.node.flow.connected_output(port), port)]
+        else:
+            return []
+
+
+# main classes
+
+
+class PortItem(GUIBase, QGraphicsWidget):
+    """The GUI representative for ports of nodes, also handling mouse events for connections."""
+
+    def __init__(self, node_gui, node_item, port, flow_view):
+        GUIBase.__init__(self, representing_component=port)
+        QGraphicsWidget.__init__(self)
+
+        self.setGraphicsItem(self)
+
+        self.node_gui = node_gui
+        self.node_item = node_item
+        self.port: NodePort = port
+        self.flow_view = flow_view
+
+        # self.port.has_been_connected.connect(self.port_connected)
+        # self.port.has_been_disconnected.connect(self.port_disconnected)
+
+        self.pin = PortItemPin(self.port, self, self.node_gui, self.node_item)
+
+        self.label = PortItemLabel(self.port, self, self.node_gui, self.node_item)
+
+        self._layout = QGraphicsGridLayout()
+        self._layout.setSpacing(0)
+        self._layout.setContentsMargins(0, 0, 0, 0)
+        self.setLayout(self._layout)
+
+    # >>> interaction boilerplate >>>
+    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())
+    # <<< interaction boilerplate <<<
+
+    def setup_ui(self):
+        pass
+
+    def port_connected(self):
+        pass
+
+    def port_disconnected(self):
+        pass
+
+
+class InputPortItem(PortItem):
+    def __init__(self, node_gui, node_item, port, input_widget: Tuple[type, str] = None):
+        super().__init__(node_gui, node_item, port, node_gui.flow_view())
+
+        self.proxy = None   # widget proxy
+        self.widget = None  # widget
+        if input_widget is not None:
+            self.create_widget(input_widget[0], input_widget[1])
+
+        self.update_widget_value = self.widget is not None  # modified by FlowView when performance mode changes
+
+        # catch up to missed connections
+        if self.port.node.flow.connected_output(self.port) is not None:
+            self.port_connected()
+
+        if self.port.type_ == 'data' and self.port.load_data is not None and self.port.load_data['has widget']:
+            c_d = self.port.load_data['widget data']
+            if c_d is not None:
+                self.widget.set_state(deserialize(c_d))
+            else:
+                # this is a little feature that lets us prevent loading of input widgets
+                # which is occasionally useful, e.g. when changing an input widget class:
+                # to prevent loading of the input widget, 'widget data' must be None
+                pass
+
+        self.setup_ui()
+
+    def setup_ui(self):
+        l = self._layout
+
+        # l.setSpacing(0)
+        l.addItem(self.pin, 0, 0)
+        l.setAlignment(self.pin, Qt.AlignVCenter | Qt.AlignLeft)
+        l.addItem(self.label, 0, 1)
+        l.setAlignment(self.label, Qt.AlignVCenter | Qt.AlignLeft)
+        if self.widget:
+            if self.widget.position == 'below':
+                l.addItem(self.proxy, 1, 0, 1, 2)
+            elif self.widget.position == 'besides':
+                l.addItem(self.proxy, 0, 2)
+            else:
+                print('Unknown input widget position:', self.widget.position)
+
+            l.setAlignment(self.proxy, Qt.AlignCenter)
+
+    def create_widget(self, widget_class, widget_pos):
+
+        if widget_class is None:
+            return
+
+        if self.port.type_ != 'data':
+            # TODO: how about input widgets for exec inputs?
+            return
+
+        params = (self.port, self, self.node_gui.node, self.node_gui, widget_pos)
+
+        # custom input widget
+        self.widget = widget_class(params)
+        self.proxy = FlowViewProxyWidget(self.flow_view, parent=self.node_item)
+        self.proxy.setWidget(self.widget)
+
+    def port_connected(self):
+        """Disables the widget"""
+        if self.widget:
+            self.widget.setEnabled(False)
+
+        # https://github.com/leon-thomm/Ryven/pull/137#issuecomment-1433783052
+        # if self.port.type_ == 'data':
+        #     self.port.connections[0].activated.connect(self._port_val_updated)
+        #
+        # self._port_val_updated(self.port.val)
+
+    def port_disconnected(self):
+        """Enables the widget again"""
+        if self.widget:
+            self.widget.setEnabled(True)
+
+    def complete_data(self, data: dict) -> dict:
+        if self.port.type_ == 'data':
+            if self.widget:
+                data['has widget'] = True
+                data['widget name'] = self.node_gui.input_widgets[self.port]['name']
+                data['widget pos'] = self.node_gui.input_widgets[self.port]['pos']
+                data['widget data'] = serialize(self.widget.get_state())
+            else:
+                data['has widget'] = False
+
+        return data
+
+
+class OutputPortItem(PortItem):
+    def __init__(self, node_gui, node_item, port):
+        super().__init__(node_gui, node_item, port, node_gui.flow_view())
+        # super(OutputPortItem, self).__init__(parent_node_instance, PortObjPos.OUTPUT, type_, label_str)
+
+        self.setup_ui()
+
+    def setup_ui(self):
+        l = self._layout
+
+        # l.setSpacing(5)
+        l.addItem(self.label, 0, 0)
+        l.setAlignment(self.label, Qt.AlignVCenter | Qt.AlignRight)
+        l.addItem(self.pin, 0, 1)
+
+        l.setAlignment(self.pin, Qt.AlignVCenter | Qt.AlignRight)
+
+
+# contents
+
+
+class PortItemPin(QGraphicsWidget):
+    def __init__(self, port, port_item, node_gui, node_item):
+        super(PortItemPin, self).__init__(node_item)
+
+        self.port = port
+        self.port_item = port_item
+        self.node_gui = node_gui
+        self.node_item = node_item
+        self.flow_view = self.node_item.flow_view
+
+        self.setGraphicsItem(self)
+        self.setAcceptHoverEvents(True)
+        self.hovered = False
+        self.setCursor(Qt.CrossCursor)
+        self.tool_tip_pos = None
+
+        self.padding = 2
+        # self.painting_width = 17
+        # self.painting_height = 17
+        # self.width = self.painting_width+2*self.padding
+        # self.height = self.painting_height+2*self.padding
+        self.width = 17
+        self.height = 17
+        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.node_item.session_design.flow_theme.paint_PI(
+            node_gui=self.node_gui,
+            painter=painter,
+            option=option,
+            node_color=self.node_gui.color,
+            type_=self.port.type_,
+            connected=is_connected(self.port),
+            rect=QRectF(self.padding, self.padding, self.width-2*self.padding, self.height-2*self.padding)
+        )
+
+    def mousePressEvent(self, event):
+        if event.button() == Qt.LeftButton:  # DRAG NEW CONNECTION
+            self.flow_view.mouse_event_taken = True
+            self.flow_view._selected_pin = self
+            self.flow_view._dragging_connection = True
+            event.accept()  # don't pass the ev ent to anything below
+        else:
+            return QGraphicsWidget.mousePressEvent(self, event)
+
+
+    def moveEvent(self, event):
+        super().moveEvent(event)
+
+        # update connections
+        conn_items = self.flow_view.connection_items
+        for c in self.port.connections:
+            i = conn_items[c]
+
+            # if the items are grouped (which means they move together), don't recompute
+            if i.out.group() is None or i.out.group() != i.inp.group():  # not entirely sure if this is working
+                i.recompute()
+
+
+    def hoverEnterEvent(self, event):
+        if self.port.type_ == 'data':  # and self.parent_port_instance.io_pos == PortPos.OUTPUT:
+            self.setToolTip(shorten(str(val(self.port)), 1000, line_break=True))
+
+        # highlight connections
+        items = self.flow_view.connection_items
+        for c in connections(self.port):
+            items[c].set_highlighted(True)
+
+        self.hovered = True
+
+        QGraphicsWidget.hoverEnterEvent(self, event)
+
+    def hoverLeaveEvent(self, event):
+
+        # un-highlight connections
+        items = self.flow_view.connection_items
+        for c in connections(self.port):
+            items[c].set_highlighted(False)
+
+        self.hovered = False
+
+        QGraphicsWidget.hoverLeaveEvent(self, event)
+
+    def get_scene_center_pos(self):
+        if not self.node_item.collapsed:
+            return QPointF(self.scenePos().x() + self.boundingRect().width()/2,
+                           self.scenePos().y() + self.boundingRect().height()/2)
+        else:
+            if isinstance(self.port_item, InputPortItem):
+                return self.node_item.get_left_body_header_vertex_scene_pos()
+            else:
+                return self.node_item.get_right_body_header_vertex_scene_pos()
+
+
+class PortItemLabel(QGraphicsWidget):
+    def __init__(self, port, port_item, node_gui, node_item):
+        super(PortItemLabel, self).__init__(node_item)
+        self.setGraphicsItem(self)
+
+        self.port = port
+        self.port_item = port_item
+        self.node_gui = node_gui
+        self.node_item = node_item
+
+        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.port.label_str))
+        self.height = font_metrics.height() * (self.port.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.node_item.session_design.flow_theme.paint_PI_label(
+            self.node_gui,
+            painter, option,
+            self.port.type_,
+            is_connected(self.port),
+            self.port.label_str,
+            self.node_gui.color,
+            self.boundingRect()
+        )
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/PortItemInputWidgets.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/PortItemInputWidgets.py
new file mode 100644
index 00000000..64bed37c
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/nodes/PortItemInputWidgets.py
@@ -0,0 +1,323 @@
+from qtpy.QtGui import QFontMetrics, QFont
+from qtpy.QtWidgets import QSpinBox, QLineEdit, QCheckBox, QComboBox
+
+from .WidgetBaseClasses import NodeInputWidget
+
+
+class DType_IW_Base(NodeInputWidget):
+
+    def __init__(self, params):
+        super().__init__(params)
+
+        self.dtype = self.input.dtype
+        self.block = False
+
+    def change_val(self, val):
+        if not self.block:
+            self.dtype.val = val
+            self.update_node_input(val)
+
+    def __str__(self):
+        return self.__class__.__name__
+
+
+class Data_IW(DType_IW_Base, QLineEdit):  # virtual
+
+    base_width = None  # specified by subclasses
+
+    def __init__(self, params):
+        DType_IW_Base.__init__(self, params)
+        QLineEdit.__init__(self)
+
+        # dtype = self.input.dtype
+        # self.last_val = None
+
+        self.setFont(QFont('source code pro', 10))
+        self.val_update_event(self.dtype.val)
+        if self.dtype.size == 's':
+            self.base_width = 30
+        elif self.dtype.size == 'm':
+            self.base_width = 70
+        elif self.dtype.size == 'l':
+            self.base_width = 150
+        self.max_width = self.base_width * 3
+        self.setFixedWidth(self.base_width)
+        self.fm = QFontMetrics(self.font())
+
+        self.setToolTip(self.dtype.doc)
+        self.textChanged.connect(self.text_changed)
+        self.editingFinished.connect(self.editing_finished)
+
+    def text_changed(self, new_text):
+        """manages resizing of widget to content"""
+
+        text_width = self.fm.width(new_text)
+        new_width = text_width+15  # add some buffer
+        if new_width < self.max_width:
+            self.setFixedWidth(new_width if new_width > self.base_width else self.base_width)
+        else:
+            self.setFixedWidth(self.max_width)
+        self.node.update_shape()
+
+    def editing_finished(self):
+        """updates the input"""
+        # if v != self.last_val:
+        # self.last_val = v
+        self.change_val(self.get_val())
+
+    def get_val(self):
+        try:
+            return eval(self.text())
+        except Exception as e:
+            return self.text()
+
+    def val_update_event(self, val):
+        """triggered when input is connected and received new data;
+        displays the data in the widget (without updating)"""
+
+        self.block = True
+        try:
+            self.setText(str(val))
+        except Exception as e:
+            pass
+        finally:
+            self.block = False
+
+    def get_state(self) -> dict:
+        return {'text': self.text()}
+
+    def set_state(self, data: dict):
+        # just show value, DO NOT UPDATE
+        self.val_update_event(data['text'])
+
+
+# custom sized classes for qss access:
+
+class Data_IW_S(Data_IW):
+    base_width = 30
+
+
+class Data_IW_M(Data_IW):
+    base_width = 70
+
+
+class Data_IW_L(Data_IW):
+    base_width = 150
+
+
+# -----------------------------------
+
+
+class String_IW(DType_IW_Base, QLineEdit):  # virtual
+
+    width = None  # specified by subclasses
+
+    def __init__(self, params):
+        DType_IW_Base.__init__(self, params)
+        QLineEdit.__init__(self)
+
+        # dtype = self.input.dtype
+        # self.last_val = None
+
+        self.setFont(QFont('source code pro', 10))
+        self.setText(self.dtype.val)
+        self.setFixedWidth(self.width)
+        self.setToolTip(self.dtype.doc)
+
+        self.editingFinished.connect(self.editing_finished)
+
+    def editing_finished(self):
+        """updates the input"""
+        self.change_val(self.get_val())
+        # v = self.get_val()
+        # if v != self.last_val:
+        #     self.update_node_input(v)
+        #     self.last_val = v
+
+    def get_val(self):
+        return self.text()
+
+    def val_update_event(self, val):
+        """triggered when input is connected and received new data;
+        displays the data in the widget (without updating)"""
+        self.block = True
+        self.setText(str(val))
+        self.block = False
+
+    def get_state(self) -> dict:
+        return {'text': self.text()}
+
+    def set_state(self, data: dict):
+        # just show value, DO NOT UPDATE
+        self.val_update_event(data['text'])
+
+
+# custom sized classes for qss access:
+
+class String_IW_S(String_IW):
+    width = 30
+
+
+class String_IW_M(String_IW):
+    width = 70
+
+
+class String_IW_L(String_IW):
+    width = 150
+
+
+# -----------------------------------
+
+
+class Integer_IW(DType_IW_Base, QSpinBox):
+    def __init__(self, params):
+        DType_IW_Base.__init__(self, params)
+        QSpinBox.__init__(self)
+
+        if self.dtype.bounds:
+            self.setRange(self.dtype.bounds[0], self.dtype.bounds[1])
+        self.setValue(self.dtype.val)
+        self.setToolTip(self.dtype.doc)
+
+        self.valueChanged.connect(self.widget_val_changed)
+
+    def widget_val_changed(self, val):
+        self.change_val(val)
+
+    def get_val(self):
+        return self.value()
+
+    def val_update_event(self, val):
+        """triggered when input is connected and received new data;
+        displays the data in the widget (without updating)"""
+        self.block = True
+        try:
+            self.setValue(val)
+        except Exception as e:
+            pass
+        finally:
+            self.block = False
+
+    def get_state(self) -> dict:
+        return {'val': self.value()}
+
+    def set_state(self, data: dict):
+        # just show value, DO NOT UPDATE
+        self.val_update_event(data['val'])
+
+
+class Float_IW(DType_IW_Base, QLineEdit):
+    def __init__(self, params):
+        DType_IW_Base.__init__(self, params)
+        QLineEdit.__init__(self)
+
+        self.setFont(QFont('source code pro', 10))
+        fm = QFontMetrics(self.font())
+        self.setMaximumWidth(fm.width(' ')*self.dtype.decimals+1)
+        self.setText(str(self.dtype.val))
+        self.setToolTip(self.dtype.doc)
+
+        self.textChanged.connect(self.widget_text_changed)
+
+    def widget_text_changed(self):
+        self.change_val(self.get_val())
+
+    def get_val(self):
+        return float(self.text())
+
+    def val_update_event(self, val):
+        """triggered when input is connected and received new data;
+        displays the data in the widget (without updating)"""
+        self.block = True
+        try:
+            self.setText(str(val))
+        except Exception as e:
+            pass
+        finally:
+            self.block = False
+
+    def get_state(self) -> dict:
+        return {'text': self.text()}
+
+    def set_state(self, data: dict):
+        # just show value, DO NOT UPDATE
+        self.val_update_event(data['text'])
+
+
+class Boolean_IW(DType_IW_Base, QCheckBox):
+    def __init__(self, params):
+        DType_IW_Base.__init__(self, params)
+        QCheckBox.__init__(self)
+
+        self.setChecked(self.dtype.val)
+
+        self.setToolTip(self.dtype.doc)
+
+        self.stateChanged.connect(self.state_changed)
+
+    def state_changed(self, state):
+        self.change_val(self.get_val())
+
+    def get_val(self):
+        return self.isChecked()
+
+    def val_update_event(self, val):
+        """triggered when input is connected and received new data;
+        displays the data in the widget (without updating)"""
+        self.block = True
+        try:
+            self.setChecked(bool(val))
+        except Exception as e:
+            pass
+        finally:
+            self.block = False
+
+    def get_state(self) -> dict:
+        return {'checked': self.isChecked()}
+
+    def set_state(self, data: dict):
+        # just show value, DO NOT UPDATE
+        self.val_update_event(data['checked'])
+
+
+class Choice_IW(DType_IW_Base, QComboBox):
+    def __init__(self, params):
+        DType_IW_Base.__init__(self, params)
+        QComboBox.__init__(self)
+
+        self.addItems(self.dtype.items)
+        self.setCurrentText(self.dtype.val)
+        self.setToolTip(self.dtype.doc)
+
+        self.currentTextChanged.connect(self.widget_text_changed)
+
+    def widget_text_changed(self):
+        self.change_val(self.get_val())
+
+    def get_val(self):
+        return self.currentText()
+
+    def val_update_event(self, val):
+        """triggered when input is connected and received new data;
+        displays the data in the widget (without updating)"""
+        self.block = True
+        try:
+            self.setCurrentText(val)
+        except Exception as e:
+            pass
+        finally:
+            self.block = False
+
+    def get_state(self) -> dict:
+        return {
+            'items': [self.itemText(i) for i in range(self.count())],
+            'active': self.currentText(),
+        }
+
+    def set_state(self, data: dict):
+        # just show value, DO NOT UPDATE
+        self.block = True
+        self.clear()
+        self.addItems(data['items'])
+        self.setCurrentText(data['active'])
+        self.block = False
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/WidgetBaseClasses.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/WidgetBaseClasses.py
new file mode 100644
index 00000000..0e66b69c
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/flows/nodes/WidgetBaseClasses.py
@@ -0,0 +1,85 @@
+"""The base classes for node custom widgets for nodes."""
+from ryvencore import Data
+
+
+class NodeMainWidget:
+    """Base class for the main widget of a node."""
+
+    def __init__(self, params):
+        self.node, self.node_item, self.node_gui = params
+
+    def get_state(self) -> dict:
+        """
+        *VIRTUAL*
+
+        Return the state of the widget, in a (pickle) serializable format.
+        """
+        data = {}
+        return data
+
+    def set_state(self, data: dict):
+        """
+        *VIRTUAL*
+
+        Set the state of the widget, where data corresponds to the dict
+        returned by get_state().
+        """
+        pass
+
+    def update_node(self):
+        self.node.update()
+
+    def update_node_shape(self):
+        self.node_item.update_shape()
+
+
+class NodeInputWidget:
+    """Base class for the input widget of a node."""
+
+    def __init__(self, params):
+        self.input, self.input_item, self.node, self.node_gui, self.position = \
+            params
+
+    def get_state(self) -> dict:
+        """
+        *VIRTUAL*
+
+        Return the state of the widget, in a (pickle) serializable format.
+        """
+        data = {}
+        return data
+
+    def set_state(self, data: dict):
+        """
+        *VIRTUAL*
+
+        Set the state of the widget, where data corresponds to the dict
+        returned by get_state().
+        """
+        pass
+
+    def val_update_event(self, val: Data):
+        """
+        *VIRTUAL*
+
+        Called when the input's value is updated through a connection.
+        This can be used to represent the value in the widget.
+        The widget is disabled when the port is connected.
+        """
+        pass
+
+    # API methods
+
+    def update_node_input(self, val: Data, silent=False):
+        """
+        Update the input's value and update the node.
+        """
+        self.input.default = val
+        if not silent:
+            self.input.node.update(self.node.inputs.index(self.input))
+
+    def update_node(self):
+        self.node.update(self.node.inputs.index(self.input))
+
+    def update_node_shape(self):
+        self.node_gui.update_shape()
diff --git a/ryvencore-qt/ryvencore_qt/src/flows/nodes/__init__.py b/ryvencore-qt/ryvencore_qt/src/flows/nodes/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ryvencore-qt/ryvencore_qt/src/utils.py b/ryvencore-qt/ryvencore_qt/src/utils.py
new file mode 100644
index 00000000..e0ba074b
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/utils.py
@@ -0,0 +1,87 @@
+import bisect
+import enum
+import json
+import pathlib
+from math import sqrt
+from typing import List, Dict
+
+from qtpy.QtCore import QPointF, QByteArray
+
+from ryvencore.utils import serialize, deserialize
+from .GlobalAttributes import *
+
+
+def pythagoras(a, b):
+    return sqrt(a ** 2 + b ** 2)
+
+
+def get_longest_line(s: str):
+    lines = s.split('\n')
+    lines = [line.replace('\n', '') for line in lines]
+    longest_line_found = ''
+    for line in lines:
+        if len(line) > len(longest_line_found):
+            longest_line_found = line
+    return line
+
+
+def shorten(s: str, max_chars: int, line_break: bool = False):
+    """Ensures, that a given string does not exceed a given max length. If it would, its cut in the middle."""
+    l = len(s)
+    if l > max_chars:
+        insert = ' . . . '
+        if line_break:
+            insert = '\n'+insert+'\n'
+        insert_length = len(insert)
+        left = s[:round((max_chars-insert_length)/2)]
+        right = s[round(l-((max_chars-insert_length)/2)):]
+        return left+insert+right
+    else:
+        return s
+
+
+def pointF_mapped(p1, p2):
+    """adds the floating part of p2 to p1"""
+    p2.setX(p1.x() + p2.x()%1)
+    p2.setY(p1.y() + p2.y()%1)
+    return p2
+
+def points_dist(p1, p2):
+    return sqrt(abs(p1.x() - p2.x())**2 + abs(p1.y() - p2.y())**2)
+
+def middle_point(p1, p2):
+    return QPointF((p1.x() + p2.x())/2, (p1.y() + p2.y())/2)
+
+
+class MovementEnum(enum.Enum):
+    # this should maybe get removed later
+    mouse_clicked = 1
+    position_changed = 2
+    mouse_released = 3
+
+
+def get_resource(filepath: str):
+    return pathlib.Path(Location.PACKAGE_PATH, 'resources', filepath)
+
+
+def change_svg_color(filepath: str, color_hex: str):
+    """Loads an SVG, changes all '#xxxxxx' occurrences to color_hex, renders it into and a pixmap and returns it"""
+
+    # https://stackoverflow.com/questions/15123544/change-the-color-of-an-svg-in-qt
+
+    from qtpy.QtSvg import QSvgRenderer
+    from qtpy.QtGui import QPixmap, QPainter
+    from qtpy.QtCore import Qt
+
+    with open(filepath) as f:
+        data = f.read()
+    data = data.replace('fill:#xxxxxx', 'fill:'+color_hex)
+
+    svg_renderer = QSvgRenderer(QByteArray(bytes(data, 'ascii')))
+
+    pix = QPixmap(svg_renderer.defaultSize())
+    pix.fill(Qt.transparent)
+    pix_painter = QPainter(pix)
+    svg_renderer.render(pix_painter)
+
+    return pix
diff --git a/ryvencore-qt/ryvencore_qt/src/widgets/EditVal_Dialog.py b/ryvencore-qt/ryvencore_qt/src/widgets/EditVal_Dialog.py
new file mode 100644
index 00000000..767bfde2
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/widgets/EditVal_Dialog.py
@@ -0,0 +1,53 @@
+from qtpy.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QPlainTextEdit, QShortcut, QMessageBox
+from qtpy.QtGui import QKeySequence
+
+
+class EditVal_Dialog(QDialog):
+
+    def __init__(self, parent, init_val):
+        super(EditVal_Dialog, self).__init__(parent)
+
+        # shortcut
+        save_shortcut = QShortcut(QKeySequence.Save, self)
+        save_shortcut.activated.connect(self.save_triggered)
+
+        main_layout = QVBoxLayout()
+
+        self.val_text_edit = QPlainTextEdit()
+        val_str = ''
+        try:
+            val_str = str(init_val)
+        except Exception as e:
+            msg_box = QMessageBox(QMessageBox.Warning, 'Value parsing failed',
+                                  'Couldn\'t stringify value', QMessageBox.Ok, self)
+            msg_box.setDefaultButton(QMessageBox.Ok)
+            msg_box.exec_()
+            self.reject()
+
+        self.val_text_edit.setPlainText(val_str)
+
+        main_layout.addWidget(self.val_text_edit)
+
+        button_box = QDialogButtonBox()
+        button_box.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok)
+        button_box.accepted.connect(self.accept)
+        button_box.rejected.connect(self.reject)
+
+        main_layout.addWidget(button_box)
+
+        self.setLayout(main_layout)
+        self.resize(450, 300)
+
+        self.setWindowTitle('edit val')
+
+    def save_triggered(self):
+        self.accept()
+
+
+    def get_val(self):
+        val = self.val_text_edit.toPlainText()
+        try:
+            val = eval(val)
+        except Exception as e:
+            pass
+        return val
\ No newline at end of file
diff --git a/ryvencore-qt/ryvencore_qt/src/widgets/FlowsListWidget.py b/ryvencore-qt/ryvencore_qt/src/widgets/FlowsListWidget.py
new file mode 100644
index 00000000..3a696b74
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/widgets/FlowsListWidget.py
@@ -0,0 +1,95 @@
+from qtpy.QtCore import Qt
+from qtpy.QtWidgets import QWidget, QMessageBox, QVBoxLayout, QLineEdit, QHBoxLayout, QPushButton, QScrollArea
+
+from .FlowsList_FlowWidget import FlowsList_FlowWidget
+
+
+class FlowsListWidget(QWidget):
+    """Convenience class for a QWidget to easily manage the flows of a session."""
+
+    def __init__(self, session_gui):
+        super().__init__()
+
+        self.session_gui = session_gui
+        self.list_widgets = []
+        self.ignore_name_line_edit_signal = False  # because disabling causes firing twice otherwise
+
+        self.setup_UI()
+
+        self.session_gui.flow_view_created.connect(self.add_new_flow)
+        self.session_gui.flow_deleted.connect(self.recreate_list)
+
+
+    def setup_UI(self):
+        main_layout = QVBoxLayout(self)
+        main_layout.setAlignment(Qt.AlignTop)
+        self.setLayout(main_layout)
+
+        # list scroll area
+
+        self.list_scroll_area = QScrollArea(self)
+        self.list_scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+        self.list_scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+        self.list_scroll_area.setWidgetResizable(True)
+        self.list_scroll_area.setContentsMargins(0, 0, 0, 0)
+
+        self.scroll_area_widget = QWidget()
+        self.scroll_area_widget.setContentsMargins(0, 0, 0, 0)
+        self.list_scroll_area.setWidget(self.scroll_area_widget)
+
+        self.list_layout = QVBoxLayout()
+        self.list_layout.setContentsMargins(0, 0, 0, 0)
+        self.list_layout.setAlignment(Qt.AlignTop)
+        self.scroll_area_widget.setLayout(self.list_layout)
+
+        self.layout().addWidget(self.list_scroll_area)
+
+        # line edit
+
+        self.new_flow_title_lineedit = QLineEdit()
+        self.new_flow_title_lineedit.setPlaceholderText('new flow\'s title')
+        self.new_flow_title_lineedit.returnPressed.connect(self.create_flow)
+
+        main_layout.addWidget(self.new_flow_title_lineedit)
+
+
+        self.recreate_list()
+
+
+    def recreate_list(self):
+        # remove flow widgets
+        for i in reversed(range(self.list_layout.count())):
+            self.list_layout.itemAt(i).widget().setParent(None)
+        self.list_widgets.clear()
+
+        # re-create flow widgets
+        for s in self.session_gui.core_session.flows:
+            new_widget = FlowsList_FlowWidget(self, self.session_gui, s)
+            self.list_widgets.append(new_widget)
+
+        # add flow widgets to layout
+        for w in self.list_widgets:
+            self.list_layout.addWidget(w)
+
+    def create_flow(self):
+        title = self.new_flow_title_lineedit.text()
+
+        if self.session_gui.core_session.flow_title_valid(title):
+            self.session_gui.core_session.create_flow(title=title)
+
+    def add_new_flow(self, flow, flow_view):
+        self.recreate_list()
+
+    def del_flow(self, flow, flow_widget):
+        msg_box = QMessageBox(QMessageBox.Warning, 'sure about deleting flow?',
+                              'You are about to delete a flow. This cannot be undone, all content will be lost. '
+                              'Do you want to continue?', QMessageBox.Cancel | QMessageBox.Yes, self)
+        msg_box.setDefaultButton(QMessageBox.Cancel)
+        ret = msg_box.exec_()
+        if ret != QMessageBox.Yes:
+            return
+
+        self.list_widgets.remove(flow_widget)
+        flow_widget.setParent(None)
+        self.session_gui.core_session.delete_flow(flow)
+        # self.recreate_list()
diff --git a/ryvencore-qt/ryvencore_qt/src/widgets/FlowsList_FlowWidget.py b/ryvencore-qt/ryvencore_qt/src/widgets/FlowsList_FlowWidget.py
new file mode 100644
index 00000000..efc52167
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/widgets/FlowsList_FlowWidget.py
@@ -0,0 +1,118 @@
+from qtpy.QtWidgets import QWidget, QHBoxLayout, QLabel, QMenu, QAction
+from qtpy.QtGui import QIcon, QImage
+from qtpy.QtCore import Qt, QEvent, QBuffer, QByteArray
+
+from ..GlobalAttributes import Location
+from .ListWidget_NameLineEdit import ListWidget_NameLineEdit
+
+
+class FlowsList_FlowWidget(QWidget):
+    """A QWidget representing a single Flow for the FlowsListWidget."""
+
+    def __init__(self, flows_list_widget, session_gui, flow):
+        super().__init__()
+
+        self.session_gui = session_gui
+        self.flow = flow
+        self.flow_view = self.session_gui.flow_views[flow]
+        self.flows_list_widget = flows_list_widget
+        self.previous_flow_title = ''
+        self._thumbnail_source = ''
+        self.ignore_title_line_edit_signal = False
+
+
+        # UI
+
+        main_layout = QHBoxLayout()
+        main_layout.setContentsMargins(0, 0, 0, 0)
+
+        #   create icon
+
+        # TODO: change this icon
+        flow_icon = QIcon(Location.PACKAGE_PATH + '/resources/pics/script_picture.png')
+
+        icon_label = QLabel()
+        icon_label.setFixedSize(20, 20)
+        icon_label.setStyleSheet('border:none;')
+        icon_label.setPixmap(flow_icon.pixmap(20, 20))
+        main_layout.addWidget(icon_label)
+
+        #   title line edit
+
+        self.title_line_edit = ListWidget_NameLineEdit(flow.title, self)
+        self.title_line_edit.setPlaceholderText('title')
+        self.title_line_edit.setEnabled(False)
+        self.title_line_edit.editingFinished.connect(self.title_line_edit_editing_finished)
+
+        main_layout.addWidget(self.title_line_edit)
+
+        self.setLayout(main_layout)
+
+
+
+    def mouseDoubleClickEvent(self, event):
+        if event.button() == Qt.LeftButton:
+            if self.title_line_edit.geometry().contains(event.pos()):
+                self.title_line_edit_double_clicked()
+                return
+
+
+    def event(self, event):
+        if event.type() == QEvent.ToolTip:
+
+            # generate preview img as QImage
+            img: QImage = self.flow_view.get_viewport_img().scaledToHeight(200)
+
+            # store the img data in QBuffer to load it directly from memory
+            buffer = QBuffer()
+            img.save(buffer, 'PNG')
+
+            # generate html from data in memory
+            html = f"<img src='data:image/png;base64, { bytes( buffer.data().toBase64() ).decode() }'>"
+
+            # show tooltip
+            self.setToolTip(html)
+
+        return QWidget.event(self, event)
+
+
+    def contextMenuEvent(self, event):
+        menu: QMenu = QMenu(self)
+
+        delete_action = QAction('delete')
+        delete_action.triggered.connect(self.action_delete_triggered)
+
+        actions = [delete_action]
+        for a in actions:
+            menu.addAction(a)
+
+        menu.exec_(event.globalPos())
+
+
+    def action_delete_triggered(self):
+        self.flows_list_widget.del_flow(self.flow, self)
+
+
+    def title_line_edit_double_clicked(self):
+        self.title_line_edit.setEnabled(True)
+        self.title_line_edit.setFocus()
+        self.title_line_edit.selectAll()
+
+        self.previous_flow_title = self.title_line_edit.text()
+
+
+    def title_line_edit_editing_finished(self):
+        if self.ignore_title_line_edit_signal:
+            return
+
+        title = self.title_line_edit.text()
+
+        self.ignore_title_line_edit_signal = True
+
+        if self.session_gui.core_session.flow_title_valid(title):
+            self.session_gui.core_session.rename_flow(flow=self.flow, title=title)
+        else:
+            self.title_line_edit.setText(self.previous_flow_title)
+
+        self.title_line_edit.setEnabled(False)
+        self.ignore_title_line_edit_signal = False
diff --git a/ryvencore-qt/ryvencore_qt/src/widgets/ListWidget_NameLineEdit.py b/ryvencore-qt/ryvencore_qt/src/widgets/ListWidget_NameLineEdit.py
new file mode 100644
index 00000000..cfc8272d
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/widgets/ListWidget_NameLineEdit.py
@@ -0,0 +1,7 @@
+from qtpy.QtWidgets import QLineEdit
+
+
+class ListWidget_NameLineEdit(QLineEdit):
+
+    def __init__(self, text, parent):
+        super().__init__(text, parent)
diff --git a/ryvencore-qt/ryvencore_qt/src/widgets/LogWidget.py b/ryvencore-qt/ryvencore_qt/src/widgets/LogWidget.py
new file mode 100644
index 00000000..ae1f33fb
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/widgets/LogWidget.py
@@ -0,0 +1,56 @@
+from qtpy.QtGui import QFont
+from qtpy.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QLabel, QPushButton, QPlainTextEdit
+
+from ryvencore.addons.Logging import Logger
+import logging
+
+
+class LogWidget(QWidget):
+    """Convenience class for a QWidget representing a log."""
+
+    def __init__(self, logger: Logger):
+        super().__init__()
+
+        self.logger = logger
+        self.logger.addHandler(logging.StreamHandler(self))
+        self.logger.sig_disabled.sub(self.disable)
+        self.logger.sig_enabled.sub(self.enable)
+
+        self.main_layout = QVBoxLayout()
+        self.header_layout = QHBoxLayout()
+
+        title_label = QLabel(self.logger.name)
+        title_label.setFont(QFont('Poppins', 12))
+        self.header_layout.addWidget(title_label)
+
+        self.remove_button = QPushButton('x')
+        self.remove_button.clicked.connect(self.remove_clicked)
+        self.header_layout.addWidget(self.remove_button)
+        self.remove_button.hide()
+
+        self.text_edit = QPlainTextEdit()
+        self.text_edit.setReadOnly(True)
+
+        self.main_layout.addLayout(self.header_layout)
+        self.main_layout.addWidget(self.text_edit)
+
+        self.setLayout(self.main_layout)
+
+    def write(self, msg: str):
+        self.text_edit.appendPlainText(msg)
+
+    def flush(self):
+        pass
+
+    # def clear(self):
+    #     self.text_edit.clear()
+
+    def disable(self):
+        self.remove_button.show()
+
+    def enable(self):
+        self.remove_button.hide()
+        self.show()
+
+    def remove_clicked(self):
+        self.hide()
diff --git a/ryvencore-qt/ryvencore_qt/src/widgets/VariablesListWidget.py b/ryvencore-qt/ryvencore_qt/src/widgets/VariablesListWidget.py
new file mode 100644
index 00000000..5536d19c
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/widgets/VariablesListWidget.py
@@ -0,0 +1,118 @@
+from qtpy.QtWidgets import QVBoxLayout, QWidget, QLineEdit, QScrollArea
+from qtpy.QtCore import Qt
+
+from .VarsList_VarWidget import VarsList_VarWidget
+
+
+class VariablesListWidget(QWidget):
+    """Convenience class for a QWidget to easily manage script variables of a script."""
+
+    def __init__(self, vars_addon, flow):
+        super(VariablesListWidget, self).__init__()
+
+        self.vars_addon = vars_addon
+        self.flow = flow
+        self.vars_addon.var_created.sub(self.on_var_created)
+        self.vars_addon.var_deleted.sub(self.on_var_deleted)
+        self.widgets = []
+        self.currently_edited_var = ''
+        self.ignore_name_line_edit_signal = False  # because disabling causes firing twice otherwise
+        # self.data_type_line_edits = []  # same here
+
+        self.setup_UI()
+
+
+    def setup_UI(self):
+        main_layout = QVBoxLayout()
+
+        self.list_layout = QVBoxLayout()
+        self.list_layout.setAlignment(Qt.AlignTop)
+
+        # list scroll area
+
+        self.list_scroll_area = QScrollArea()
+        self.list_scroll_area.setWidgetResizable(True)
+        self.list_scroll_area.setContentsMargins(0, 0, 0, 0)
+
+        w = QWidget()
+        w.setContentsMargins(0, 0, 0, 0)
+        w.setLayout(self.list_layout)
+
+        self.list_scroll_area.setWidget(w)
+
+        main_layout.addWidget(self.list_scroll_area)
+
+        # controls
+
+        self.new_var_name_lineedit = QLineEdit()
+        self.new_var_name_lineedit.setPlaceholderText('new var\'s title')
+        self.new_var_name_lineedit.returnPressed.connect(self.new_var_LE_return_pressed)
+
+        main_layout.addWidget(self.new_var_name_lineedit)
+
+        self.setContentsMargins(0, 0, 0, 0)
+        self.setLayout(main_layout)
+
+        self.recreate_list()
+
+
+    def on_var_created(self, flow, name, var):
+        if flow == self.flow:
+            self.widgets.append(
+                VarsList_VarWidget(self, self.vars_addon, self.flow, var)
+            )
+            self.rebuild_list()
+
+
+    def on_var_deleted(self, flow, name):
+        # apparently, I cannot remove widgets the same way I add them
+        self.recreate_list()
+
+
+    def recreate_list(self):
+        for w in self.widgets:
+            w.hide()
+            del w
+
+        self.widgets.clear()
+        # self.data_type_line_edits.clear()
+
+        for var_name, var_info in self.vars_addon.flow_variables[self.flow].items():
+            new_widget = VarsList_VarWidget(self, self.vars_addon, self.flow, var_info['var'])
+            # new_widget.name_LE_editing_finished.connect(self.name_line_edit_editing_finished)
+            self.widgets.append(new_widget)
+
+        self.rebuild_list()
+
+
+    def rebuild_list(self):
+        for i in range(self.list_layout.count()):
+            self.list_layout.removeItem(self.list_layout.itemAt(0))
+
+        for w in self.widgets:
+            self.list_layout.addWidget(w)
+
+
+    def new_var_LE_return_pressed(self):
+        name = self.new_var_name_lineedit.text()
+        if not self.vars_addon.var_name_valid(self.flow, name=name):
+            return
+        v = self.vars_addon.create_var(self.flow, name=name)
+
+
+    # def name_line_edit_editing_finished(self):
+    #     var_widget: VarsList_VarWidget = self.sender()
+    #     var_widget.name_line_edit.setEnabled(False)
+    #
+    #     # search for name issues
+    #     new_var_name = var_widget.name_line_edit.text()
+    #     for v in self.vars_manager.variables:
+    #         if v.name == new_var_name:
+    #             var_widget.name_line_edit.setText(self.currently_edited_var.name)
+    #             return
+    #
+    #     var_widget.var.name = new_var_name
+
+
+    def del_var(self, var):
+        self.vars_addon.delete_var(self.flow, var.name)
diff --git a/ryvencore-qt/ryvencore_qt/src/widgets/VarsList_VarWidget.py b/ryvencore-qt/ryvencore_qt/src/widgets/VarsList_VarWidget.py
new file mode 100644
index 00000000..315634ea
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/widgets/VarsList_VarWidget.py
@@ -0,0 +1,146 @@
+from qtpy.QtWidgets import QWidget, QHBoxLayout, QLabel, QMenu, QAction
+from qtpy.QtGui import QIcon, QDrag
+from qtpy.QtCore import QMimeData, Qt, QEvent, QByteArray
+
+import json
+
+from ..GlobalAttributes import Location
+from .ListWidget_NameLineEdit import ListWidget_NameLineEdit
+from ..utils import shorten
+from .EditVal_Dialog import EditVal_Dialog
+from ryvencore.addons.Variables import VarsAddon
+
+
+class VarsList_VarWidget(QWidget):
+    """A QWidget representing a single script variable for the VariablesListWidget."""
+
+    def __init__(self, vars_list_widget, vars_addon: VarsAddon, flow, var):
+        super().__init__()
+
+        self.vars_addon = vars_addon
+        self.flow = flow
+        self.var = var
+        self.vars_list_widget = vars_list_widget
+        self.previous_var_name = ''  # for editing
+
+        self.ignore_name_line_edit_signal = False
+
+
+        # UI
+
+        main_layout = QHBoxLayout()
+        main_layout.setContentsMargins(0, 0, 0, 0)
+
+        # create icon
+
+        variable_icon = QIcon(Location.PACKAGE_PATH+'/resources/pics/variable_picture.png')
+
+        icon_label = QLabel()
+        icon_label.setFixedSize(15, 15)
+        icon_label.setStyleSheet('border:none;')
+        icon_label.setPixmap(variable_icon.pixmap(15, 15))
+        main_layout.addWidget(icon_label)
+
+        #   name line edit
+
+        self.name_line_edit = ListWidget_NameLineEdit(self.var.name, self)
+        self.name_line_edit.setPlaceholderText('name')
+        self.name_line_edit.setEnabled(False)
+        self.name_line_edit.editingFinished.connect(self.name_line_edit_editing_finished)
+
+        main_layout.addWidget(self.name_line_edit)
+
+        self.setLayout(main_layout)
+
+
+
+    def mouseDoubleClickEvent(self, event):
+        if event.button() == Qt.LeftButton:
+            if self.name_line_edit.geometry().contains(event.pos()):
+                self.name_line_edit_double_clicked()
+                return
+
+
+    def mousePressEvent(self, event):
+        if event.button() == Qt.LeftButton:
+            drag = QDrag(self)
+            mime_data = QMimeData()
+            data_text = self.get_drag_data()
+            data = QByteArray(bytes(data_text, 'utf-8'))
+            mime_data.setData('text/plain', data)
+            drag.setMimeData(mime_data)
+            drop_action = drag.exec_()
+            return
+
+
+    def event(self, event):
+        if event.type() == QEvent.ToolTip:
+            val_str = ''
+            try:
+                val_str = str(self.var.get())
+            except Exception as e:
+                val_str = "couldn't stringify value"
+            self.setToolTip('val type: '+str(type(self.var.get()))+'\nval: '+shorten(val_str, 3000, line_break=True))
+
+        return QWidget.event(self, event)
+
+
+    def contextMenuEvent(self, event):
+        menu: QMenu = QMenu(self)
+
+        delete_action = QAction('delete')
+        delete_action.triggered.connect(self.action_delete_triggered)
+
+        edit_value_action = QAction('edit value')
+        edit_value_action.triggered.connect(self.action_edit_val_triggered)
+
+        actions = [delete_action, edit_value_action]
+        for a in actions:
+            menu.addAction(a)
+
+        menu.exec_(event.globalPos())
+
+
+    def action_delete_triggered(self):
+        self.vars_list_widget.del_var(self.var)
+
+
+    def action_edit_val_triggered(self):
+        edit_var_val_dialog = EditVal_Dialog(self, self.var.get())
+        accepted = edit_var_val_dialog.exec_()
+        if accepted:
+            self.var.set(edit_var_val_dialog.get_val())
+            # self.vars_addon.create_var(self.flow, self.var.name, edit_var_val_dialog.get_val())
+
+
+    def name_line_edit_double_clicked(self):
+        self.name_line_edit.setEnabled(True)
+        self.name_line_edit.setFocus()
+        self.name_line_edit.selectAll()
+
+        self.previous_var_name = self.name_line_edit.text()
+
+
+    def get_drag_data(self):
+        data = {'type': 'variable',
+                'name': self.var.name,
+                'value': self.var.get()}  # value is probably unnecessary
+        data_text = json.dumps(data)
+        return data_text
+
+
+    def name_line_edit_editing_finished(self):
+        if self.ignore_name_line_edit_signal:
+            return
+
+        name = self.name_line_edit.text()
+
+        self.ignore_name_line_edit_signal = True
+
+        if self.vars_addon.var_name_valid(self.flow, name):
+            self.var.name = name
+        else:
+            self.name_line_edit.setText(self.previous_var_name)
+
+        self.name_line_edit.setEnabled(False)
+        self.ignore_name_line_edit_signal = False
diff --git a/ryvencore-qt/ryvencore_qt/src/widgets/__init__.py b/ryvencore-qt/ryvencore_qt/src/widgets/__init__.py
new file mode 100644
index 00000000..8cc20803
--- /dev/null
+++ b/ryvencore-qt/ryvencore_qt/src/widgets/__init__.py
@@ -0,0 +1,7 @@
+# list widgets
+from .VariablesListWidget import VariablesListWidget as VarsList
+from .FlowsListWidget import FlowsListWidget as FlowsList
+from ..flows.node_list_widget.NodeListWidget import NodeListWidget
+
+# logging
+from .LogWidget import LogWidget
diff --git a/ryvencore-qt/setup.cfg b/ryvencore-qt/setup.cfg
new file mode 100644
index 00000000..651e86fc
--- /dev/null
+++ b/ryvencore-qt/setup.cfg
@@ -0,0 +1,27 @@
+[metadata]
+name = ryvencore-qt
+version = v0.4.0a2
+author = Leon Thomm
+author_email = l.thomm@mailbox.org
+description = Qt frontend for ryvencore; Library for building Visual Node Editors
+long_description = file: README.md
+long_description_content_type = text/markdown
+license_file = LICENSE
+url = https://github.com/leon-thomm/ryvencore-qt
+project_urls =
+    Website = https://ryven.org
+classifiers =
+    Programming Language :: Python :: 3
+    License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)
+    Operating System :: OS Independent
+
+[options]
+packages = find:
+include_package_data = True
+python_requires = >=3.6
+install_requires =
+    ryvencore ==0.4.*
+    PySide2
+    QtPy
+    waiting
+    textdistance
diff --git a/ryvencore-qt/setup.py b/ryvencore-qt/setup.py
new file mode 100644
index 00000000..b024da80
--- /dev/null
+++ b/ryvencore-qt/setup.py
@@ -0,0 +1,4 @@
+from setuptools import setup
+
+
+setup()


Test patch:
diff --git a/ryven/unused/test.stl b/ryven-editor/ryven/unused/test.stl
similarity index 100%
rename from ryven/unused/test.stl
rename to ryven-editor/ryven/unused/test.stl
diff --git a/tests/built_in_nodes.py b/tests/built_in_nodes.py
deleted file mode 100644
index 4d39161e..00000000
--- a/tests/built_in_nodes.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import os
-import unittest
-from os.path import join, dirname
-
-import ryvencore as rc
-from ryven.node_env import init_node_env
-from ryven.main.packages.nodes_package import NodesPackage
-from ryven import import_nodes_package
-
-
-class BuiltInNodesTestCase(unittest.TestCase):
-
-    def test_load_builtin_nodes(self):
-        os.environ['RYVEN_MODE'] = 'no-gui'
-        init_node_env()
-        session = rc.Session(gui=False)
-        nodes, data_types = import_nodes_package(NodesPackage(
-            directory=join(dirname(__file__), '../ryven/main/packages/built_in/')
-        ))
-        session.register_data_types(data_types)
-        session.register_node_types(nodes)
-        print('registered nodes: ', nodes)
-
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tests/console.py b/tests/console.py
deleted file mode 100644
index a9d8bc0f..00000000
--- a/tests/console.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import json
-import unittest
-
-import ryvencore as rc
-from ryven.main.RyvenConsole import load_project_and_nodes
-
-
-class RyvenConsoleTestCase(unittest.TestCase):
-    # This class tests the working of the underlying methods used by RyvenConsole, i.e. that do not use
-    # the GUI elements of Ryven.
-
-    def test_load_empty_flow(self):
-        # Create empty flow
-        rc.Base.Base._global_id_ctr.ctr = -1
-        session = rc.Session()
-        session.create_flow('main')
-        serialize = session.serialize()
-        json_string = json.dumps(serialize)
-        del session
-
-        test_data_path = ".\\data\\test.json"
-        with open(test_data_path, "w") as f:
-            # Writing data to a file
-            f.write(json_string)
-
-        # Given how the Global IDs work (they are re-generated every time an object is created)
-        # we reset the counter manually to ensure the IDs are the same
-        rc.Base.Base._global_id_ctr.ctr = -1
-
-        # Reload file
-        session = rc.Session()
-        load_project_and_nodes(test_data_path, [], None, session)
-
-        self.assertEqual(serialize, session.serialize())
-
-
-if __name__ == '__main__':
-    unittest.main()