5 |
6 | `ryvencore-qt` provides Qt-based GUI classes for [ryvencore](https://github.com/leon-thomm/ryvencore), to provide a visual flow-based programming interface. The [Ryven](https://github.com/leon-thomm/Ryven) editor is built on top of `ryvencore-qt`, and their development is currently tightly coupled.
7 |
8 | ### Installation
9 |
10 | You need to have Python and pip installed. Then, either install from PyPI using pip:
11 |
12 | ```
13 | pip install ryvencore-qt
14 | ```
15 |
16 | or build from sources
17 |
18 | ```
19 | git clone https://github.com/leon-thomm/ryvencore-qt
20 | cd ryvencore-qt
21 | pip install .
22 | ```
23 |
24 | ### Dependencies
25 |
26 | ryvencore-qt uses Python bindings for Qt using [QtPy](https://github.com/spyder-ide/qtpy). I usually run it with PySide2, running on PySide6 should also work with minor changes. PyQt is not supported, due to crucial inheritance restrictions in PyQt.
27 |
28 | ### Documentation
29 |
30 | An extensive documentation doesn't currently exist.
31 |
32 | ### quick start
33 |
34 | The below code demonstrates how to set up an editor with custom defined nodes. You can also find the code in the *examples* folder.
35 |
36 | `main.py`
37 | ``` python
38 | # Qt
39 | import sys
40 | import os
41 | os.environ['QT_API'] = 'pyside2' # tells QtPy to use PySide2
42 | from qtpy.QtWidgets import QMainWindow, QApplication
43 |
44 | # ryvencore-qt
45 | import ryvencore_qt as rc
46 | from nodes import export_nodes
47 |
48 |
49 | if __name__ == "__main__":
50 |
51 | # first, we create the Qt application and a window
52 | app = QApplication()
53 | mw = QMainWindow()
54 |
55 | # now we initialize a new ryvencore-qt session
56 | session = rc.Session()
57 | session.design.set_flow_theme(name='pure light') # setting the design theme
58 |
59 | # and register our nodes
60 | session.register_nodes(export_nodes)
61 |
62 | # to get a flow where we can place nodes, we need to crate a new script
63 | script = session.create_script('hello world', flow_view_size=[800, 500])
64 |
65 | # getting the flow widget of the newly created script
66 | flow_view = session.flow_views[script]
67 | mw.setCentralWidget(flow_view) # and show it in the main window
68 |
69 | # finally, show the window and run the application
70 | mw.show()
71 | sys.exit(app.exec_())
72 | ```
73 |
74 | `nodes.py`
75 | ```python
76 | import ryvencore_qt as rc
77 | from random import random
78 |
79 |
80 | # let's define some nodes
81 | # to easily see something in action, we create one node generating random numbers, and one that prints them
82 |
83 | class PrintNode(rc.Node):
84 | """Prints your data"""
85 |
86 | title = 'Print'
87 | init_inputs = [
88 | rc.NodeInputBP(),
89 | ]
90 | init_outputs = []
91 | color = '#A9D5EF'
92 |
93 | # we could also skip the constructor here
94 | def __init__(self, params):
95 | super().__init__(params)
96 |
97 | def update_event(self, inp=-1):
98 | print(
99 | self.input(0) # get data from the first input
100 | )
101 |
102 |
103 | class RandNode(rc.Node):
104 | """Generates scaled random float values"""
105 |
106 | title = 'Rand'
107 | init_inputs = [
108 | rc.NodeInputBP(dtype=rc.dtypes.Data(default=1)),
109 | ]
110 | init_outputs = [
111 | rc.NodeOutputBP(),
112 | ]
113 | color = '#fcba03'
114 |
115 | def update_event(self, inp=-1):
116 | # random float between 0 and value at input
117 | val = random() * self.input(0)
118 |
119 | # setting the value of the first output
120 | self.set_output_val(0, val)
121 |
122 |
123 | export_nodes = [
124 | PrintNode,
125 | RandNode,
126 | ]
127 | ```
128 |
129 | ### Development
130 |
131 | The individual subpackages have their own READMEs giving a quick overview which should be quite helpful to gain understanding about implementations.
132 |
133 | Cheers.
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # removing macros
2 |
3 | - [x] remove macro related fields in Session
4 | - [x] remove macro options in ScriptsListwidget
5 | - [x] backup `MacroScript.py` and `MacroNodesTypes.py` in Ryven `macros` package
6 | - [x] delete `Macroscript.py`, `MacroNodeTypes.py`
7 | - [x] check `__init__.py` files
8 | - [x] check `Script.py`
9 | - [x] check for remaining 'macro' occurrences
10 |
11 | # cleanups
12 |
13 | - [x] packages in `setup.py`
14 |
15 | # compilable core
16 |
17 | - [x] implement signaling system with minimal interface in `ryvencore`
18 | - [ ] ~~implement translation system in `ryvencore-qt` to convert those into qt signals~~
19 | - [X] subclass `ryvencore.Session` in `ryvencore-qt` and expose in `ryvencore_qt.__init__`
20 | - [x] subclass `ryvencore.Node` in `ryvencore-qt` and expose in `ryvencore_qt.__init__`
21 | - [x] remove all `Session.CLASSES` dependencies in the core (maybe just keep it in the rcqt Session?)
22 |
23 | # release notes
24 |
25 | - change all `set_state(self, data)` to `set_state(self, data, version)`
26 | - add version tags to your nodes
27 | - add tag lists to your nodes
28 | - load, repair, save and verify your projects
29 | - macros?
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/docs/.nojekyll
--------------------------------------------------------------------------------
/docs/img/function_node.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/docs/img/function_node.png
--------------------------------------------------------------------------------
/docs/img/logic_editor_screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/docs/img/logic_editor_screenshot1.png
--------------------------------------------------------------------------------
/docs/img/logic_editor_screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/docs/img/logic_editor_screenshot2.png
--------------------------------------------------------------------------------
/docs/img/logic_editor_screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/docs/img/logic_editor_screenshot3.png
--------------------------------------------------------------------------------
/docs/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/docs/img/logo.png
--------------------------------------------------------------------------------
/docs/img/macro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/docs/img/macro.png
--------------------------------------------------------------------------------
/docs/img/macro2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/docs/img/macro2.png
--------------------------------------------------------------------------------
/docs/img/ryvencore-drawio_.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/docs/img/ryvencore-drawio_.png
--------------------------------------------------------------------------------
/docs/img/ryvencore_screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/docs/img/ryvencore_screenshot1.png
--------------------------------------------------------------------------------
/docs/img/stylus_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/docs/img/stylus_light.png
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/docs/unused/_sidebar.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | * [Welcome](/)
4 | * [Features](features.md)
5 |
6 | [comment]: <> (* [API](api.md))
--------------------------------------------------------------------------------
/docs/unused/gui.md:
--------------------------------------------------------------------------------
1 | # GUI
2 |
3 | Adding intuitive GUI to your nodes is of mayor importance to create a nice interface. Therefore, in ryvencore you can register your very own widget classes and are not restricted to some fixed set of available standard widgets. However, there are also a few convenience widgets which make your life a lot easier.
4 |
5 | ## Convenience GUI Classes
6 |
7 | All those classes only use ryvencore's public API and you could implement them all yourself. The list should grow over time. All these are classes come from Ryven.
8 |
9 | ### Script List Widget
10 |
11 | A simple list widget for creating, renaming and deleting scripts and function-scripts. To catch the according events (i.e. `script_created`, `script_renamed` etc), use the signals of the `Session` class.
12 |
13 | ### Variables List Widget
14 |
15 | A synchronous widget to the script list widget for script variables. You can create, rename, delete script variables and change their values which results in all registered receivers to update.
16 |
17 | ### Log Widget
18 |
19 | A very basic widget for outputting data of a log. Use the `Script.logger.new_log_created()` signal to catch instantiation of new logs. If you want to implement your own, you will need the `Log`'s signals `enabled`, `disabled`, `cleared`, `wrote`.
20 |
21 | ### Input Widgets
22 |
23 | - `std line edit ` aka `std line edit m`, `std line edit s`, `std line edit l`
24 | - `std spin box`
25 |
26 | For styling those, refer to thir classes `RCIW_BUILTIN_LineEdit`, `RCIW_BUILTIN_SpinBox`.
27 |
28 | I really would like to add many more widgets to this list in the future.
29 |
30 | ## Writing your own GUI
31 |
32 | All custom widgets must be QWidgets and subclass one of ryvencore's widget base classes. Both classes have similar functionality for serialization and loading as `Node`. They have methods `get_state() -> dict` and `set_state(data: dict)` (and also a `remove_event()` right now) to subclass.
33 |
34 | ### Main Widget
35 |
36 | A main widget must additionally subclass ryvencore's `MWB` (MainWidgetBase) class. Example:
37 |
38 | ```python
39 | import ryvencore_qt as rc
40 | from PySide2.QtWidgets import QPushButton
41 |
42 |
43 | class MyMainWidget(rc.NodeMainWidget, QPushButton):
44 | def __init__(self, params):
45 | rc.NodeMainWidget.__init__(self, params)
46 | QPushButton.__init__(self)
47 |
48 | # then do your stuff like
49 | self.setEnabled(False)
50 | self.clicked.connect(self.node.update)
51 | ```
52 |
53 | After `rc.MWB.__init__(self, params)`, you have access to the node object via `self.node`. A custom main widget class must be referenced in the node's class definition
54 |
55 | ```python
56 | class MyNode(rc.Node):
57 |
58 | title = '...'
59 | # ...
60 | main_widget_class = MyMainWidget
61 | # ...
62 |
63 | def __init__(self, params):
64 | super().__init__(params)
65 |
66 | # ...
67 | ```
68 |
69 | ### Input Widget
70 |
71 | An input widget must additionally subclass ryvencore's `IWB` (InputWidgetBase) class. The initialization process is exactly the same as with `MWB` shown in the example above. After the `IWB` constructor the refs `self.node` to the node object and `self.input` for the node's input port object which contains the widget are available. Custom input widget classes, together with the names that should be used to refer to them node internally, must be referenced in the node's class definition
72 |
73 | ```python
74 | class MyNode(rc.Node):
75 |
76 | title = '...'
77 | # ...
78 | input_widget_classes = {'some input widget': MyInputWidget}
79 | # ...
80 |
81 | def __init__(self, params):
82 | super().__init__(params)
83 |
84 | # ...
85 | ```
--------------------------------------------------------------------------------
/docs/unused/threading.md:
--------------------------------------------------------------------------------
1 | # Threading
2 |
3 | ## Overview
4 |
5 | One of the biggest internal changes in `ryvencore-qt`/`ryvencore` compared to Ryven 2, is the threading compatibility. `ryvencore` provides abstract components and `ryvencore-qt` provides the Qt GUI for the flows (plus some convenience widgets). Therefore, in order to run a session instance in a thread different from the GUI thread, the internal communication between `ryvencore` and `ryvencore-qt` must be set up in a thread save way. To achieve this, `ryvencore-qt` provides custom *WRAPPER* classes for all `ryvencore` components that the frontend needs to communicate with directly. Those wrappers do almost nothing but adding Qt signals to all the API methods, so every time an API method of the backend has been executed, a Qt signal is sent which the frontend components who need to get notified of this API method execution (like creation of a new node must notify the `FlowView`) listen for.
6 |
7 | > [!WARNING]
8 | > `Session.serialize()` shouldn't be called in concurrently as the the serialization of the `FlowView` is currently joined with the session's thread by setting an attribute (see implementation). This may change.
9 |
10 | ## Programming Nodes
11 |
12 | A `Node` object lives in the same thread as the `Session` object. Their GUI items, however, including all **custom widgets**, live in the GUI thread. Therefore, you need to make sure that the communication between your nodes and their custom widgets is thread save, which usually means: implemented using Qt's signals and slots.
13 |
14 | ### Communication from Nodes to Widgets
15 |
16 | Use the `Node.view_place_event()` to manage initial connections between the node and its widgets which will exist at this point.
17 |
18 | Example:
19 | ```python
20 | import ryvencore_qt as rc
21 |
22 |
23 | class MyNode(rc.Node):
24 | # ...
25 |
26 | def __init__(self, params):
27 | super().__init__(params)
28 |
29 | if self.session.gui:
30 | from PySide2.QtCore import QObject, Signal
31 |
32 | class SIGNALS(QObject):
33 | notify_gui = Signal(object)
34 |
35 | self.signals = SIGNALS
36 |
37 | def view_place_event(self):
38 | # remember, the view_place_event will only be called when there is frontend
39 | self.signals.notify_gui.connect(self.main_widget().some_method)
40 | self.main_widget().some_input_signal.connect(self.process_input)
41 |
42 | def update_event(self, inp=-1):
43 | # ...
44 | self.signals.notify_gui.emit(something)
45 | # ...
46 | ```
47 |
48 | ### Communication from Widgets to Nodes
49 |
50 | For custom widgets, since QWidgets are QObjects, you can directly add all your signals as static attributes to your custom widget class.
51 |
--------------------------------------------------------------------------------
/drawio_v0.0.2.xml:
--------------------------------------------------------------------------------
1 | 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
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | Here you can find some examples / tutorials which might be helpful. Also make sure to read over the below list of implementation-focused features for building visual node editors with this library. Furthermore, for node implementations there is a directory with examples in the Ryven repo.
4 |
5 | ## Features
6 |
7 | #### load & save
8 | All serialization and loading of projects. Data is stored using `json`, and for some parts `pickle`.
9 |
10 | ```python
11 | project: dict = my_session.data()
12 | with open(filepath, 'w') as f:
13 | f.write(json.dumps(project))
14 | ```
15 |
16 | #### simple nodes system
17 | All information of a node is part of its class. A minimal node definition can be as simple as this
18 |
19 | ```python
20 | import ryvencore_qt as rc
21 |
22 | class PrintNode(rc.Node):
23 | """Prints your data."""
24 |
25 | title = 'Print'
26 | init_inputs = [
27 | rc.NodeInputBP()
28 | ]
29 | color = '#A9D5EF'
30 |
31 | def update_event(self, inp=-1):
32 | print(self.input(0))
33 | ```
34 |
35 | #### dynamic nodes registration mechanism
36 | You can register and unregister nodes at any time. Registered nodes can be placed in a flow.
37 | ```python
38 | my_session.register_nodes( [ ] )
39 | ```
40 |
41 | #### right-click operations system for nodes
42 | which can be edited through the API at any time
43 | ```python
44 | self.actions[f'remove input {i}'] = {
45 | 'method': self.rem_input,
46 | 'data': i,
47 | }
48 |
49 | # with some method...
50 | def rem_input(self, index):
51 | self.delete_input(index)
52 | del self.actions[f'remove input {len(self.inputs)}']
53 | ```
54 |
55 | #### Qt widgets
56 | You can add custom QWidgets for the frontend of your nodes.
57 |
58 | ```python
59 | class MyNode(rc.Node):
60 | #...
61 | main_widget_class: QWidget = MyNodeMainWidget
62 | main_widget_pos = 'below ports' # alternatively 'between ports'
63 | # ...
64 | ```
65 |
66 | #### many different modifiable themes
67 | See [Features Page](https://leon-thomm.github.io/ryvencore-qt/features/).
68 |
69 | #### exec flow support
70 | While data flows should be the most common use case, exec flows (like [UnrealEngine BluePrints](https://docs.unrealengine.com/4.26/en-US/ProgrammingAndScripting/Blueprints/)) are also supported.
71 |
72 | #### logging support
73 | ```python
74 | import logging
75 |
76 | class MyNode(rc.Node):
77 | def __init__(self, params):
78 | super().__init__(params)
79 |
80 | self.my_logger = self.new_logger(title='nice log')
81 |
82 | def update_event(self, inp=-1):
83 | self.my_logger.info('updated!')
84 | ```
85 |
86 | #### variables system
87 | with an update mechanism to build nodes that automatically adapt to change of variables
88 |
89 | ```python
90 | import logging
91 |
92 | class MyNode(rc.Node):
93 | # ...
94 |
95 | def somewhere(self):
96 | self.register_var_receiver(name='some_var_name', method=process_new_var_val)
97 |
98 | # with a method
99 | def process_new_var_val(self, val):
100 | print(f'received a new val!\n{val}\n')
101 | ```
102 |
103 | #### stylus support for adding handwritten notes
104 | 
105 |
106 | #### rendering flow images
107 | ...
--------------------------------------------------------------------------------
/examples/readme/main.py:
--------------------------------------------------------------------------------
1 | # Qt
2 | import sys
3 | import os
4 | os.environ['QT_API'] = 'pyside2' # tells QtPy to use PySide2
5 | from qtpy.QtWidgets import QMainWindow, QApplication
6 |
7 | # ryvencore-qt
8 | import ryvencore_qt as rc
9 | from nodes import export_nodes
10 |
11 |
12 | if __name__ == "__main__":
13 |
14 | # first, we create the Qt application and a window
15 | app = QApplication()
16 | mw = QMainWindow()
17 |
18 | # now we initialize a new ryvencore-qt session
19 | session = rc.Session()
20 | session.design.set_flow_theme(name='pure light') # setting the design theme
21 |
22 | # and register our nodes and create a script
23 | session.register_nodes(export_nodes)
24 |
25 | # to get a flow where we can place nodes, we need to crate a new script
26 | script = session.create_script('hello world', flow_view_size=[800, 500])
27 |
28 | # getting the flow widget of the newly created script
29 | flow_view = session.flow_views[script]
30 | mw.setCentralWidget(flow_view) # and show it in the main window
31 |
32 | # finally, show the window and run the application
33 | mw.show()
34 | sys.exit(app.exec_())
--------------------------------------------------------------------------------
/examples/readme/nodes.py:
--------------------------------------------------------------------------------
1 | import ryvencore_qt as rc
2 | from random import random
3 |
4 |
5 | # let's define some nodes
6 | # to easily see something in action, we create one node generating random numbers, and one that prints them
7 |
8 | class PrintNode(rc.Node):
9 | """Prints your data"""
10 |
11 | title = 'Print'
12 | init_inputs = [
13 | rc.NodeInputBP(),
14 | ]
15 | init_outputs = []
16 | color = '#A9D5EF'
17 |
18 | # we could also skip the constructor here
19 | def __init__(self, params):
20 | super().__init__(params)
21 |
22 | def update_event(self, inp=-1):
23 | print(
24 | self.input(0) # get data from the first input
25 | )
26 |
27 |
28 | class RandNode(rc.Node):
29 | """Generates scaled random float values"""
30 |
31 | title = 'Rand'
32 | init_inputs = [
33 | rc.NodeInputBP(dtype=rc.dtypes.Data(default=1)),
34 | ]
35 | init_outputs = [
36 | rc.NodeOutputBP(),
37 | ]
38 | color = '#fcba03'
39 |
40 | def update_event(self, inp=-1):
41 | # random float between 0 and value at input
42 | val = random() * self.input(0)
43 |
44 | # setting the value of the first output
45 | self.set_output_val(0, val)
46 |
47 |
48 | export_nodes = [
49 | PrintNode,
50 | RandNode,
51 | ]
--------------------------------------------------------------------------------
/examples/readme/readme.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | from random import random
4 |
5 | os.environ['QT_API'] = 'pyside2' # tells qtpy to use PyQt5
6 | import ryvencore_qt as rc
7 | from qtpy.QtWidgets import QMainWindow, QApplication
8 |
9 |
10 | class PrintNode(rc.Node):
11 | """Prints your data"""
12 |
13 | # all basic properties
14 | title = 'Print'
15 | init_inputs = [
16 | rc.NodeInputBP()
17 | ]
18 | init_outputs = []
19 | color = '#A9D5EF'
20 |
21 | # see API doc for a full list of properties
22 |
23 | # we could also skip the constructor here
24 | def __init__(self, params):
25 | super().__init__(params)
26 |
27 | def update_event(self, inp=-1):
28 | data = self.input(0) # get data from the first input
29 | print(data)
30 |
31 |
32 | class RandNode(rc.Node):
33 | """Generates random float"""
34 |
35 | title = 'Rand'
36 | init_inputs = [
37 | rc.NodeInputBP(dtype=rc.dtypes.Data(default=1))
38 | ]
39 | init_outputs = [
40 | rc.NodeOutputBP()
41 | ]
42 | color = '#fcba03'
43 |
44 | def update_event(self, inp=-1):
45 | # random float between 0 and value at input
46 | val = random() * self.input(0)
47 |
48 | # setting the value of the first output
49 | self.set_output_val(0, val)
50 |
51 |
52 | if __name__ == "__main__":
53 |
54 | # creating the application and a window
55 | app = QApplication()
56 | mw = QMainWindow()
57 |
58 | # creating the session, registering, creating script
59 | session = rc.Session()
60 | session.design.set_flow_theme(name='pure light')
61 | session.register_nodes([PrintNode, RandNode])
62 | script = session.create_script('hello world', flow_view_size=[800, 500])
63 |
64 | mw.setCentralWidget(session.flow_views[script])
65 |
66 | mw.show()
67 | sys.exit(app.exec_())
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = [
3 | "setuptools",
4 | "wheel"
5 | ]
6 | build-backend = "setuptools.build_meta"
--------------------------------------------------------------------------------
/ryvencore_qt/__init__.py:
--------------------------------------------------------------------------------
1 | # set package path (for resources etc.)
2 | import os
3 | from .src.GlobalAttributes import Location
4 | Location.PACKAGE_PATH = os.path.normpath(os.path.dirname(__file__))
5 |
6 | os.environ['RC_MODE'] = 'gui' # set ryvencore gui mode
7 | os.environ['QT_ENABLE_HIGHDPI_SCALING'] = '1'
8 |
9 | # expose ryvencore
10 | import ryvencore
11 |
12 | from .src.SessionGUI import SessionGUI
13 | from .src.flows.nodes.NodeGUI import NodeGUI
14 |
15 | # customer base classes
16 | from ryvencore import Node
17 | from .src.flows.nodes.WidgetBaseClasses import NodeMainWidget, NodeInputWidget
18 |
19 | # gui classes
20 | from .src.widgets import *
21 | from .src.flows.FlowTheme import flow_themes
22 |
--------------------------------------------------------------------------------
/ryvencore_qt/resources/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/__init__.py
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/__init__.py
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/asap/Asap-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/asap/Asap-Bold.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/asap/Asap-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/asap/Asap-BoldItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/asap/Asap-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/asap/Asap-Italic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/asap/Asap-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/asap/Asap-Medium.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/asap/Asap-MediumItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/asap/Asap-MediumItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/asap/Asap-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/asap/Asap-Regular.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/asap/Asap-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/asap/Asap-SemiBold.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/asap/Asap-SemiBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/asap/Asap-SemiBoldItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/asap/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright 2018 The Asap Project Authors (https://github.com/Omnibus-Type/Asap).
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/asap/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/asap/__init__.py
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright 2014-2017 Indian Type Foundry (info@indiantypefoundry.com)
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-Black.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-BlackItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-BlackItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-Bold.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-BoldItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraBold.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraBoldItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraLight.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraLight.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraLightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-ExtraLightItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-Italic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-Light.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-LightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-LightItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-Medium.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-MediumItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-MediumItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-Regular.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-SemiBold.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-SemiBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-SemiBoldItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-Thin.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/Poppins-ThinItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/Poppins-ThinItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/poppins/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/poppins/__init__.py
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/OFL.txt:
--------------------------------------------------------------------------------
1 | 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.
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Black.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-BlackItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-BlackItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Bold.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-BoldItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-ExtraLight.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-ExtraLight.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-ExtraLightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-ExtraLightItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Italic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Light.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-LightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-LightItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Medium.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-MediumItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-MediumItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-Regular.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-SemiBold.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-SemiBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/source_code_pro/SourceCodePro-SemiBoldItalic.ttf
--------------------------------------------------------------------------------
/ryvencore_qt/resources/fonts/source_code_pro/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/fonts/source_code_pro/__init__.py
--------------------------------------------------------------------------------
/ryvencore_qt/resources/node_collapse_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
76 |
--------------------------------------------------------------------------------
/ryvencore_qt/resources/node_expand_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
76 |
--------------------------------------------------------------------------------
/ryvencore_qt/resources/pics/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/pics/__init__.py
--------------------------------------------------------------------------------
/ryvencore_qt/resources/pics/code_block_picture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/pics/code_block_picture.png
--------------------------------------------------------------------------------
/ryvencore_qt/resources/pics/code_block_picture2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/pics/code_block_picture2.png
--------------------------------------------------------------------------------
/ryvencore_qt/resources/pics/code_block_picture3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/pics/code_block_picture3.png
--------------------------------------------------------------------------------
/ryvencore_qt/resources/pics/function_picture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/pics/function_picture.png
--------------------------------------------------------------------------------
/ryvencore_qt/resources/pics/function_picture_.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/pics/function_picture_.png
--------------------------------------------------------------------------------
/ryvencore_qt/resources/pics/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/pics/logo.png
--------------------------------------------------------------------------------
/ryvencore_qt/resources/pics/macro_node_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/pics/macro_node_icon.png
--------------------------------------------------------------------------------
/ryvencore_qt/resources/pics/macro_script_picture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/pics/macro_script_picture.png
--------------------------------------------------------------------------------
/ryvencore_qt/resources/pics/opencv_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/pics/opencv_example.png
--------------------------------------------------------------------------------
/ryvencore_qt/resources/pics/script_picture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/pics/script_picture.png
--------------------------------------------------------------------------------
/ryvencore_qt/resources/pics/script_picture.svg:
--------------------------------------------------------------------------------
1 |
2 |
57 |
--------------------------------------------------------------------------------
/ryvencore_qt/resources/pics/script_picture_old.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/pics/script_picture_old.png
--------------------------------------------------------------------------------
/ryvencore_qt/resources/pics/variable_picture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/pics/variable_picture.png
--------------------------------------------------------------------------------
/ryvencore_qt/resources/pics/warning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/resources/pics/warning.png
--------------------------------------------------------------------------------
/ryvencore_qt/resources/warning.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
114 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/Design.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from qtpy.QtCore import QObject, Signal
4 | from qtpy.QtGui import QFontDatabase
5 |
6 | from .flows.FlowTheme import FlowTheme, flow_themes
7 | from .GlobalAttributes import Location
8 |
9 |
10 | class Design(QObject):
11 | """Design serves as a container for the stylesheet and flow themes, and sends signals to notify GUI elements
12 | on change of the flow theme. A configuration for the flow themes can be loaded from a json file."""
13 |
14 | global_stylesheet = ''
15 |
16 | flow_theme_changed = Signal(str)
17 | performance_mode_changed = Signal(str)
18 |
19 | def __init__(self):
20 | super().__init__()
21 |
22 | self.flow_themes = flow_themes
23 | self.flow_theme: FlowTheme = None
24 | self.default_flow_size = None
25 | self.performance_mode: str = None
26 | self.node_item_shadows_enabled: bool = None
27 | self.animations_enabled: bool = None
28 | self.node_selection_stylesheet: str = None
29 |
30 | # load standard default values
31 | self._default_flow_theme = self.flow_themes[-1]
32 | self.set_performance_mode('pretty')
33 | self.set_animations_enabled(True)
34 | self.default_flow_size = [1000, 700]
35 | self.set_flow_theme(self._default_flow_theme)
36 |
37 | @staticmethod
38 | def register_fonts():
39 | db = QFontDatabase()
40 | db.addApplicationFont(
41 | Location.PACKAGE_PATH + '/resources/fonts/poppins/Poppins-Medium.ttf'
42 | )
43 | db.addApplicationFont(
44 | Location.PACKAGE_PATH + '/resources/fonts/source_code_pro/SourceCodePro-Regular.ttf'
45 | )
46 | db.addApplicationFont(
47 | Location.PACKAGE_PATH + '/resources/fonts/asap/Asap-Regular.ttf'
48 | )
49 |
50 | def load_from_config(self, filepath: str):
51 | """Loads design configs from a config json file"""
52 |
53 | f = open(filepath, 'r')
54 | data = f.read()
55 | f.close()
56 |
57 | IMPORT_DATA = json.loads(data)
58 |
59 | if 'flow themes' in IMPORT_DATA:
60 | # load flow theme configs
61 | FTID = IMPORT_DATA['flow themes']
62 | for flow_theme in self.flow_themes:
63 | flow_theme.load(FTID)
64 |
65 | if 'init flow theme' in IMPORT_DATA:
66 | self._default_flow_theme = self.flow_theme_by_name(IMPORT_DATA.get('init flow theme'))
67 | self.set_flow_theme(self._default_flow_theme)
68 |
69 | if 'init performance mode' in IMPORT_DATA:
70 | self.set_performance_mode(IMPORT_DATA['init performance mode'])
71 |
72 | if 'init animations enabled' in IMPORT_DATA:
73 | self.set_animations_enabled(IMPORT_DATA['init animations enabled'])
74 |
75 | if 'default flow size' in IMPORT_DATA:
76 | self.default_flow_size = IMPORT_DATA['default flow size']
77 |
78 | def available_flow_themes(self) -> dict:
79 | return {theme.name: theme for theme in self.flow_themes}
80 |
81 | def flow_theme_by_name(self, name: str) -> FlowTheme:
82 | for theme in self.flow_themes:
83 | if theme.name.casefold() == name.casefold():
84 | return theme
85 | return None
86 |
87 | def set_flow_theme(self, theme: FlowTheme = None, name: str = ''):
88 | """You can either specify the theme by name, or directly provide a FlowTheme object"""
89 | if theme:
90 | self.flow_theme = theme
91 | elif name and name != '':
92 | self.flow_theme = self.flow_theme_by_name(name)
93 | else:
94 | return
95 |
96 | self.node_selection_stylesheet = self.flow_theme.build_node_selection_stylesheet()
97 |
98 | self.flow_theme_changed.emit(self.flow_theme.name)
99 |
100 |
101 | def set_performance_mode(self, new_mode: str):
102 | self.performance_mode = new_mode
103 | if new_mode == 'fast':
104 | self.node_item_shadows_enabled = False
105 | else:
106 | self.node_item_shadows_enabled = True
107 |
108 | self.performance_mode_changed.emit(self.performance_mode)
109 |
110 | def set_animations_enabled(self, b: bool):
111 | self.animations_enabled = b
112 |
113 | def set_node_item_shadows(self, b: bool):
114 | self.node_item_shadows_enabled = b
115 |
116 |
117 |
118 |
119 |
120 | # default_node_selection_stylesheet = '''
121 | # '''
122 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/GUIBase.py:
--------------------------------------------------------------------------------
1 | from ryvencore.Base import Base
2 |
3 |
4 | class GUIBase:
5 | """Base class for GUI items that represent specific backend components"""
6 |
7 | # every frontend GUI object that represents some specific component from the backend
8 | # is stored there under the the global id of the represented component.
9 | # used for completing data (serialization)
10 | FRONTEND_COMPONENT_ASSIGNMENTS = {} # component global id : GUI object
11 |
12 | @staticmethod
13 | def get_complete_data_function(session):
14 | """
15 | generates a function that searches through generated data by the backend and calls
16 | complete_data() on frontend components that represent them to add frontend data
17 | """
18 |
19 | def analyze(obj):
20 | """Searches recursively through obj and calls complete_data(obj) on associated
21 | frontend components (instances of GUIBase)"""
22 |
23 | if isinstance(obj, dict):
24 | GID = obj.get('GID')
25 | if GID is not None:
26 | # find representative
27 | comp = GUIBase.FRONTEND_COMPONENT_ASSIGNMENTS.get(GID)
28 | if comp:
29 | obj = comp.complete_data(obj)
30 |
31 | # look for child objects
32 | for key, value in obj.items():
33 | obj[key] = analyze(value)
34 |
35 | elif isinstance(obj, list):
36 | for i in range(len(obj)):
37 | item = obj[i]
38 | item = analyze(item)
39 | obj[i] = item
40 |
41 | return obj
42 |
43 | return analyze
44 |
45 | def __init__(self, representing_component: Base = None):
46 | """parameter `representing` indicates representation of a specific backend component"""
47 | if representing_component is not None:
48 | GUIBase.FRONTEND_COMPONENT_ASSIGNMENTS[representing_component.global_id] = self
49 |
50 | # OVERRIDE
51 | def complete_data(self, data: dict) -> dict:
52 | """completes the data dict of the represented backend component by adding all frontend data"""
53 | return data
54 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/GlobalAttributes.py:
--------------------------------------------------------------------------------
1 | class Location:
2 | PACKAGE_PATH = None
3 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/RCQT.py:
--------------------------------------------------------------------------------
1 | """Namespace for the frontend"""
2 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/README.md:
--------------------------------------------------------------------------------
1 | # `ryvencore-qt` - overview
2 |
3 | ## modules
4 |
5 | - `Design.py` manages the flow designs (themes, animations, performance mode etc.).
6 | - `GlobalAttributes.py` stores some static information, currently just the package location after installation.
7 | - The `GUIBase` is the base class for all frontend components and implements the `complete_data()` function to complete the data dicts from core components by adding all state defining data of frontend representations of those components. For example a `NodeItem` implements `complete_data()` to complete the data dict generated by its `Node` object and adds some fields like color, position in scene, display title etc.
8 | - The `SessionThreadInterface` provides an abstraction to perform communication between the frontend thread (`FT`), and the core thread (`CT`), in case `FT != CT`. A frontend component can trigger method execution in `CT` and get the method result in the same step by `SessionThreadInterface_Frontend.run()`. A backend component can do the same thing in the other direction by using the complementary `SessionThreadInterface_Backend.run()`. Notice that, as soon as there is a remote-capable JS-based frontend alternative, I will probably remove this from here, as threading compatibility strongly complicates the frontend here.
9 | - `utils.py` includes some important functions that may be used in different places.
10 |
11 | ## packages
12 |
13 | - `conv_gui` hosts some convenience GUI widget classes to quickly build a small editor with most basic features.
14 | - `core_wrapper` contains the `ryvencore` wrapper, adding some Qt signals to API methods, etc. These reimplementations defined in `core_wrapper` are stored in the session's `CLASSES` dict.
15 | - `flows` hosts all the GUI regarding flows and everything contained.
16 | - `ryvencore` is the core/engine/backend, currently still part of this repository because its development is closely tied to `ryvencore-qt` at the moment, but using `import ryvencore_qt.ryvencore` you can use `ryvencore` directly without anything from `ryvencore-qt`.
--------------------------------------------------------------------------------
/ryvencore_qt/src/SessionGUI.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from qtpy.QtCore import QObject, Signal, Qt
4 | from qtpy.QtWidgets import QWidget, QApplication
5 |
6 | import ryvencore
7 |
8 | from .flows.FlowView import FlowView
9 | from .Design import Design
10 | from .GUIBase import GUIBase
11 |
12 |
13 | class SessionGUI(GUIBase, QObject):
14 | """
15 | ryvencore-qt's Session wrapper class, implementing the GUI.
16 | Any session with a GUI must be created through this class.
17 | Access the ryvencore session through the :code:`session`
18 | attribute, and the GUI from the ryvencore session through the
19 | :code:`gui` attribute. Once instantiated, you can simply use
20 | the :code:`session` directly to create, rename, delete flows,
21 | register nodes, etc.
22 | """
23 |
24 | flow_created = Signal(object)
25 | flow_deleted = Signal(object)
26 | flow_renamed = Signal(object, str)
27 | flow_view_created = Signal(object, object)
28 |
29 | def __init__(self, gui_parent: QWidget):
30 | GUIBase.__init__(self)
31 | QObject.__init__(self)
32 |
33 | self.core_session = ryvencore.Session(gui=True, load_addons=True)
34 | setattr(self.core_session, 'gui', self)
35 |
36 | self.gui_parent = gui_parent
37 |
38 | # flow views
39 | self.flow_views = {} # {Flow : FlowView}
40 |
41 | # register complete_data function
42 | ryvencore.set_complete_data_func(self.get_complete_data_function(self))
43 |
44 | # load design
45 | app = QApplication.instance()
46 | app.setAttribute(Qt.AA_UseHighDpiPixmaps)
47 | Design.register_fonts()
48 | self.design = Design()
49 |
50 | # connect to session
51 | self.core_session.flow_created.sub(self._flow_created)
52 | self.core_session.flow_deleted.sub(self._flow_deleted)
53 | self.core_session.flow_renamed.sub(self._flow_renamed)
54 |
55 | def _flow_created(self, flow: ryvencore.Flow):
56 | """
57 | Builds the flow view for a newly created flow, saves it in
58 | self.flow_views, and emits the flow_view_created signal.
59 | """
60 | self.flow_created.emit(flow)
61 |
62 | self.flow_views[flow] = FlowView(
63 | session_gui=self,
64 | flow=flow,
65 | parent=self.gui_parent,
66 | )
67 | self.flow_view_created.emit(flow, self.flow_views[flow])
68 |
69 | return flow
70 |
71 | def _flow_deleted(self, flow: ryvencore.Flow):
72 | """
73 | Removes the flow view for a deleted flow from self.flow_views.
74 | """
75 | self.flow_views.pop(flow)
76 | self.flow_deleted.emit(flow)
77 |
78 | def _flow_renamed(self, flow: ryvencore.Flow, new_name: str):
79 | """
80 | Renames the flow view for a renamed flow.
81 | """
82 | self.flow_renamed.emit(flow, new_name)
--------------------------------------------------------------------------------
/ryvencore_qt/src/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/src/__init__.py
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/FlowCommands.py:
--------------------------------------------------------------------------------
1 | """
2 | This file contains the implementations of undoable actions for FlowView.
3 | """
4 |
5 |
6 | from qtpy.QtCore import QObject, QPointF
7 | from qtpy.QtWidgets import QUndoCommand
8 |
9 | from .drawings.DrawingObject import DrawingObject
10 | from .nodes.NodeItem import NodeItem
11 | from typing import Tuple
12 | from ryvencore.NodePort import NodePort, NodeInput, NodeOutput
13 |
14 |
15 | class FlowUndoCommand(QObject, QUndoCommand):
16 | """
17 | The main difference to normal QUndoCommands is the activate feature. This allows the flow widget to add the
18 | undo command to the undo stack before redo() is called. This is important since some of these commands can cause
19 | other commands to be added while they are performing redo(), so to prevent those commands to be added to the
20 | undo stack before the parent command, it is here blocked at first.
21 | """
22 |
23 | def __init__(self, flow_view):
24 |
25 | self.flow_view = flow_view
26 | self.flow = flow_view.flow
27 | self._activated = False
28 |
29 | QObject.__init__(self)
30 | QUndoCommand.__init__(self)
31 |
32 | def activate(self):
33 | self._activated = True
34 | self.redo()
35 |
36 | def redo(self) -> None:
37 | if not self._activated:
38 | return
39 | else:
40 | self.redo_()
41 |
42 | def undo(self) -> None:
43 | self.undo_()
44 |
45 | def redo_(self):
46 | """subclassed"""
47 | pass
48 |
49 | def undo_(self):
50 | """subclassed"""
51 | pass
52 |
53 |
54 | class MoveComponents_Command(FlowUndoCommand):
55 | def __init__(self, flow_view, items_list, p_from, p_to):
56 | super(MoveComponents_Command, self).__init__(flow_view)
57 |
58 | self.items_list = items_list
59 | self.p_from = p_from
60 | self.p_to = p_to
61 | self.last_item_group_pos = p_to
62 |
63 | def undo_(self):
64 | items_group = self.items_group()
65 | items_group.setPos(self.p_from)
66 | self.last_item_group_pos = items_group.pos()
67 | self.destroy_items_group(items_group)
68 |
69 | def redo_(self):
70 | items_group = self.items_group()
71 | items_group.setPos(self.p_to - self.last_item_group_pos)
72 | self.destroy_items_group(items_group)
73 |
74 |
75 | def items_group(self):
76 | return self.flow_view.scene().createItemGroup(self.items_list)
77 |
78 | def destroy_items_group(self, items_group):
79 | self.flow_view.scene().destroyItemGroup(items_group)
80 |
81 |
82 | class PlaceNode_Command(FlowUndoCommand):
83 |
84 | def __init__(self, flow_view, node_class, pos):
85 | super().__init__(flow_view)
86 |
87 | self.node_class = node_class
88 | self.node = None
89 | self.item_pos = pos
90 |
91 | def undo_(self):
92 | self.flow.remove_node(self.node)
93 |
94 | def redo_(self):
95 | if self.node:
96 | self.flow.add_node(self.node)
97 | else:
98 | self.node = self.flow.create_node(self.node_class)
99 |
100 |
101 | class PlaceDrawing_Command(FlowUndoCommand):
102 | def __init__(self, flow_view, posF, drawing):
103 | super().__init__(flow_view)
104 |
105 | self.drawing = drawing
106 | self.drawing_obj_place_pos = posF
107 | self.drawing_obj_pos = self.drawing_obj_place_pos
108 |
109 | def undo_(self):
110 | # The drawing_obj_pos is not anymore the drawing_obj_place_pos because after the
111 | # drawing object was completed, its actual position got recalculated according to all points and differs from
112 | # the initial pen press pos (=drawing_obj_place_pos). See DrawingObject.finished().
113 |
114 | self.drawing_obj_pos = self.drawing.pos()
115 |
116 | self.flow_view.remove_component(self.drawing)
117 |
118 | def redo_(self):
119 | self.flow_view.add_drawing(self.drawing, self.drawing_obj_pos)
120 |
121 |
122 | class RemoveComponents_Command(FlowUndoCommand):
123 |
124 | def __init__(self, flow_view, items):
125 | super().__init__(flow_view)
126 |
127 | self.items = items
128 | self.broken_connections = [] # the connections that go beyond the removed nodes and need to be restored in undo
129 | self.internal_connections = set()
130 |
131 | self.node_items = []
132 | self.nodes = []
133 | self.drawings = []
134 | for i in self.items:
135 | if isinstance(i, NodeItem):
136 | self.node_items.append(i)
137 | self.nodes.append(i.node)
138 | elif isinstance(i, DrawingObject):
139 | self.drawings.append(i)
140 |
141 | for n in self.nodes:
142 | for i in n.inputs:
143 | cp = n.flow.connected_output(i)
144 | if cp is not None:
145 | cn = cp.node
146 | if cn not in self.nodes:
147 | self.broken_connections.append((cp, i))
148 | else:
149 | self.internal_connections.add((cp, i))
150 | for o in n.outputs:
151 | for cp in n.flow.connected_inputs(o):
152 | cn = cp.node
153 | if cn not in self.nodes:
154 | self.broken_connections.append((o, cp))
155 | else:
156 | self.internal_connections.add((o, cp))
157 |
158 | def undo_(self):
159 |
160 | # add nodes
161 | for n in self.nodes:
162 | self.flow.add_node(n)
163 |
164 | # add drawings
165 | for d in self.drawings:
166 | self.flow_view.add_drawing(d)
167 |
168 | # add connections
169 | self.restore_broken_connections()
170 | self.restore_internal_connections()
171 |
172 | def redo_(self):
173 |
174 | # remove connections
175 | self.remove_broken_connections()
176 | self.remove_internal_connections()
177 |
178 | # remove nodes
179 | for n in self.nodes:
180 | self.flow.remove_node(n)
181 |
182 | # remove drawings
183 | for d in self.drawings:
184 | self.flow_view.remove_drawing(d)
185 |
186 | def restore_internal_connections(self):
187 | for c in self.internal_connections:
188 | self.flow.add_connection(c)
189 |
190 | def remove_internal_connections(self):
191 | for c in self.internal_connections:
192 | self.flow.remove_connection(c)
193 |
194 | def restore_broken_connections(self):
195 | for c in self.broken_connections:
196 | self.flow.add_connection(c)
197 |
198 | def remove_broken_connections(self):
199 | for c in self.broken_connections:
200 | self.flow.remove_connection(c)
201 |
202 |
203 | class ConnectPorts_Command(FlowUndoCommand):
204 |
205 | def __init__(self, flow_view, out, inp):
206 | super().__init__(flow_view)
207 |
208 | # CAN ALSO LEAD TO DISCONNECT INSTEAD OF CONNECT!!
209 |
210 | self.out = out
211 | self.inp = inp
212 | self.connection = None
213 | self.connecting = True
214 |
215 | for i in flow_view.flow.connected_inputs(out):
216 | if i == self.inp:
217 | self.connection = (out, i)
218 | self.connecting = False
219 |
220 |
221 | def undo_(self):
222 | if self.connecting:
223 | # remove connection
224 | self.flow.remove_connection(self.connection)
225 | else:
226 | # recreate former connection
227 | self.flow.add_connection(self.connection)
228 |
229 | def redo_(self):
230 | if self.connecting:
231 | if self.connection:
232 | self.flow.add_connection(self.connection)
233 | else:
234 | # connection hasn't been created yet
235 | self.connection = self.flow.connect_nodes(self.out, self.inp)
236 | else:
237 | # remove existing connection
238 | self.flow.remove_connection(self.connection)
239 |
240 |
241 |
242 |
243 | class Paste_Command(FlowUndoCommand):
244 |
245 | def __init__(self, flow_view, data, offset_for_middle_pos):
246 | super().__init__(flow_view)
247 |
248 | self.data = data
249 | self.modify_data_positions(offset_for_middle_pos)
250 | self.pasted_components = None
251 |
252 |
253 | def modify_data_positions(self, offset):
254 | """adds the offset to the components' positions in data"""
255 |
256 | for node in self.data['nodes']:
257 | node['pos x'] = node['pos x'] + offset.x()
258 | node['pos y'] = node['pos y'] + offset.y()
259 | for drawing in self.data['drawings']:
260 | drawing['pos x'] = drawing['pos x'] + offset.x()
261 | drawing['pos y'] = drawing['pos y'] + offset.y()
262 |
263 | def redo_(self):
264 | if self.pasted_components is None:
265 | self.pasted_components = {}
266 |
267 | # create components
268 | self.create_drawings()
269 |
270 | self.pasted_components['nodes'], self.pasted_components['connections'] = \
271 | self.flow.load_components(
272 | nodes_data=self.data['nodes'],
273 | conns_data=self.data['connections'],
274 | output_data=self.data['output data'],
275 | )
276 |
277 | self.select_new_components_in_view()
278 | else:
279 | self.add_existing_components()
280 |
281 | def undo_(self):
282 | # remove components and their items from flow
283 | for c in self.pasted_components['connections']:
284 | self.flow.remove_connection(c)
285 | for n in self.pasted_components['nodes']:
286 | self.flow.remove_node(n)
287 | for d in self.pasted_components['drawings']:
288 | self.flow_view.remove_drawing(d)
289 |
290 | def add_existing_components(self):
291 | # add existing components and items to flow
292 | for n in self.pasted_components['nodes']:
293 | self.flow.add_node(n)
294 | for c in self.pasted_components['connections']:
295 | self.flow.add_connection(c)
296 | for d in self.pasted_components['drawings']:
297 | self.flow_view.add_drawing(d)
298 |
299 | self.select_new_components_in_view()
300 |
301 | def select_new_components_in_view(self):
302 | self.flow_view.clear_selection()
303 | for d in self.pasted_components['drawings']:
304 | d: DrawingObject
305 | d.setSelected(True)
306 | for n in self.pasted_components['nodes']:
307 | n: NodeItem
308 | ni: NodeItem = self.flow_view.node_items[n]
309 | ni.setSelected(True)
310 |
311 | def create_drawings(self):
312 | drawings = []
313 | for d in self.data['drawings']:
314 | new_drawing = self.flow_view.create_drawing(d)
315 | self.flow_view.add_drawing(new_drawing, posF=QPointF(d['pos x'], d['pos y']))
316 | drawings.append(new_drawing)
317 | self.pasted_components['drawings'] = drawings
318 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/FlowViewProxyWidget.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtWidgets import QGraphicsProxyWidget
2 |
3 |
4 | class FlowViewProxyWidget(QGraphicsProxyWidget):
5 | """Ensures easy controls event handling for QProxyWidgets in the flow."""
6 |
7 | def __init__(self, flow_view, parent=None):
8 | super(FlowViewProxyWidget, self).__init__(parent)
9 |
10 | self.flow_view = flow_view
11 |
12 |
13 | def mousePressEvent(self, arg__1):
14 | QGraphicsProxyWidget.mousePressEvent(self, arg__1)
15 | if arg__1.isAccepted():
16 | self.flow_view.mouse_event_taken = True
17 |
18 | def mouseReleaseEvent(self, arg__1):
19 | self.flow_view.mouse_event_taken = True
20 | QGraphicsProxyWidget.mouseReleaseEvent(self, arg__1)
21 |
22 | def wheelEvent(self, event):
23 | QGraphicsProxyWidget.wheelEvent(self, event)
24 |
25 | def keyPressEvent(self, arg__1):
26 | QGraphicsProxyWidget.keyPressEvent(self, arg__1)
27 | if arg__1.isAccepted():
28 | self.flow_view.ignore_key_event = True
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/FlowViewStylusModesWidget.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtWidgets import QWidget, QPushButton, QHBoxLayout, QSlider, QColorDialog
2 | from qtpy.QtCore import Qt
3 | from qtpy.QtGui import QColor
4 |
5 |
6 | class FlowViewStylusModesWidget(QWidget):
7 | def __init__(self, flow_view):
8 | super(FlowViewStylusModesWidget, self).__init__()
9 |
10 | self.setObjectName('FlowViewStylusModesWidget')
11 |
12 | # GENERAL ATTRIBUTES
13 | self.flow_view = flow_view
14 | self.pen_color = QColor(255, 255, 0)
15 | self.stylus_buttons_visible = True
16 |
17 | # stylus button
18 | self.stylus_button = QPushButton('stylus') # show/hide
19 | self.stylus_button.clicked.connect(self.on_stylus_button_clicked)
20 |
21 | # mode
22 | self.set_stylus_mode_comment_button = QPushButton('comment')
23 | self.set_stylus_mode_comment_button.clicked.connect(self.on_comment_button_clicked)
24 | self.set_stylus_mode_edit_button = QPushButton('edit')
25 | self.set_stylus_mode_edit_button.clicked.connect(self.on_edit_button_clicked)
26 |
27 | # pen style
28 | self.pen_color_button = QPushButton('color')
29 | self.pen_color_button.clicked.connect(self.on_choose_color_clicked)
30 | self.pen_width_slider = QSlider(Qt.Horizontal)
31 | self.pen_width_slider.setRange(1, 100)
32 | self.pen_width_slider.setValue(20)
33 |
34 |
35 | # MAIN LAYOUT
36 | main_horizontal_layout = QHBoxLayout()
37 |
38 | main_horizontal_layout.addWidget(self.pen_color_button)
39 | main_horizontal_layout.addWidget(self.pen_width_slider)
40 | main_horizontal_layout.addWidget(self.set_stylus_mode_comment_button)
41 | main_horizontal_layout.addWidget(self.set_stylus_mode_edit_button)
42 | main_horizontal_layout.addWidget(self.stylus_button)
43 |
44 | self.setLayout(main_horizontal_layout)
45 |
46 | self.setStyleSheet('''
47 | QWidget#FlowViewStylusModesWidget {
48 | background: transparent;
49 | }
50 | ''')
51 |
52 | self.hide_stylus_buttons()
53 | self.hide_pen_style_widgets()
54 |
55 | def pen_width(self):
56 | return self.pen_width_slider.value()/20
57 |
58 | def hide_stylus_buttons(self):
59 | self.set_stylus_mode_edit_button.hide()
60 | self.set_stylus_mode_comment_button.hide()
61 | self.stylus_buttons_visible = False
62 |
63 | def show_stylus_buttons(self):
64 | self.set_stylus_mode_edit_button.show()
65 | self.set_stylus_mode_comment_button.show()
66 | self.stylus_buttons_visible = True
67 |
68 | def hide_pen_style_widgets(self):
69 | self.pen_color_button.hide()
70 | self.pen_width_slider.hide()
71 |
72 | def show_pen_style_widgets(self):
73 | self.pen_color_button.show()
74 | self.pen_width_slider.show()
75 |
76 | def on_stylus_button_clicked(self):
77 | if self.stylus_buttons_visible:
78 | self.hide_pen_style_widgets()
79 | self.hide_stylus_buttons()
80 | else:
81 | self.show_stylus_buttons()
82 |
83 | self.adjustSize()
84 | self.flow_view.set_stylus_proxy_pos()
85 |
86 | def on_edit_button_clicked(self):
87 | self.flow_view.stylus_mode = 'edit'
88 | # self.pen_style_widget.hide()
89 | self.hide_pen_style_widgets()
90 |
91 | # if I don't hide and show the settings_widget manually here, the stylus mode buttons take up the additional
92 | # space when clicking on comment and then edit. self.adjustSize() does not seem to work properly here...
93 | self.hide_stylus_buttons()
94 | self.show_stylus_buttons()
95 | # self.settings_widget.hide()
96 | # self.settings_widget.show()
97 |
98 | self.adjustSize()
99 | self.flow_view.set_stylus_proxy_pos()
100 | # self.flow.setDragMode(QGraphicsView.RubberBandDrag)
101 |
102 | def on_comment_button_clicked(self):
103 | self.flow_view.stylus_mode = 'comment'
104 | # self.pen_style_widget.show()
105 | self.show_pen_style_widgets()
106 | self.adjustSize()
107 | self.flow_view.set_stylus_proxy_pos()
108 | # self.flow.setDragMode(QGraphicsView.NoDrag)
109 |
110 | def on_choose_color_clicked(self):
111 | self.pen_color = QColorDialog.getColor(self.pen_color, options=QColorDialog.ShowAlphaChannel,
112 | title='Choose pen color')
113 | self.update_color_button_SS()
114 |
115 |
116 | def update_color_button_SS(self):
117 |
118 | self.pen_color_button.setStyleSheet(
119 | '''
120 | QPushButton {
121 | background-color: '''+self.pen_color.name()+''';
122 | }'''
123 | )
124 |
125 | def get_pen_settings(self):
126 | return {'color': self.pen_color.name(),
127 | 'base stroke weight': self.pen_width_slider.value()/10}
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/FlowViewZoomWidget.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtWidgets import QWidget, QPushButton, QHBoxLayout
2 |
3 |
4 | class FlowViewZoomWidget(QWidget):
5 | def __init__(self, flow_view):
6 | super(FlowViewZoomWidget, self).__init__()
7 |
8 | self.flow_view = flow_view
9 |
10 | self.setObjectName('FlowViewZoomWidget')
11 |
12 | self.zoom_in_button = QPushButton('+')
13 | self.zoom_in_button.clicked.connect(self.on_zoom_in_button_clicked)
14 | self.zoom_out_button = QPushButton('-')
15 | self.zoom_out_button.clicked.connect(self.on_zoom_out_button_clicked)
16 |
17 | main_horizontal_layout = QHBoxLayout()
18 |
19 | main_horizontal_layout.addWidget(self.zoom_out_button)
20 | main_horizontal_layout.addWidget(self.zoom_in_button)
21 | self.setLayout(main_horizontal_layout)
22 |
23 | self.setStyleSheet('''
24 | QWidget#FlowViewZoomWidget {
25 | background: transparent;
26 | }
27 | ''')
28 |
29 |
30 | def on_zoom_in_button_clicked(self):
31 | self.flow_view.zoom_in(250)
32 |
33 | def on_zoom_out_button_clicked(self):
34 | self.flow_view.zoom_out(250)
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/README.md:
--------------------------------------------------------------------------------
1 | # GUI for flows
2 |
3 | The frontend for flows mainly consists of the classes
4 |
5 | - `FlowView` - representing `ryvencore.Flow`
6 | - `NodeItem` - representing `ryvencore.Node`
7 | - `ConnectionItem` - representing `ryvencore.Connection`
8 |
9 | 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.
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/src/flows/__init__.py
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/connections/ConnectionItem.py:
--------------------------------------------------------------------------------
1 | # import math
2 | from qtpy.QtCore import QMarginsF
3 | from qtpy.QtCore import QRectF, QPointF, Qt
4 | from qtpy.QtGui import QPainter, QColor, QRadialGradient, QPainterPath, QPen
5 | from qtpy.QtWidgets import QGraphicsPathItem, QGraphicsItem, QStyleOptionGraphicsItem
6 |
7 | from ...GUIBase import GUIBase
8 | from ...utils import sqrt
9 | from ...utils import pythagoras
10 |
11 |
12 | class ConnectionItem(GUIBase, QGraphicsPathItem):
13 | """The GUI representative for a connection. The classes ExecConnectionItem and DataConnectionItem will be ready
14 | for reimplementation later, so users can add GUI for the enhancements of DataConnection and ExecConnection,
15 | like input fields for weights."""
16 |
17 | def __init__(self, connection, session_design):
18 | #GUIBase.__init__(self, representing_component=connection) # ConnectionItem doesn't have a representing component
19 | QGraphicsPathItem.__init__(self)
20 |
21 | self.setAcceptHoverEvents(True)
22 |
23 | self.connection = connection
24 | out, inp = self.connection
25 |
26 | out_port_index = out.node.outputs.index(out)
27 | inp_port_index = inp.node.inputs.index(inp)
28 | self.out_item = out.node.gui.item.outputs[out_port_index]
29 | self.inp_item = inp.node.gui.item.inputs[inp_port_index]
30 |
31 | self.session_design = session_design
32 | self.session_design.flow_theme_changed.connect(self.recompute)
33 | self.session_design.performance_mode_changed.connect(self.recompute)
34 |
35 | # for rendering flow pictures
36 | self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
37 |
38 | self.recompute()
39 |
40 | def recompute(self):
41 | """Updates scene position and recomputes path, pen and gradient"""
42 |
43 | # position
44 | self.setPos(self.out_pos())
45 |
46 | # path
47 | self.setPath(
48 | self.connection_path(
49 | QPointF(0, 0),
50 | self.inp_pos()-self.scenePos()
51 | )
52 | )
53 |
54 | # pen
55 | pen = self.get_pen()
56 |
57 | # brush
58 | self.setBrush(Qt.NoBrush)
59 |
60 | # gradient
61 | if self.session_design.performance_mode == 'pretty':
62 | c = pen.color()
63 | w = self.path().boundingRect().width()
64 | h = self.path().boundingRect().height()
65 | gradient = QRadialGradient(
66 | self.boundingRect().center(),
67 | pythagoras(w, h) / 2
68 | )
69 |
70 | c_r = c.red()
71 | c_g = c.green()
72 | c_b = c.blue()
73 |
74 | # this offset will be 1 if inp.x >> out.x and 0 if inp.x < out.x
75 | # hence, no fade for the gradient if the connection goes backwards
76 | offset_mult: float = max(
77 | 0,
78 | min(
79 | (self.inp_pos().x() - self.out_pos().x()) / 200,
80 | 1
81 | )
82 | )
83 |
84 | # and if the input is very far away from the output, decrease the gradient fade so the connection
85 | # doesn't fully disappear at the ends and stays visible
86 | if self.inp_pos().x() > self.out_pos().x():
87 | offset_mult = min(
88 | offset_mult,
89 | 2000 / (self.dist(self.inp_pos(), self.out_pos()))
90 | )
91 | # zucker.
92 |
93 | gradient.setColorAt(0.0, QColor(c_r, c_g, c_b, 255))
94 | gradient.setColorAt(0.75, QColor(c_r, c_g, c_b, 255 - round(55 * offset_mult)))
95 | gradient.setColorAt(0.95, QColor(c_r, c_g, c_b, 255 - round(255 * offset_mult)))
96 |
97 | pen.setBrush(gradient)
98 |
99 | self.setPen(pen)
100 |
101 | def out_pos(self) -> QPointF:
102 | """The current global scene position of the pin of the output port"""
103 |
104 | return self.out_item.pin.get_scene_center_pos()
105 |
106 | def inp_pos(self) -> QPointF:
107 | """The current global scene position of the pin of the input port"""
108 |
109 | return self.inp_item.pin.get_scene_center_pos()
110 |
111 | def set_highlighted(self, b: bool):
112 | pen: QPen = self.pen()
113 |
114 | if b:
115 | pen.setWidthF(self.pen_width() * 2)
116 | else:
117 | pen.setWidthF(self.pen_width())
118 | self.recompute()
119 |
120 | self.setPen(pen)
121 |
122 | def get_pen(self) -> QPen:
123 | pass
124 |
125 | def pen_width(self) -> int:
126 | pass
127 |
128 | def flow_theme(self):
129 | return self.session_design.flow_theme
130 |
131 | def hoverEnterEvent(self, event):
132 | self.set_highlighted(True)
133 | super().hoverEnterEvent(event)
134 |
135 | def hoverLeaveEvent(self, event):
136 | self.set_highlighted(False)
137 | super().hoverLeaveEvent(event)
138 |
139 | @staticmethod
140 | def dist(p1: QPointF, p2: QPointF) -> float:
141 | """Returns the diagonal distance between the points using pythagoras"""
142 |
143 | dx = p2.x()-p1.x()
144 | dy = p2.y()-p1.y()
145 | return sqrt((dx**2) + (dy**2))
146 |
147 |
148 | @staticmethod
149 | def connection_path(p1: QPointF, p2: QPointF) -> QPainterPath:
150 | """Returns the painter path for drawing the connection, using the usual cubic connection path by default"""
151 |
152 | return default_cubic_connection_path(p1, p2)
153 |
154 |
155 | class ExecConnectionItem(ConnectionItem):
156 |
157 | def pen_width(self):
158 | return self.flow_theme().exec_conn_width
159 |
160 | def get_pen(self):
161 | theme = self.flow_theme()
162 | pen = QPen(theme.exec_conn_color, theme.exec_conn_width)
163 | pen.setStyle(theme.exec_conn_pen_style)
164 | pen.setCapStyle(Qt.RoundCap)
165 | return pen
166 |
167 |
168 | class DataConnectionItem(ConnectionItem):
169 |
170 | def pen_width(self):
171 | return self.flow_theme().data_conn_width
172 |
173 | def get_pen(self):
174 | theme = self.flow_theme()
175 | pen = QPen(theme.data_conn_color, theme.data_conn_width)
176 | pen.setStyle(theme.data_conn_pen_style)
177 | pen.setCapStyle(Qt.RoundCap)
178 | return pen
179 |
180 |
181 | def default_cubic_connection_path(p1: QPointF, p2: QPointF):
182 | """Returns the nice looking QPainterPath from p1 to p2"""
183 |
184 | path = QPainterPath()
185 |
186 | path.moveTo(p1)
187 |
188 | dx = p2.x() - p1.x()
189 | adx = abs(dx)
190 | dy = p2.y() - p1.y()
191 | ady = abs(dy)
192 | distance = sqrt((dx ** 2) + (dy ** 2))
193 | x1, y1 = p1.x(), p1.y()
194 | x2, y2 = p2.x(), p2.y()
195 |
196 | if ((x1 < x2 - 30) or distance < 100) and (x1 < x2):
197 | # STANDARD FORWARD
198 | path.cubicTo(x1 + ((x2 - x1) / 2), y1,
199 | x1 + ((x2 - x1) / 2), y2,
200 | x2, y2)
201 | elif x2 < x1 - 100 and adx > ady * 2:
202 | # STRONG BACKWARDS
203 | path.cubicTo(x1 + 100 + (x1 - x2) / 10, y1,
204 | x1 + 100 + (x1 - x2) / 10, y1 + (dy / 2),
205 | x1 + (dx / 2), y1 + (dy / 2))
206 | path.cubicTo(x2 - 100 - (x1 - x2) / 10, y2 - (dy / 2),
207 | x2 - 100 - (x1 - x2) / 10, y2,
208 | x2, y2)
209 | else:
210 | # STANDARD BACKWARDS
211 | path.cubicTo(x1 + 100 + (x1 - x2) / 3, y1,
212 | x2 - 100 - (x1 - x2) / 3, y2,
213 | x2, y2)
214 |
215 | return path
216 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/connections/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/src/flows/connections/__init__.py
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/drawings/DrawingObject.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtWidgets import QGraphicsItem
2 | from qtpy.QtGui import QPen, QPainter, QColor, QPainterPath
3 | from qtpy.QtCore import Qt, QRectF, QPointF, QLineF
4 |
5 | from ...utils import MovementEnum
6 |
7 |
8 | class DrawingObject(QGraphicsItem):
9 | """GUI implementation for 'drawing objects' in the scene, written by hand using a stylus pen"""
10 |
11 | def __init__(self, flow_view, load_data=None):
12 | super(DrawingObject, self).__init__()
13 |
14 | self.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable |
15 | QGraphicsItem.ItemSendsScenePositionChanges)
16 | self.setAcceptHoverEvents(True)
17 | self.setCacheMode(QGraphicsItem.DeviceCoordinateCache) # for rendering flow pictures
18 |
19 | self.flow_view = flow_view
20 | self.color = None
21 | self.base_stroke_weight = None
22 | self.type = 'pen' # so far the only available, but I already save it so I could add more types in the future
23 | self.points = []
24 | self.stroke_weights = []
25 | self.pen_stroke_weight = 0 # approx. avg of self.stroke_weights
26 | self.rect = None
27 | self.path: QPainterPath = None
28 | self.width = -1
29 | self.height = -1
30 | self.finished = False
31 |
32 | # viewport_pos enables global floating points for precise pen positions
33 | self.viewport_pos: QPointF = load_data['viewport pos'] if 'viewport pos' in load_data else None
34 | # if the drawing gets loaded, its correct global floating pos is already correct (gets set by flow then)
35 |
36 | self.movement_state = None # ugly - should get replaced later, see NodeItem, same issue
37 | self.movement_pos_from = None
38 |
39 | if 'points' in load_data:
40 | p_c = load_data['points']
41 | for p in p_c:
42 | if type(p) == list:
43 | x = p[0]
44 | y = p[1]
45 | w = p[2]
46 | self.points.append(QPointF(x, y))
47 | self.stroke_weights.append(w)
48 | elif type(p) == dict: # backwards compatibility
49 | x = p['x']
50 | y = p['y']
51 | w = p['w']
52 | self.points.append(QPointF(x, y))
53 | self.stroke_weights.append(w)
54 | self.finished = True
55 |
56 | self.color = QColor(load_data['color'])
57 | self.base_stroke_weight = load_data['base stroke weight']
58 |
59 |
60 | def paint(self, painter, option, widget=None):
61 |
62 | if not self.finished:
63 | for i in range(1, len(self.points)):
64 | pen = QPen()
65 | pen.setColor(self.color)
66 | pen_width = (self.stroke_weights[i] + 0.2) * self.base_stroke_weight
67 | pen.setWidthF(pen_width)
68 | if i == 1 or i == len(self.points) - 1:
69 | pen.setCapStyle(Qt.RoundCap)
70 | painter.setPen(pen)
71 | painter.setRenderHint(QPainter.Antialiasing)
72 | painter.setRenderHint(QPainter.HighQualityAntialiasing)
73 | painter.drawLine(self.points[i - 1], self.points[i])
74 | return
75 |
76 | if not self.path and self.finished:
77 | if len(self.points) == 0:
78 | return
79 |
80 | self.path = QPainterPath()
81 | self.path.moveTo(self.points[0])
82 | avg_weight = self.stroke_weights[0]
83 | for i in range(1, len(self.points)):
84 | self.path.lineTo(self.points[i])
85 | avg_weight += self.stroke_weights[i]
86 | self.pen_stroke_weight = (avg_weight/len(self.points) + 0.2)*self.base_stroke_weight
87 |
88 | pen = QPen()
89 | pen.setColor(self.color)
90 | pen.setWidthF(self.pen_stroke_weight)
91 | painter.setPen(pen)
92 | painter.setRenderHint(QPainter.Antialiasing)
93 | # painter.setRenderHint(QPainter.HighQualityAntialiasing)
94 | painter.drawPath(self.path)
95 |
96 | def append_point(self, posF_in_view: QPointF) -> bool:
97 | """
98 | Only used for active drawing.
99 | Appends a point (floating, in viewport coordinates) only if the distance to the last one isn't too small
100 | """
101 |
102 | p: QPointF = (self.viewport_pos + posF_in_view) - self.pos()
103 | p.setX(round(p.x(), 2))
104 | p.setY(round(p.y(), 2))
105 |
106 | if len(self.points) > 0:
107 | line = QLineF(self.points[-1], p)
108 | if line.length() < 0.5:
109 | return False
110 |
111 | self.points.append(p)
112 | return True
113 |
114 | def finish(self):
115 | """
116 | Computes the correct center position and updates the relative position for all points.
117 | """
118 |
119 | # Correct bounding rect (so far (0,0) is at the start of the line, but it should be in the middle)
120 |
121 | rect_center = self.get_points_rect_center()
122 | for p in self.points:
123 | p.setX(p.x()-rect_center.x())
124 | p.setY(p.y()-rect_center.y())
125 | self.setPos(self.pos()+rect_center)
126 |
127 | self.rect = self.get_points_rect()
128 |
129 | self.finished = True
130 |
131 | def get_points_rect(self):
132 | """Computes the 'bounding rect' for all points"""
133 |
134 | if len(self.points) == 0:
135 | return QRectF(0, 0, 0, 0)
136 | x_coords = [p.x() for p in self.points]
137 | y_coords = [p.y() for p in self.points]
138 | left = min(x_coords)
139 | right = max(x_coords)
140 | up = min(y_coords)
141 | down = max(y_coords)
142 |
143 | rect = QRectF(left, up, right - left, down - up)
144 |
145 | self.width = rect.width()
146 | self.height = rect.height()
147 | return rect
148 |
149 | def get_points_rect_center(self):
150 | """Returns the center point for the 'bounding rect' for all points"""
151 |
152 | return self.get_points_rect().center()
153 |
154 | def boundingRect(self):
155 | if self.rect:
156 | return self.rect
157 | else:
158 | return self.get_points_rect()
159 |
160 | def itemChange(self, change, value):
161 | if change == QGraphicsItem.ItemPositionChange:
162 | self.flow_view.viewport().update()
163 | if self.movement_state == MovementEnum.mouse_clicked:
164 | self.movement_state = MovementEnum.position_changed
165 |
166 | return QGraphicsItem.itemChange(self, change, value)
167 |
168 | def mousePressEvent(self, event):
169 | """Used for Moving-Commands in Flow - may be replaced later with a nicer determination of a move action."""
170 |
171 | self.movement_state = MovementEnum.mouse_clicked
172 | self.movement_pos_from = self.pos()
173 | return QGraphicsItem.mousePressEvent(self, event)
174 |
175 | def mouseReleaseEvent(self, event):
176 | """Used for Moving-Commands in Flow - may be replaced later with a nicer determination of a move action."""
177 |
178 | if self.movement_state == MovementEnum.position_changed:
179 | self.flow_view.selected_components_moved(self.pos() - self.movement_pos_from)
180 | self.movement_state = None
181 | return QGraphicsItem.mouseReleaseEvent(self, event)
182 |
183 | def data_(self):
184 | drawing_dict = {
185 | 'pos x': self.pos().x(),
186 | 'pos y': self.pos().y(),
187 | 'color': self.color.name(),
188 | 'type': self.type,
189 | 'base stroke weight': self.base_stroke_weight
190 | }
191 | points_list = []
192 | for i in range(len(self.points)):
193 | p = self.points[i]
194 | points_list.append([p.x(), p.y(), self.stroke_weights[i]])
195 | drawing_dict['points'] = points_list
196 | return drawing_dict
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 | # ALTERNATIVE QGRAPHICSPATHITEM-BASED IMPLEMENTATION:
211 |
212 | # ...
213 | # def paint(self, painter, option, widget=None):
214 | #
215 | # self.setBrush(Qt.NoBrush)
216 | #
217 | # if self.finished:
218 | # super().paint(painter, option,widget)
219 | # else:
220 | # if len(self.stroke_weights) == 0:
221 | # return
222 | #
223 | # # pen
224 | # pen = QPen(self.color)
225 | # pen.setWidthF(self.stroke_weights[-1])
226 | # pen.setCapStyle(Qt.RoundCap)
227 | # self.setPen(pen)
228 | #
229 | # # path
230 | # path = QPainterPath(self.points[0])
231 | # for i, p in enumerate(self.points, start=1):
232 | # path.lineTo(p)
233 | # self.setPath(path)
234 | # self.path_generated = True
235 | #
236 | # super().paint(painter, option, widget)
237 | #
238 | # ...
239 | #
240 | # def finish(self):
241 | #
242 | # self.finished = True
243 | # self.update()
244 |
245 | # it is a bit worse in appearance but might be a lot faster
246 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/drawings/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/src/flows/drawings/__init__.py
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/node_list_widget/NodeListWidget.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtWidgets import QWidget, QVBoxLayout, QLineEdit, QScrollArea
2 | from qtpy.QtCore import Qt, Signal
3 |
4 | from ryvencore import Node
5 | from .utils import search, sort_nodes, inc, dec
6 | from ..node_list_widget.NodeWidget import NodeWidget
7 |
8 | from statistics import median
9 |
10 |
11 | class NodeListWidget(QWidget):
12 |
13 | # SIGNALS
14 | escaped = Signal()
15 | node_chosen = Signal(object)
16 |
17 | def __init__(self, session):
18 | super().__init__()
19 |
20 | self.session = session
21 | self.nodes: list[type[Node]] = []
22 |
23 | self.current_nodes = [] # currently selectable nodes
24 | self.active_node_widget_index = -1 # index of focused node widget
25 | self.active_node_widget = None # focused node widget
26 | self.node_widgets = {} # Node-NodeWidget assignments
27 | self._node_widget_index_counter = 0
28 |
29 | self._setup_UI()
30 |
31 |
32 | def _setup_UI(self):
33 |
34 | self.main_layout = QVBoxLayout(self)
35 | self.main_layout.setAlignment(Qt.AlignTop)
36 | self.setLayout(self.main_layout)
37 |
38 | # adding all stuff to the layout
39 | self.search_line_edit = QLineEdit(self)
40 | self.search_line_edit.setPlaceholderText('search for node...')
41 | self.search_line_edit.textChanged.connect(self._update_view)
42 | self.layout().addWidget(self.search_line_edit)
43 |
44 |
45 | self.list_scroll_area = QScrollArea(self)
46 | self.list_scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
47 | self.list_scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
48 | self.list_scroll_area.setWidgetResizable(True)
49 | self.list_scroll_area.setContentsMargins(0, 0, 0, 0)
50 |
51 | self.list_scroll_area_widget = QWidget()
52 | self.list_scroll_area_widget.setContentsMargins(0, 0, 0, 0)
53 | self.list_scroll_area.setWidget(self.list_scroll_area_widget)
54 |
55 | self.list_layout = QVBoxLayout()
56 | self.list_layout.setContentsMargins(0, 0, 0, 0)
57 | self.list_layout.setAlignment(Qt.AlignTop)
58 | self.list_scroll_area_widget.setLayout(self.list_layout)
59 |
60 | self.layout().addWidget(self.list_scroll_area)
61 |
62 | self._update_view('')
63 |
64 | self.setStyleSheet(self.session.design.node_selection_stylesheet)
65 |
66 | self.search_line_edit.setFocus()
67 |
68 |
69 | def mousePressEvent(self, event):
70 | # need to accept the event, so the scene doesn't process it further
71 | QWidget.mousePressEvent(self, event)
72 | event.accept()
73 |
74 |
75 | def keyPressEvent(self, event):
76 | """key controls"""
77 |
78 | num_items = len(self.current_nodes)
79 |
80 | if event.key() == Qt.Key_Escape:
81 | self.escaped.emit()
82 |
83 | elif event.key() == Qt.Key_Down:
84 | self._set_active_node_widget_index(
85 | inc(self.active_node_widget_index, length=num_items)
86 | )
87 | elif event.key() == Qt.Key_Up:
88 | self._set_active_node_widget_index(
89 | dec(self.active_node_widget_index, num_items)
90 | )
91 |
92 | elif event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter:
93 | if len(self.current_nodes) > 0:
94 | self._place_node(self.active_node_widget_index)
95 | else:
96 | event.setAccepted(False)
97 |
98 |
99 | def wheelEvent(self, event):
100 | # need to accept the event, so the scene doesn't process it further
101 | QWidget.wheelEvent(self, event)
102 | event.accept()
103 |
104 |
105 | def refocus(self):
106 | """focuses the search line edit and selects the text"""
107 | self.search_line_edit.setFocus()
108 | self.search_line_edit.selectAll()
109 |
110 |
111 | def update_list(self, nodes):
112 | """update the list of available nodes"""
113 | self.nodes = sort_nodes(nodes)
114 | self._update_view('')
115 |
116 |
117 | def _update_view(self, search_text=''):
118 | if len(self.nodes) == 0:
119 | return
120 |
121 | search_text = search_text.lower()
122 |
123 | # remove all node widgets
124 |
125 | for i in reversed(range(self.list_layout.count())):
126 | self.list_layout.itemAt(i).widget().setParent(None)
127 |
128 | self.current_nodes.clear()
129 |
130 | self._node_widget_index_counter = 0
131 |
132 | # search
133 | sorted_distances = search(
134 | items={
135 | n: [n.title.lower()] + n.tags
136 | for n in self.nodes
137 | },
138 | text=search_text
139 | )
140 |
141 | # create node widgets
142 | cutoff = median(sorted_distances.values())
143 | for n, dist in sorted_distances.items():
144 | if search_text != '' and dist > cutoff:
145 | continue
146 |
147 | self.current_nodes.append(n)
148 |
149 | if self.node_widgets.get(n) is None:
150 | self.node_widgets[n] = self._create_node_widget(n)
151 |
152 | self.list_layout.addWidget(self.node_widgets[n])
153 |
154 | # focus on first result
155 | if len(self.current_nodes) > 0:
156 | self._set_active_node_widget_index(0)
157 |
158 |
159 | def _create_node_widget(self, node):
160 | node_widget = NodeWidget(self, node)
161 | node_widget.custom_focused_from_inside.connect(self._node_widget_focused_from_inside)
162 | node_widget.setObjectName('node_widget_' + str(self._node_widget_index_counter))
163 | self._node_widget_index_counter += 1
164 | node_widget.chosen.connect(self._node_widget_chosen)
165 |
166 | return node_widget
167 |
168 | def _node_widget_focused_from_inside(self):
169 | index = self.list_layout.indexOf(self.sender())
170 | self._set_active_node_widget_index(index)
171 |
172 | def _set_active_node_widget_index(self, index):
173 | self.active_node_widget_index = index
174 | node_widget = self.list_layout.itemAt(index).widget()
175 |
176 | if self.active_node_widget:
177 | self.active_node_widget.set_custom_focus(False)
178 |
179 | node_widget.set_custom_focus(True)
180 | self.active_node_widget = node_widget
181 | self.list_scroll_area.ensureWidgetVisible(self.active_node_widget)
182 |
183 |
184 | def _node_widget_chosen(self):
185 | index = int(self.sender().objectName()[self.sender().objectName().rindex('_')+1:])
186 | self._place_node(index)
187 |
188 |
189 | def _place_node(self, index):
190 | node_index = index
191 | node = self.current_nodes[node_index]
192 | self.node_chosen.emit(node)
193 | self.escaped.emit()
194 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/node_list_widget/NodeWidget.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from qtpy.QtWidgets import QLineEdit, QWidget, QLabel, QGridLayout, QHBoxLayout, QSpacerItem, QSizePolicy, QStyleOption, QStyle
4 | from qtpy.QtGui import QFont, QPainter, QColor, QDrag
5 | from qtpy.QtCore import Signal, Qt, QMimeData
6 |
7 |
8 | class NodeWidget(QWidget):
9 |
10 | chosen = Signal()
11 | custom_focused_from_inside = Signal()
12 |
13 | def __init__(self, parent, node):
14 | super(NodeWidget, self).__init__(parent)
15 |
16 | self.custom_focused = False
17 | self.node = node
18 |
19 | self.left_mouse_pressed_on_me = False
20 |
21 | # UI
22 | main_layout = QGridLayout()
23 | main_layout.setContentsMargins(0, 0, 0, 0)
24 |
25 | self_ = self
26 | class NameLabel(QLineEdit):
27 | def __init__(self, text):
28 | super().__init__(text)
29 |
30 | self.setReadOnly(True)
31 | self.setFont(QFont('Source Code Pro', 8))
32 | def mouseMoveEvent(self, ev):
33 | self_.custom_focused_from_inside.emit()
34 | ev.ignore()
35 | def mousePressEvent(self, ev):
36 | ev.ignore()
37 | def mouseReleaseEvent(self, ev):
38 | ev.ignore()
39 |
40 | name_label = NameLabel(node.title)
41 |
42 | type_layout = QHBoxLayout()
43 |
44 | #type_label = QLabel(node.type_)
45 | #type_label.setFont(QFont('Segoe UI', 8, italic=True))
46 | # type_label.setStyleSheet('color: white;')
47 |
48 | main_layout.addWidget(name_label, 0, 0)
49 | #main_layout.addWidget(type_label, 0, 1)
50 |
51 | self.setLayout(main_layout)
52 | self.setContentsMargins(0, 0, 0, 0)
53 | self.setMaximumWidth(250)
54 |
55 | self.setToolTip(node.__doc__)
56 | self.update_stylesheet()
57 |
58 |
59 | def mousePressEvent(self, event):
60 | self.custom_focused_from_inside.emit()
61 | if event.button() == Qt.LeftButton:
62 | self.left_mouse_pressed_on_me = True
63 |
64 | def mouseMoveEvent(self, event):
65 | if self.left_mouse_pressed_on_me:
66 | drag = QDrag(self)
67 | mime_data = QMimeData()
68 | mime_data.setData('application/json', bytes(json.dumps(
69 | {
70 | 'type': 'node',
71 | 'node identifier': self.node.identifier,
72 | }
73 | ), encoding='utf-8'))
74 | drag.setMimeData(mime_data)
75 | drop_action = drag.exec_()
76 |
77 | def mouseReleaseEvent(self, event):
78 | self.left_mouse_pressed_on_me = False
79 | if self.geometry().contains(self.mapToParent(event.pos())):
80 | self.chosen.emit()
81 |
82 | def set_custom_focus(self, new_focus):
83 | self.custom_focused = new_focus
84 | self.update_stylesheet()
85 |
86 | def update_stylesheet(self):
87 | color = self.node.GUI.color if hasattr(self.node, 'GUI') else '#888888'
88 |
89 | r, g, b = QColor(color).red(), QColor(color).green(), QColor(color).blue()
90 |
91 | new_style_sheet = f'''
92 | NodeWidget {{
93 | border: 1px solid rgba(255,255,255,150);
94 | border-radius: 2px;
95 | {(
96 | f'background-color: rgba(255,255,255,80);'
97 | ) if self.custom_focused else ''}
98 | }}
99 | QLabel {{
100 | background: transparent;
101 | }}
102 | QLineEdit {{
103 | color: white;
104 | background: transparent;
105 | border: none;
106 | padding: 2px;
107 | }}
108 | '''
109 |
110 | self.setStyleSheet(new_style_sheet)
111 |
112 | def paintEvent(self, event): # just to enable stylesheets
113 | o = QStyleOption()
114 | o.initFrom(self)
115 | p = QPainter(self)
116 | self.style().drawPrimitive(QStyle.PE_Widget, o, p, self)
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/node_list_widget/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/src/flows/node_list_widget/__init__.py
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/node_list_widget/utils.py:
--------------------------------------------------------------------------------
1 | import textdistance
2 |
3 |
4 | def dec(i: int, length: int) -> int:
5 | if i != 0:
6 | return i - 1
7 | else:
8 | return length - 1
9 |
10 | def inc(i: int, length: int) -> int:
11 | if i != length - 1:
12 | return i + 1
13 | else:
14 | return 0
15 |
16 |
17 | def sort_nodes(nodes):
18 | return sorted(nodes, key=lambda x: x.title.lower())
19 |
20 |
21 | def sort_by_val(d: dict) -> dict:
22 | return {
23 | k: v
24 | for k, v in sorted(
25 | d.items(),
26 |
27 | # x: (key, value); sort by value
28 | key=lambda x: x[1]
29 | )
30 | }
31 |
32 |
33 | def search(items: dict, text: str) -> dict:
34 | """performs the search on `items` under search string `text`"""
35 | dist = textdistance.sorensen_dice.distance
36 |
37 | distances = {}
38 |
39 | for item, tags in items.items():
40 | min_dist = 1.0
41 | for tag in tags:
42 | min_dist = min(min_dist, dist(text, tag))
43 |
44 | distances[item] = min_dist
45 |
46 | return sort_by_val(distances)
47 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/nodes/NodeErrorIndicator.py:
--------------------------------------------------------------------------------
1 | import traceback
2 |
3 | from qtpy.QtWidgets import QGraphicsPixmapItem
4 | from qtpy.QtGui import QPixmap, QImage
5 |
6 | from ...GUIBase import GUIBase
7 | from ...utils import get_resource
8 |
9 |
10 | class NodeErrorIndicator(GUIBase, QGraphicsPixmapItem):
11 |
12 | def __init__(self, node_item):
13 | GUIBase.__init__(self)
14 | QGraphicsPixmapItem.__init__(self, parent=node_item)
15 |
16 | self.node = node_item
17 | self.pix = QPixmap(str(get_resource('pics/warning.png')))
18 | self.setPixmap(self.pix)
19 | self.setScale(0.1)
20 | self.setOffset(-self.boundingRect().width()/2, -self.boundingRect().width()/2)
21 |
22 | def set_error(self, e):
23 | error_msg = ''.join([
24 | f'
{line}
'
25 | for line in traceback.format_exc().splitlines()
26 | ])
27 |
28 | self.setToolTip(
29 | f''
30 | f'{error_msg}'
31 | f''
32 | )
33 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/nodes/NodeGUI.py:
--------------------------------------------------------------------------------
1 | from queue import Queue
2 | from typing import List, Dict, Tuple, Optional, Union
3 |
4 | from qtpy.QtCore import QObject, Signal
5 |
6 |
7 | class NodeGUI(QObject):
8 | """
9 | Interface class between nodes and their GUI representation.
10 | """
11 |
12 | # customizable gui attributes
13 | description_html: str = None
14 | main_widget_class: list = None
15 | main_widget_pos: str = 'below ports'
16 | input_widget_classes: dict = {}
17 | init_input_widgets: dict = {}
18 | style: str = 'normal'
19 | color: str = '#c69a15'
20 | display_title: str = None
21 | icon: str = None
22 |
23 | # qt signals
24 | updating = Signal()
25 | update_error = Signal(object)
26 | input_added = Signal(int, object)
27 | output_added = Signal(int, object)
28 | input_removed = Signal(int, object)
29 | output_removed = Signal(int, object)
30 | update_shape_triggered = Signal()
31 | hide_unconnected_ports_triggered = Signal()
32 | show_unconnected_ports_triggered = Signal()
33 |
34 | def __init__(self, params):
35 | QObject.__init__(self)
36 |
37 | node, session_gui = params
38 | self.node = node
39 | self.item = None # set by the node item directly after this __init__ call
40 | self.session_gui = session_gui
41 | setattr(node, 'gui', self)
42 |
43 | self.actions = self._init_default_actions()
44 |
45 | if self.display_title is None:
46 | self.display_title = self.node.title
47 |
48 | self.input_widgets = {} # {input: widget name}
49 | for i, widget_data in self.init_input_widgets.items():
50 | self.input_widgets[self.node.inputs[i]] = widget_data
51 | # using attach_input_widgets() one can buffer input widget
52 | # names for inputs that are about to get created
53 | self._next_input_widgets = Queue()
54 |
55 | self.error_during_update = False
56 |
57 | # turn ryvencore signals into Qt signals
58 | self.node.updating.sub(self._on_updating)
59 | self.node.update_error.sub(self._on_update_error)
60 | self.node.input_added.sub(self._on_new_input_added)
61 | self.node.output_added.sub(self._on_new_output_added)
62 | self.node.input_removed.sub(self._on_input_removed)
63 | self.node.output_removed.sub(self._on_output_removed)
64 |
65 | def initialized(self):
66 | """
67 | *VIRTUAL*
68 |
69 | Called after the node GUI has been fully initialized.
70 | The Node has been created already (including all ports) and loaded.
71 | No connections have been made to ports of the node yet.
72 | """
73 | pass
74 |
75 | """
76 | slots
77 | """
78 |
79 | # TODO: displaying update errors is currently prevented by the
80 | # lack of an appropriate updated event in ryvencore.
81 | # Update: there is an updating event now.
82 |
83 | # def on_updated(self, inp):
84 | # if self.error_during_update:
85 | # # an error should prevent an update event, so if we
86 | # # are here, the update was successful
87 | # self.self.error_during_update = False
88 | # self.item.remove_error_message()
89 | # self.updated.emit()
90 | #
91 | def _on_update_error(self, e):
92 | # self.item.display_error(e)
93 | # self.error_during_update = True
94 | self.update_error.emit(e)
95 |
96 | def _on_updating(self, inp: int):
97 | # update input widget
98 | if inp != -1 and self.item.inputs[inp].widget is not None:
99 | o = self.node.flow.connected_output(self.node.inputs[inp])
100 | if o is not None:
101 | self.item.inputs[inp].widget.val_update_event(o.val)
102 |
103 | self.updating.emit()
104 |
105 | def _on_new_input_added(self, _, index, inp):
106 | if not self._next_input_widgets.empty():
107 | self.input_widgets[inp] = self._next_input_widgets.get()
108 | self.input_added.emit(index, inp)
109 |
110 | def _on_new_output_added(self, _, index, out):
111 | self.output_added.emit(index, out)
112 |
113 | def _on_input_removed(self, _, index, inp):
114 | self.input_removed.emit(index, inp)
115 |
116 | def _on_output_removed(self, _, index, out):
117 | self.output_removed.emit(index, out)
118 |
119 | """
120 | actions
121 |
122 | TODO: move actions to ryvencore?
123 | """
124 |
125 | def _init_default_actions(self):
126 | """
127 | Returns the default actions every node should have
128 | """
129 | return {
130 | 'update shape': {'method': self.update_shape},
131 | 'hide unconnected ports': {'method': self.hide_unconnected_ports},
132 | 'change title': {'method': self.change_title},
133 | }
134 |
135 | def _deserialize_actions(self, actions_data):
136 | """
137 | Recursively reconstructs the actions dict from the serialized version
138 | """
139 |
140 | def _transform(actions_data: dict):
141 | """
142 | Mutates the actions_data argument by replacing the method names
143 | with the actual methods. Doesn't modify the original dict.
144 | """
145 | new_actions = {}
146 | for key, value in actions_data.items():
147 | if key == 'method':
148 | try:
149 | value = getattr(self, value)
150 | except AttributeError:
151 | print(f'Warning: action method "{value}" not found in node "{self.node.title}", skipping.')
152 | elif isinstance(value, dict):
153 | value = _transform(value)
154 | new_actions[key] = value
155 | return new_actions
156 |
157 | return _transform(actions_data)
158 |
159 | def _serialize_actions(self, actions):
160 | """
161 | Recursively transforms the actions dict into a JSON-compatible dict
162 | by replacing methods with their name. Doesn't modify the original dict.
163 | """
164 |
165 | def _transform(actions: dict):
166 | new_actions = {}
167 | for key, value in actions.items():
168 | if key == 'method':
169 | new_actions[key] = value.__name__
170 | elif isinstance(value, dict):
171 | new_actions[key] = _transform(value)
172 | else:
173 | new_actions[key] = value
174 | return new_actions
175 |
176 | return _transform(actions)
177 |
178 | """
179 | serialization
180 | """
181 |
182 | def data(self):
183 | return {
184 | 'actions': self._serialize_actions(self.actions),
185 | 'display title': self.display_title,
186 | }
187 |
188 | def load(self, data):
189 | if 'actions' in data: # otherwise keep default
190 | self.actions = self._deserialize_actions(data['actions'])
191 | if 'display title' in data:
192 | self.display_title = data['display title']
193 |
194 | if 'special actions' in data: # backward compatibility
195 | self.actions = self._deserialize_actions(data['special actions'])
196 |
197 | """
198 | GUI access methods
199 | """
200 |
201 | def set_display_title(self, t: str):
202 | self.display_title = t
203 | self.update_shape()
204 |
205 | def flow_view(self):
206 | return self.item.flow_view
207 |
208 | def main_widget(self):
209 | """Returns the main_widget object, or None if the item doesn't exist (yet)"""
210 |
211 | return self.item.main_widget
212 |
213 | def attach_input_widgets(self, widget_names: List[str]):
214 | """Attaches the input widget to the next created input."""
215 |
216 | for w in widget_names:
217 | self._next_input_widgets.queue(w)
218 |
219 | def input_widget(self, index: int):
220 | """Returns a reference to the widget of the corresponding input"""
221 |
222 | return self.item.inputs[index].widget
223 |
224 | def session_stylesheet(self):
225 | return self.session_gui.design.global_stylesheet
226 |
227 | def update_shape(self):
228 | """Causes recompilation of the whole shape of the GUI item."""
229 |
230 | self.update_shape_triggered.emit()
231 |
232 | def hide_unconnected_ports(self):
233 | """Hides all ports that are not connected to anything."""
234 |
235 | del self.actions['hide unconnected ports']
236 | self.actions['show unconnected ports'] = {'method': self.show_unconnected_ports}
237 | self.hide_unconnected_ports_triggered.emit()
238 |
239 | def show_unconnected_ports(self):
240 | """Shows all ports that are not connected to anything."""
241 |
242 | del self.actions['show unconnected ports']
243 | self.actions['hide unconnected ports'] = {'method': self.hide_unconnected_ports}
244 | self.show_unconnected_ports_triggered.emit()
245 |
246 | def change_title(self):
247 | from qtpy.QtWidgets import QDialog, QVBoxLayout, QLineEdit
248 |
249 | class ChangeTitleDialog(QDialog):
250 | def __init__(self, title):
251 | super().__init__()
252 | self.new_title = None
253 | self.setLayout(QVBoxLayout())
254 | self.line_edit = QLineEdit(title)
255 | self.layout().addWidget(self.line_edit)
256 | self.line_edit.returnPressed.connect(self.return_pressed)
257 |
258 | def return_pressed(self):
259 | self.new_title = self.line_edit.text()
260 | self.accept()
261 |
262 | d = ChangeTitleDialog(self.display_title)
263 | d.exec_()
264 | if d.new_title:
265 | self.set_display_title(d.new_title)
266 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/nodes/NodeItemAction.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtCore import Signal
2 | from qtpy.QtWidgets import QAction
3 |
4 |
5 | class NodeItemAction(QAction):
6 | """A custom implementation of QAction that additionally stores transmitted 'data' which can be intuitively used
7 | in subclasses f.ex. to determine the exact source of the action triggered. For more info see GitHub docs.
8 | It shall not be a must to use the data parameter though. For that reason, there are two different signals,
9 | one that triggers with transmitted data, one without.
10 | So, if a special action does not have 'data', the connected method does not need to have a data parameter.
11 | Both signals get connected to the target method but only if data isn't None, the signal with the data parameter
12 | is used."""
13 |
14 | triggered_with_data = Signal(object, object)
15 | triggered_without_data = Signal(object)
16 |
17 | def __init__(self, node_gui, text, method, menu, data=None):
18 | super(NodeItemAction, self).__init__(text=text, parent=menu)
19 |
20 | self.node_gui = node_gui
21 | self.data = data
22 | self.method = method
23 | self.triggered.connect(self.triggered_)
24 |
25 | def triggered_(self):
26 | if self.data is not None:
27 | self.grab_method()(self.data)
28 | else:
29 | self.grab_method()()
30 |
31 | def grab_method(self):
32 | # the method object could have changed since the action was created
33 | return getattr(self.node_gui, self.method.__name__)
34 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/nodes/NodeItemAnimator.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtCore import QObject, QPropertyAnimation, Property
2 | from qtpy.QtGui import QColor
3 | from qtpy.QtWidgets import QGraphicsItem
4 |
5 |
6 | class NodeItemAnimator(QObject):
7 |
8 | def __init__(self, node_item):
9 | super(NodeItemAnimator, self).__init__()
10 |
11 | self.node_item = node_item
12 | self.animation_running = False
13 |
14 | self.title_activation_animation = QPropertyAnimation(self, b"p_title_color")
15 | self.title_activation_animation.setDuration(700)
16 | self.title_activation_animation.finished.connect(self.finished)
17 | self.body_activation_animation = QPropertyAnimation(self, b"p_body_color")
18 | self.body_activation_animation.setDuration(700)
19 |
20 | def start(self):
21 | self.animation_running = True
22 | self.title_activation_animation.start()
23 | self.body_activation_animation.start()
24 |
25 | def stop(self):
26 | # reset color values. it would just freeze without
27 | self.title_activation_animation.setCurrentTime(self.title_activation_animation.duration())
28 | self.body_activation_animation.setCurrentTime(self.body_activation_animation.duration())
29 |
30 | self.title_activation_animation.stop()
31 | self.body_activation_animation.stop()
32 |
33 | def finished(self):
34 | self.animation_running = False
35 |
36 | def running(self):
37 | return self.animation_running
38 |
39 | def reload_values(self):
40 | self.stop()
41 |
42 | # self.node_item.title_label.update_design()
43 | self.title_activation_animation.setKeyValueAt(0, self.get_title_color())
44 | self.title_activation_animation.setKeyValueAt(0.3, self.get_body_color().lighter().lighter())
45 | self.title_activation_animation.setKeyValueAt(1, self.get_title_color())
46 |
47 | self.body_activation_animation.setKeyValueAt(0, self.get_body_color())
48 | self.body_activation_animation.setKeyValueAt(0.3, self.get_body_color().lighter())
49 | self.body_activation_animation.setKeyValueAt(1, self.get_body_color())
50 |
51 | def fading_out(self):
52 | return self.title_activation_animation.currentTime()/self.title_activation_animation.duration() >= 0.3
53 |
54 | def set_animation_max(self):
55 | self.title_activation_animation.setCurrentTime(0.3*self.title_activation_animation.duration())
56 | self.body_activation_animation.setCurrentTime(0.3*self.body_activation_animation.duration())
57 |
58 | def get_body_color(self):
59 | return self.node_item.color
60 |
61 | def set_body_color(self, val):
62 | self.node_item.color = val
63 | QGraphicsItem.update(self.node_item)
64 |
65 | p_body_color = Property(QColor, get_body_color, set_body_color)
66 |
67 |
68 | def get_title_color(self):
69 | return self.node_item.widget.title_label.color
70 |
71 | def set_title_color(self, val):
72 | self.node_item.widget.title_label.color = val
73 | # QGraphicsItem.update(self.node_item)
74 |
75 | p_title_color = Property(QColor, get_title_color, set_title_color)
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/nodes/NodeItemWidget.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtCore import QPointF, QRectF, Qt, QSizeF
2 | from qtpy.QtWidgets import QGraphicsWidget, QGraphicsLinearLayout, QSizePolicy
3 |
4 | from .NodeItem_CollapseButton import NodeItem_CollapseButton
5 | from ..FlowViewProxyWidget import FlowViewProxyWidget
6 | # from .Node import Node
7 | from .NodeItem_Icon import NodeItem_Icon
8 | from .NodeItem_TitleLabel import TitleLabel
9 | from .PortItem import InputPortItem, OutputPortItem
10 |
11 |
12 | class NodeItemWidget(QGraphicsWidget):
13 | """The QGraphicsWidget managing all GUI components of a NodeItem in widgets and layouts."""
14 |
15 | def __init__(self, node_gui, node_item):
16 | super().__init__(parent=node_item)
17 |
18 | self.node_gui = node_gui
19 | self.node_item = node_item
20 | self.flow_view = self.node_item.flow_view
21 | self.flow = self.flow_view.flow
22 |
23 | self.body_padding = 6
24 | self.header_padding = (0, 0, 0, 0) # theme dependent and hence updated in setup_layout()!
25 |
26 | self.icon = NodeItem_Icon(node_gui, node_item) if node_gui.icon else None
27 | self.collapse_button = NodeItem_CollapseButton(node_gui, node_item) if node_gui.style == 'normal' else None
28 | self.title_label = TitleLabel(node_gui, node_item)
29 | self.main_widget_proxy: FlowViewProxyWidget = None
30 | if self.node_item.main_widget:
31 | self.main_widget_proxy = FlowViewProxyWidget(self.flow_view)
32 | self.main_widget_proxy.setWidget(self.node_item.main_widget)
33 | self.header_layout: QGraphicsWidget = None
34 | self.header_widget: QGraphicsWidget = None
35 | self.body_layout: QGraphicsLinearLayout = None
36 | self.body_widget: QGraphicsWidget = None
37 | self.inputs_layout: QGraphicsLinearLayout = None
38 | self.outputs_layout: QGraphicsLinearLayout = None
39 | self.setLayout(self.setup_layout())
40 |
41 | def setup_layout(self) -> QGraphicsLinearLayout:
42 |
43 | self.header_padding = self.node_item.session_design.flow_theme.header_padding
44 |
45 | # main layout
46 | layout = QGraphicsLinearLayout(Qt.Vertical)
47 | layout.setContentsMargins(0, 0, 0, 0)
48 | layout.setSpacing(0)
49 |
50 | if self.node_gui.style == 'normal':
51 | self.header_widget = QGraphicsWidget()
52 | # self.header_widget.setContentsMargins(0, 0, 0, 0)
53 | self.header_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
54 | self.header_layout = QGraphicsLinearLayout(Qt.Horizontal)
55 | self.header_layout.setSpacing(5)
56 | self.header_layout.setContentsMargins(
57 | *self.header_padding
58 | )
59 | if self.icon:
60 | self.header_layout.addItem(self.icon)
61 | self.header_layout.setAlignment(self.icon, Qt.AlignVCenter | Qt.AlignLeft)
62 |
63 | self.header_layout.addItem(self.title_label)
64 |
65 | self.header_layout.addItem(self.collapse_button)
66 | self.header_layout.setAlignment(self.collapse_button, Qt.AlignVCenter | Qt.AlignRight)
67 |
68 | self.header_widget.setLayout(self.header_layout)
69 | # layout.addItem(self.header_layout)
70 | layout.addItem(self.header_widget)
71 | # layout.setAlignment(self.title_label, Qt.AlignTop)
72 | else:
73 | self.setZValue(self.title_label.zValue() + 1)
74 |
75 | # inputs
76 | self.inputs_layout = QGraphicsLinearLayout(Qt.Vertical)
77 | self.inputs_layout.setSpacing(2)
78 |
79 | # outputs
80 | self.outputs_layout = QGraphicsLinearLayout(Qt.Vertical)
81 | self.outputs_layout.setSpacing(2)
82 |
83 | # body
84 | self.body_widget = QGraphicsWidget()
85 | # self.body_widget.setContentsMargins(0, 0, 0, 0)
86 | self.body_layout = QGraphicsLinearLayout(Qt.Horizontal)
87 | self.body_layout.setContentsMargins(
88 | self.body_padding,
89 | self.body_padding,
90 | self.body_padding,
91 | self.body_padding
92 | )
93 |
94 | self.body_layout.setSpacing(4)
95 | self.body_layout.addItem(self.inputs_layout)
96 | self.body_layout.setAlignment(self.inputs_layout, Qt.AlignVCenter | Qt.AlignLeft)
97 | self.body_layout.addStretch()
98 | self.body_layout.addItem(self.outputs_layout)
99 | self.body_layout.setAlignment(self.outputs_layout, Qt.AlignVCenter | Qt.AlignRight)
100 |
101 | self.body_widget.setLayout(self.body_layout)
102 |
103 | # layout.addItem(self.body_layout)
104 | layout.addItem(self.body_widget)
105 |
106 | return layout
107 |
108 | def rebuild_ui(self):
109 | """Due to some really strange and annoying behaviour of these QGraphicsWidgets, they don't want to shrink
110 | automatically when content is removed, they just stay large, even with a Minimum SizePolicy. I didn't find a
111 | way around that yet, so for now I have to recreate the whole layout and make sure the widget uses the smallest
112 | size possible."""
113 |
114 | # if I don't manually remove the ports from the layouts,
115 | # they get deleted when setting the widget's layout to None below
116 | for i, inp in enumerate(self.node_item.inputs):
117 | self.inputs_layout.removeAt(0)
118 | # inp.rebuild_ui()
119 | for i, out in enumerate(self.node_item.outputs):
120 | self.outputs_layout.removeAt(0)
121 | # out.rebuild_ui()
122 |
123 | self.setLayout(None)
124 | self.resize(self.minimumSize())
125 | self.setLayout(self.setup_layout())
126 |
127 | if self.node_item.collapsed:
128 | return
129 |
130 | for inp_item in self.node_item.inputs:
131 | self.add_input_to_layout(inp_item)
132 | for out_item in self.node_item.outputs:
133 | self.add_output_to_layout(out_item)
134 |
135 | if self.node_item.main_widget:
136 | self.add_main_widget_to_layout()
137 |
138 | def update_shape(self):
139 |
140 | self.title_label.update_shape()
141 |
142 | # makes extended node items shrink according to resizing input widgets
143 | if not self.node_item.initializing:
144 | self.rebuild_ui()
145 | # strangely, this only works for small node items without this, not for normal ones
146 |
147 | mw = self.node_item.main_widget
148 | if mw is not None: # maybe the main_widget got resized
149 | # self.main_widget_proxy.setMaximumSize(mw.size())
150 |
151 | # self.main_widget_proxy.setMaximumSize(mw.maximumSize())
152 |
153 | self.main_widget_proxy.setMaximumSize(QSizeF(mw.size()))
154 | self.main_widget_proxy.setMinimumSize(QSizeF(mw.size()))
155 |
156 | self.adjustSize()
157 | self.adjustSize()
158 |
159 | self.body_layout.invalidate()
160 | self.layout().invalidate()
161 | self.layout().activate()
162 | # very essential; repositions everything in case content has changed (inputs/outputs/widget)
163 |
164 | if self.node_gui.style == 'small':
165 |
166 | # making it recompute its true minimumWidth here
167 | self.adjustSize()
168 |
169 | if self.layout().minimumWidth() < self.title_label.width + 15:
170 | self.layout().setMinimumWidth(self.title_label.width + 15)
171 | self.layout().activate()
172 |
173 | w = self.boundingRect().width()
174 | h = self.boundingRect().height()
175 | rect = QRectF(QPointF(-w / 2, -h / 2),
176 | QPointF(w / 2, h / 2))
177 | self.setPos(rect.left(), rect.top())
178 |
179 | if not self.node_gui.style == 'normal':
180 | if self.icon:
181 | self.icon.setPos(
182 | QPointF(-self.icon.boundingRect().width() / 2,
183 | -self.icon.boundingRect().height() / 2)
184 | )
185 | self.title_label.hide()
186 | else:
187 | self.title_label.setPos(
188 | QPointF(-self.title_label.boundingRect().width() / 2,
189 | -self.title_label.boundingRect().height() / 2)
190 | )
191 |
192 |
193 | def add_main_widget_to_layout(self):
194 | if self.node_gui.main_widget_pos == 'between ports':
195 | self.body_layout.insertItem(1, self.main_widget_proxy)
196 | self.body_layout.insertStretch(2)
197 |
198 | elif self.node_gui.main_widget_pos == 'below ports':
199 | self.layout().addItem(self.main_widget_proxy)
200 | self.layout().setAlignment(self.main_widget_proxy, Qt.AlignHCenter)
201 |
202 | def add_input_to_layout(self, inp: InputPortItem):
203 | if self.inputs_layout.count() > 0:
204 | self.inputs_layout.addStretch()
205 | self.inputs_layout.addItem(inp)
206 | self.inputs_layout.setAlignment(inp, Qt.AlignLeft)
207 |
208 | def insert_input_into_layout(self, index: int, inp: InputPortItem):
209 | self.inputs_layout.insertItem(index * 2 + 1, inp) # *2 bcs of the stretches
210 | self.inputs_layout.setAlignment(inp, Qt.AlignLeft)
211 | if len(self.node_gui.node.inputs) > 1:
212 | self.inputs_layout.insertStretch(index * 2 + 1) # *2+1 because of the stretches, too
213 |
214 | def remove_input_from_layout(self, inp: InputPortItem):
215 | self.inputs_layout.removeItem(inp)
216 |
217 | # just a temporary workaround for the issues discussed here:
218 | # https://forum.qt.io/topic/116268/qgraphicslayout-not-properly-resizing-to-change-of-content
219 | self.rebuild_ui()
220 |
221 | def add_output_to_layout(self, out: OutputPortItem):
222 | if self.outputs_layout.count() > 0:
223 | self.outputs_layout.addStretch()
224 | self.outputs_layout.addItem(out)
225 | self.outputs_layout.setAlignment(out, Qt.AlignRight)
226 |
227 | def insert_output_into_layout(self, index: int, out: OutputPortItem):
228 | self.outputs_layout.insertItem(index * 2 + 1, out) # *2 because of the stretches
229 | self.outputs_layout.setAlignment(out, Qt.AlignRight)
230 | if len(self.node_gui.node.outputs) > 1:
231 | self.outputs_layout.insertStretch(index * 2 + 1) # *2+1 because of the stretches, too
232 |
233 | def remove_output_from_layout(self, out: OutputPortItem):
234 | self.outputs_layout.removeItem(out)
235 |
236 | # just a temporary workaround for the issues discussed here:
237 | # https://forum.qt.io/topic/116268/qgraphicslayout-not-properly-resizing-to-change-of-content
238 | self.rebuild_ui()
239 |
240 | def collapse(self):
241 | self.body_widget.hide()
242 | if self.main_widget_proxy:
243 | self.main_widget_proxy.hide()
244 |
245 | def expand(self):
246 | self.body_widget.show()
247 | if self.main_widget_proxy:
248 | self.main_widget_proxy.show()
249 |
250 | def hide_unconnected_ports(self):
251 | for inp in self.node_item.node.inputs:
252 | if self.flow.connected_output(inp) is None:
253 | inp.hide()
254 | for out in self.node_item.node.outputs:
255 | if len(self.flow.connected_inputs(out)):
256 | out.hide()
257 |
258 | def show_unconnected_ports(self):
259 | for inp in self.node_item.inputs:
260 | inp.show()
261 | for out in self.node_item.outputs:
262 | out.show()
263 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/nodes/NodeItem_CollapseButton.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtCore import QSize, QRectF, QPointF, QSizeF, Qt
2 | from qtpy.QtWidgets import QGraphicsWidget, QGraphicsLayoutItem
3 | from qtpy.QtGui import QColor
4 |
5 | from ...GlobalAttributes import Location
6 | from ...utils import change_svg_color, get_resource
7 |
8 |
9 | class NodeItem_CollapseButton(QGraphicsWidget):
10 | def __init__(self, node_gui, node_item):
11 | super().__init__(parent=node_item)
12 |
13 | self.node_gui = node_gui
14 | self.node_item = node_item
15 |
16 | self.size = QSizeF(14, 7)
17 |
18 | self.setGraphicsItem(self)
19 | self.setCursor(Qt.PointingHandCursor)
20 |
21 |
22 | self.collapse_pixmap = change_svg_color(
23 | get_resource('node_collapse_icon.svg'),
24 | self.node_gui.color
25 | )
26 | self.expand_pixmap = change_svg_color(
27 | get_resource('node_expand_icon.svg'),
28 | self.node_gui.color
29 | )
30 |
31 |
32 | def boundingRect(self):
33 | return QRectF(QPointF(0, 0), self.size)
34 |
35 | def setGeometry(self, rect):
36 | self.prepareGeometryChange()
37 | QGraphicsLayoutItem.setGeometry(self, rect)
38 | self.setPos(rect.topLeft())
39 |
40 | def sizeHint(self, which, constraint=...):
41 | return QSizeF(self.size.width(), self.size.height())
42 |
43 | def mousePressEvent(self, event):
44 | event.accept() # make sure the event doesn't get passed on
45 | self.node_item.flow_view.mouse_event_taken = True
46 |
47 | if self.node_item.collapsed:
48 | self.node_item.expand()
49 | else:
50 | self.node_item.collapse()
51 |
52 | # def hoverEnterEvent(self, event):
53 |
54 | def paint(self, painter, option, widget=None):
55 |
56 | # doesn't work: ...
57 | # painter.setRenderHint(QPainter.Antialiasing, True)
58 | # painter.setRenderHint(QPainter.HighQualityAntialiasing, True)
59 | # painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
60 |
61 | if not self.node_item.hovered:
62 | return
63 |
64 | if self.node_item.collapsed:
65 | pixmap = self.expand_pixmap
66 | else:
67 | pixmap = self.collapse_pixmap
68 |
69 | painter.drawPixmap(
70 | 0, 0,
71 | self.size.width(), self.size.height(),
72 | pixmap
73 | )
74 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/nodes/NodeItem_Icon.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtCore import QSize, QRectF, QPointF, QSizeF
2 | from qtpy.QtGui import QPixmap, QImage, QPainter, QIcon, QPicture
3 | from qtpy.QtWidgets import QGraphicsPixmapItem, QGraphicsWidget, QGraphicsLayoutItem
4 |
5 | from ...utils import change_svg_color
6 |
7 |
8 | class NodeItem_Icon(QGraphicsWidget):
9 | def __init__(self, node_gui, node_item):
10 | super().__init__(parent=node_item)
11 |
12 | if node_gui.style == 'normal':
13 | self.size = QSize(20, 20)
14 | else:
15 | self.size = QSize(50, 50)
16 |
17 | self.setGraphicsItem(self)
18 |
19 | image = QImage(node_gui.icon)
20 | self.pixmap = QPixmap.fromImage(image)
21 | # self.pixmap = change_svg_color(node.icon, node.color)
22 |
23 |
24 | def boundingRect(self):
25 | return QRectF(QPointF(0, 0), self.size)
26 |
27 | def setGeometry(self, rect):
28 | self.prepareGeometryChange()
29 | QGraphicsLayoutItem.setGeometry(self, rect)
30 | self.setPos(rect.topLeft())
31 |
32 | def sizeHint(self, which, constraint=...):
33 | return QSizeF(self.size.width(), self.size.height())
34 |
35 |
36 | def paint(self, painter, option, widget=None):
37 |
38 | # TODO: anti aliasing for node icons
39 |
40 | # this doesn't work: ...
41 | # painter.setRenderHint(QPainter.Antialiasing, True)
42 | # painter.setRenderHint(QPainter.HighQualityAntialiasing, True)
43 | # painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
44 |
45 |
46 | painter.drawPixmap(
47 | 0, 0,
48 | self.size.width(), self.size.height(),
49 | self.pixmap
50 | )
51 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/nodes/NodeItem_TitleLabel.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtCore import QRectF, QPointF, QSizeF, Property
2 | from qtpy.QtGui import QFont, QFontMetricsF, QColor
3 | from qtpy.QtWidgets import QGraphicsWidget, QGraphicsLayoutItem, QGraphicsItem
4 |
5 | from ...utils import get_longest_line
6 |
7 |
8 | class TitleLabel(QGraphicsWidget):
9 |
10 | def __init__(self, node_gui, node_item):
11 | super(TitleLabel, self).__init__(parent=node_item)
12 |
13 | self.setGraphicsItem(self)
14 |
15 | self.node_gui = node_gui
16 | self.node_item = node_item
17 |
18 | font = QFont('Poppins', 15) if self.node_gui.style == 'normal' else \
19 | QFont('K2D', 20, QFont.Bold, True) # should be quite similar to every specific font chosen by the painter
20 | self.fm = QFontMetricsF(font)
21 | self.title_str, self.width, self.height = None, None, None
22 | self.update_shape()
23 |
24 | self.color = QColor(30, 43, 48)
25 | self.pen_width = 1.5
26 | self.hovering = False # whether the mouse is hovering over the parent NI (!)
27 |
28 | # # Design.flow_theme_changed.connect(self.theme_changed)
29 | # self.update_design()
30 |
31 | def update_shape(self):
32 | self.title_str = self.node_gui.display_title
33 |
34 | # approximately!
35 | self.width = self.fm.width(get_longest_line(self.title_str)+'___')
36 | self.height = self.fm.height() * 0.7 * (self.title_str.count('\n') + 1)
37 |
38 | def boundingRect(self):
39 | return QRectF(QPointF(0, 0), self.geometry().size())
40 |
41 | def setGeometry(self, rect):
42 | self.prepareGeometryChange()
43 | QGraphicsLayoutItem.setGeometry(self, rect)
44 | self.setPos(rect.topLeft())
45 |
46 | def sizeHint(self, which, constraint=...):
47 | return QSizeF(self.width, self.height)
48 |
49 | def paint(self, painter, option, widget=None):
50 | self.node_item.session_design.flow_theme.paint_NI_title_label(
51 | self.node_gui, self.node_item.isSelected(), self.hovering, painter, option,
52 | self.design_style(), self.title_str,
53 | self.node_item.color, self.boundingRect()
54 | )
55 |
56 | def design_style(self):
57 | return self.node_gui.style
58 |
59 | def set_NI_hover_state(self, hovering: bool):
60 | self.hovering = hovering
61 | # self.update_design()
62 | self.update()
63 |
64 | # ANIMATION STUFF
65 | def get_color(self):
66 | return self.color
67 |
68 | def set_color(self, val):
69 | self.color = val
70 | QGraphicsItem.update(self)
71 |
72 | p_color = Property(QColor, get_color, set_color)
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/nodes/PortItemInputWidgets.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtGui import QFontMetrics, QFont
2 | from qtpy.QtWidgets import QSpinBox, QLineEdit, QCheckBox, QComboBox
3 |
4 | from .WidgetBaseClasses import NodeInputWidget
5 |
6 |
7 | class DType_IW_Base(NodeInputWidget):
8 |
9 | def __init__(self, params):
10 | super().__init__(params)
11 |
12 | self.dtype = self.input.dtype
13 | self.block = False
14 |
15 | def change_val(self, val):
16 | if not self.block:
17 | self.dtype.val = val
18 | self.update_node_input(val)
19 |
20 | def __str__(self):
21 | return self.__class__.__name__
22 |
23 |
24 | class Data_IW(DType_IW_Base, QLineEdit): # virtual
25 |
26 | base_width = None # specified by subclasses
27 |
28 | def __init__(self, params):
29 | DType_IW_Base.__init__(self, params)
30 | QLineEdit.__init__(self)
31 |
32 | # dtype = self.input.dtype
33 | # self.last_val = None
34 |
35 | self.setFont(QFont('source code pro', 10))
36 | self.val_update_event(self.dtype.val)
37 | if self.dtype.size == 's':
38 | self.base_width = 30
39 | elif self.dtype.size == 'm':
40 | self.base_width = 70
41 | elif self.dtype.size == 'l':
42 | self.base_width = 150
43 | self.max_width = self.base_width * 3
44 | self.setFixedWidth(self.base_width)
45 | self.fm = QFontMetrics(self.font())
46 |
47 | self.setToolTip(self.dtype.doc)
48 | self.textChanged.connect(self.text_changed)
49 | self.editingFinished.connect(self.editing_finished)
50 |
51 | def text_changed(self, new_text):
52 | """manages resizing of widget to content"""
53 |
54 | text_width = self.fm.width(new_text)
55 | new_width = text_width+15 # add some buffer
56 | if new_width < self.max_width:
57 | self.setFixedWidth(new_width if new_width > self.base_width else self.base_width)
58 | else:
59 | self.setFixedWidth(self.max_width)
60 | self.node.update_shape()
61 |
62 | def editing_finished(self):
63 | """updates the input"""
64 | # if v != self.last_val:
65 | # self.last_val = v
66 | self.change_val(self.get_val())
67 |
68 | def get_val(self):
69 | try:
70 | return eval(self.text())
71 | except Exception as e:
72 | return self.text()
73 |
74 | def val_update_event(self, val):
75 | """triggered when input is connected and received new data;
76 | displays the data in the widget (without updating)"""
77 |
78 | self.block = True
79 | try:
80 | self.setText(str(val))
81 | except Exception as e:
82 | pass
83 | finally:
84 | self.block = False
85 |
86 | def get_state(self) -> dict:
87 | return {'text': self.text()}
88 |
89 | def set_state(self, data: dict):
90 | # just show value, DO NOT UPDATE
91 | self.val_update_event(data['text'])
92 |
93 |
94 | # custom sized classes for qss access:
95 |
96 | class Data_IW_S(Data_IW):
97 | base_width = 30
98 |
99 |
100 | class Data_IW_M(Data_IW):
101 | base_width = 70
102 |
103 |
104 | class Data_IW_L(Data_IW):
105 | base_width = 150
106 |
107 |
108 | # -----------------------------------
109 |
110 |
111 | class String_IW(DType_IW_Base, QLineEdit): # virtual
112 |
113 | width = None # specified by subclasses
114 |
115 | def __init__(self, params):
116 | DType_IW_Base.__init__(self, params)
117 | QLineEdit.__init__(self)
118 |
119 | # dtype = self.input.dtype
120 | # self.last_val = None
121 |
122 | self.setFont(QFont('source code pro', 10))
123 | self.setText(self.dtype.val)
124 | self.setFixedWidth(self.width)
125 | self.setToolTip(self.dtype.doc)
126 |
127 | self.editingFinished.connect(self.editing_finished)
128 |
129 | def editing_finished(self):
130 | """updates the input"""
131 | self.change_val(self.get_val())
132 | # v = self.get_val()
133 | # if v != self.last_val:
134 | # self.update_node_input(v)
135 | # self.last_val = v
136 |
137 | def get_val(self):
138 | return self.text()
139 |
140 | def val_update_event(self, val):
141 | """triggered when input is connected and received new data;
142 | displays the data in the widget (without updating)"""
143 | self.block = True
144 | self.setText(str(val))
145 | self.block = False
146 |
147 | def get_state(self) -> dict:
148 | return {'text': self.text()}
149 |
150 | def set_state(self, data: dict):
151 | # just show value, DO NOT UPDATE
152 | self.val_update_event(data['text'])
153 |
154 |
155 | # custom sized classes for qss access:
156 |
157 | class String_IW_S(String_IW):
158 | width = 30
159 |
160 |
161 | class String_IW_M(String_IW):
162 | width = 70
163 |
164 |
165 | class String_IW_L(String_IW):
166 | width = 150
167 |
168 |
169 | # -----------------------------------
170 |
171 |
172 | class Integer_IW(DType_IW_Base, QSpinBox):
173 | def __init__(self, params):
174 | DType_IW_Base.__init__(self, params)
175 | QSpinBox.__init__(self)
176 |
177 | if self.dtype.bounds:
178 | self.setRange(self.dtype.bounds[0], self.dtype.bounds[1])
179 | self.setValue(self.dtype.val)
180 | self.setToolTip(self.dtype.doc)
181 |
182 | self.valueChanged.connect(self.widget_val_changed)
183 |
184 | def widget_val_changed(self, val):
185 | self.change_val(val)
186 |
187 | def get_val(self):
188 | return self.value()
189 |
190 | def val_update_event(self, val):
191 | """triggered when input is connected and received new data;
192 | displays the data in the widget (without updating)"""
193 | self.block = True
194 | try:
195 | self.setValue(val)
196 | except Exception as e:
197 | pass
198 | finally:
199 | self.block = False
200 |
201 | def get_state(self) -> dict:
202 | return {'val': self.value()}
203 |
204 | def set_state(self, data: dict):
205 | # just show value, DO NOT UPDATE
206 | self.val_update_event(data['val'])
207 |
208 |
209 | class Float_IW(DType_IW_Base, QLineEdit):
210 | def __init__(self, params):
211 | DType_IW_Base.__init__(self, params)
212 | QLineEdit.__init__(self)
213 |
214 | self.setFont(QFont('source code pro', 10))
215 | fm = QFontMetrics(self.font())
216 | self.setMaximumWidth(fm.width(' ')*self.dtype.decimals+1)
217 | self.setText(str(self.dtype.val))
218 | self.setToolTip(self.dtype.doc)
219 |
220 | self.textChanged.connect(self.widget_text_changed)
221 |
222 | def widget_text_changed(self):
223 | self.change_val(self.get_val())
224 |
225 | def get_val(self):
226 | return float(self.text())
227 |
228 | def val_update_event(self, val):
229 | """triggered when input is connected and received new data;
230 | displays the data in the widget (without updating)"""
231 | self.block = True
232 | try:
233 | self.setText(str(val))
234 | except Exception as e:
235 | pass
236 | finally:
237 | self.block = False
238 |
239 | def get_state(self) -> dict:
240 | return {'text': self.text()}
241 |
242 | def set_state(self, data: dict):
243 | # just show value, DO NOT UPDATE
244 | self.val_update_event(data['text'])
245 |
246 |
247 | class Boolean_IW(DType_IW_Base, QCheckBox):
248 | def __init__(self, params):
249 | DType_IW_Base.__init__(self, params)
250 | QCheckBox.__init__(self)
251 |
252 | self.setChecked(self.dtype.val)
253 |
254 | self.setToolTip(self.dtype.doc)
255 |
256 | self.stateChanged.connect(self.state_changed)
257 |
258 | def state_changed(self, state):
259 | self.change_val(self.get_val())
260 |
261 | def get_val(self):
262 | return self.isChecked()
263 |
264 | def val_update_event(self, val):
265 | """triggered when input is connected and received new data;
266 | displays the data in the widget (without updating)"""
267 | self.block = True
268 | try:
269 | self.setChecked(bool(val))
270 | except Exception as e:
271 | pass
272 | finally:
273 | self.block = False
274 |
275 | def get_state(self) -> dict:
276 | return {'checked': self.isChecked()}
277 |
278 | def set_state(self, data: dict):
279 | # just show value, DO NOT UPDATE
280 | self.val_update_event(data['checked'])
281 |
282 |
283 | class Choice_IW(DType_IW_Base, QComboBox):
284 | def __init__(self, params):
285 | DType_IW_Base.__init__(self, params)
286 | QComboBox.__init__(self)
287 |
288 | self.addItems(self.dtype.items)
289 | self.setCurrentText(self.dtype.val)
290 | self.setToolTip(self.dtype.doc)
291 |
292 | self.currentTextChanged.connect(self.widget_text_changed)
293 |
294 | def widget_text_changed(self):
295 | self.change_val(self.get_val())
296 |
297 | def get_val(self):
298 | return self.currentText()
299 |
300 | def val_update_event(self, val):
301 | """triggered when input is connected and received new data;
302 | displays the data in the widget (without updating)"""
303 | self.block = True
304 | try:
305 | self.setCurrentText(val)
306 | except Exception as e:
307 | pass
308 | finally:
309 | self.block = False
310 |
311 | def get_state(self) -> dict:
312 | return {
313 | 'items': [self.itemText(i) for i in range(self.count())],
314 | 'active': self.currentText(),
315 | }
316 |
317 | def set_state(self, data: dict):
318 | # just show value, DO NOT UPDATE
319 | self.block = True
320 | self.clear()
321 | self.addItems(data['items'])
322 | self.setCurrentText(data['active'])
323 | self.block = False
324 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/nodes/WidgetBaseClasses.py:
--------------------------------------------------------------------------------
1 | """The base classes for node custom widgets for nodes."""
2 | from ryvencore import Data
3 |
4 |
5 | class NodeMainWidget:
6 | """Base class for the main widget of a node."""
7 |
8 | def __init__(self, params):
9 | self.node, self.node_item, self.node_gui = params
10 |
11 | # OVERRIDE
12 | def get_state(self) -> dict:
13 | """
14 | *VIRTUAL*
15 |
16 | Return the state of the widget, in a (pickle) serializable format.
17 | """
18 | data = {}
19 | return data
20 |
21 | def set_state(self, data: dict):
22 | """
23 | *VIRTUAL*
24 |
25 | Set the state of the widget, where data corresponds to the dict
26 | returned by get_state().
27 | """
28 | pass
29 |
30 | # def remove_event(self):
31 | # """
32 | # *VIRTUAL*
33 | #
34 | # Called when the input is removed.
35 | # """
36 | # pass
37 |
38 | def update_node(self):
39 | self.node.update()
40 |
41 | def update_node_shape(self):
42 | self.node_item.update_shape()
43 |
44 |
45 | class NodeInputWidget:
46 | """Base class for the input widget of a node."""
47 |
48 | def __init__(self, params):
49 | self.input, self.input_item, self.node, self.node_gui, self.position = \
50 | params
51 |
52 | def get_state(self) -> dict:
53 | """
54 | *VIRTUAL*
55 |
56 | Return the state of the widget, in a (pickle) serializable format.
57 | """
58 | data = {}
59 | return data
60 |
61 | # OVERRIDE
62 | def set_state(self, data: dict):
63 | """
64 | *VIRTUAL*
65 |
66 | Set the state of the widget, where data corresponds to the dict
67 | returned by get_state().
68 | """
69 | pass
70 |
71 | # OVERRIDE
72 | # def remove_event(self):
73 | # pass
74 |
75 | def val_update_event(self, val: Data):
76 | """
77 | *VIRTUAL*
78 |
79 | Called when the input's value is updated through a connection.
80 | This can be used to represent the value in the widget.
81 | The widget is disabled when the port is connected.
82 | """
83 | pass
84 |
85 | """
86 |
87 | API methods
88 |
89 | """
90 |
91 | def update_node_input(self, val: Data, silent=False):
92 | """
93 | Update the input's value and update the node.
94 | """
95 | self.input.default = val
96 | if not silent:
97 | self.input.node.update(self.node.inputs.index(self.input))
98 |
99 | def update_node(self):
100 | self.node.update(self.node.inputs.index(self.input))
101 |
102 | def update_node_shape(self):
103 | self.node_gui.update_shape()
104 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/flows/nodes/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/leon-thomm/ryvencore-qt/87d3adf5df9db76076e2d02c87d52991a9a4a493/ryvencore_qt/src/flows/nodes/__init__.py
--------------------------------------------------------------------------------
/ryvencore_qt/src/utils.py:
--------------------------------------------------------------------------------
1 | import bisect
2 | import enum
3 | import json
4 | import pathlib
5 | from math import sqrt
6 | from typing import List, Dict
7 |
8 | from qtpy.QtCore import QPointF, QByteArray
9 |
10 | from ryvencore.utils import serialize, deserialize
11 | from .GlobalAttributes import *
12 |
13 |
14 | class Container:
15 | """used for threading; accessed from multiple threads"""
16 |
17 | def __init__(self):
18 | self.payload = None
19 | self.has_been_set = False
20 |
21 | def set(self, val):
22 | self.payload = val
23 | self.has_been_set = True
24 |
25 | def is_set(self):
26 | return self.has_been_set
27 |
28 |
29 | def pythagoras(a, b):
30 | return sqrt(a ** 2 + b ** 2)
31 |
32 |
33 | def get_longest_line(s: str):
34 | lines = s.split('\n')
35 | lines = [line.replace('\n', '') for line in lines]
36 | longest_line_found = ''
37 | for line in lines:
38 | if len(line) > len(longest_line_found):
39 | longest_line_found = line
40 | return line
41 |
42 |
43 | def shorten(s: str, max_chars: int, line_break: bool = False):
44 | """Ensures, that a given string does not exceed a given max length. If it would, its cut in the middle."""
45 | l = len(s)
46 | if l > max_chars:
47 | insert = ' . . . '
48 | if line_break:
49 | insert = '\n'+insert+'\n'
50 | insert_length = len(insert)
51 | left = s[:round((max_chars-insert_length)/2)]
52 | right = s[round(l-((max_chars-insert_length)/2)):]
53 | return left+insert+right
54 | else:
55 | return s
56 |
57 |
58 | def pointF_mapped(p1, p2):
59 | """adds the floating part of p2 to p1"""
60 | p2.setX(p1.x() + p2.x()%1)
61 | p2.setY(p1.y() + p2.y()%1)
62 | return p2
63 |
64 | def points_dist(p1, p2):
65 | return sqrt(abs(p1.x() - p2.x())**2 + abs(p1.y() - p2.y())**2)
66 |
67 | def middle_point(p1, p2):
68 | return QPointF((p1.x() + p2.x())/2, (p1.y() + p2.y())/2)
69 |
70 |
71 | class MovementEnum(enum.Enum):
72 | # this should maybe get removed later
73 | mouse_clicked = 1
74 | position_changed = 2
75 | mouse_released = 3
76 |
77 |
78 | def get_resource(filepath: str):
79 | return pathlib.Path(Location.PACKAGE_PATH, 'resources', filepath)
80 |
81 |
82 | def change_svg_color(filepath: str, color_hex: str):
83 | """Loads an SVG, changes all '#xxxxxx' occurrences to color_hex, renders it into and a pixmap and returns it"""
84 |
85 | # https://stackoverflow.com/questions/15123544/change-the-color-of-an-svg-in-qt
86 |
87 | from qtpy.QtSvg import QSvgRenderer
88 | from qtpy.QtGui import QPixmap, QPainter
89 | from qtpy.QtCore import Qt
90 |
91 | with open(filepath) as f:
92 | data = f.read()
93 | data = data.replace('fill:#xxxxxx', 'fill:'+color_hex)
94 |
95 | svg_renderer = QSvgRenderer(QByteArray(bytes(data, 'ascii')))
96 |
97 | pix = QPixmap(svg_renderer.defaultSize())
98 | pix.fill(Qt.transparent)
99 | pix_painter = QPainter(pix)
100 | svg_renderer.render(pix_painter)
101 |
102 | return pix
103 |
104 |
105 |
106 | def translate_project(project: Dict) -> Dict:
107 | """
108 | Transforms a v3.0 project file into something that can be loaded in v3.1,
109 | i.e. turns macros into scripts and removes macro nodes from the flows.
110 | """
111 | # TODO: this needs to be changed to match ryvencore 0.4 structure
112 | new_project = project.copy()
113 |
114 | # turn macros into scripts
115 |
116 | fixed_scripts = []
117 |
118 | for script in (project['macro scripts']+project['scripts']):
119 |
120 | new_script = script.copy()
121 |
122 | # remove macro nodes
123 | new_nodes, removed_node_indices = remove_macro_nodes(script['flow']['nodes'])
124 | new_script['flow']['nodes'] = new_nodes
125 |
126 | # fix connections
127 | new_script['flow']['connections'] = fix_connections(script['flow']['connections'], removed_node_indices)
128 |
129 | fixed_scripts.append(new_script)
130 |
131 | del new_project['macro scripts']
132 | new_project['scripts'] = fixed_scripts
133 |
134 | return new_project
135 |
136 |
137 | def remove_macro_nodes(nodes):
138 | """
139 | removes all macro nodes from the nodes list and returns the new list as well as the indices of the removed nodes
140 | """
141 |
142 | new_nodes = []
143 | removed_node_indices = []
144 |
145 | for n_i in range(len(nodes)):
146 | node = nodes[n_i]
147 |
148 | if node['identifier'] in ('BUILTIN_MacroInputNode', 'BUILTIN_MacroOutputNode') or \
149 | node['identifier'].startswith('MACRO_NODE_'):
150 | removed_node_indices.append(n_i)
151 | else:
152 | new_nodes.append(node)
153 |
154 | return new_nodes, removed_node_indices
155 |
156 |
157 | def fix_connections(connections: Dict, removed_node_indices: List) -> List:
158 | """
159 | removes connections to removed nodes and fixes node indices of the other ones
160 | """
161 |
162 | import bisect
163 |
164 | new_connections = []
165 |
166 | for conn in connections:
167 | if conn['parent node index'] in removed_node_indices or conn['connected node'] in removed_node_indices:
168 | # remove connection
169 | continue
170 | else:
171 | # fix node indices
172 | pni = conn['parent node index']
173 | cni = conn['connected node']
174 |
175 | # calculate the number of removed nodes with indices < pni | cni
176 | num_smaller_removed_pni = bisect.bisect_left(removed_node_indices, pni)
177 | num_smaller_removed_cni = bisect.bisect_left(removed_node_indices, cni)
178 |
179 | c = conn.copy()
180 |
181 | # decrease indices accordingly
182 | c['parent node index'] = pni - num_smaller_removed_pni
183 | c['connected node'] = cni - num_smaller_removed_cni
184 |
185 | new_connections.append(c)
186 |
187 | return new_connections
188 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/widgets/EditVal_Dialog.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QPlainTextEdit, QShortcut, QMessageBox
2 | from qtpy.QtGui import QKeySequence
3 |
4 |
5 | class EditVal_Dialog(QDialog):
6 |
7 | def __init__(self, parent, init_val):
8 | super(EditVal_Dialog, self).__init__(parent)
9 |
10 | # shortcut
11 | save_shortcut = QShortcut(QKeySequence.Save, self)
12 | save_shortcut.activated.connect(self.save_triggered)
13 |
14 | main_layout = QVBoxLayout()
15 |
16 | self.val_text_edit = QPlainTextEdit()
17 | val_str = ''
18 | try:
19 | val_str = str(init_val)
20 | except Exception as e:
21 | msg_box = QMessageBox(QMessageBox.Warning, 'Value parsing failed',
22 | 'Couldn\'t stringify value', QMessageBox.Ok, self)
23 | msg_box.setDefaultButton(QMessageBox.Ok)
24 | msg_box.exec_()
25 | self.reject()
26 |
27 | self.val_text_edit.setPlainText(val_str)
28 |
29 | main_layout.addWidget(self.val_text_edit)
30 |
31 | button_box = QDialogButtonBox()
32 | button_box.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok)
33 | button_box.accepted.connect(self.accept)
34 | button_box.rejected.connect(self.reject)
35 |
36 | main_layout.addWidget(button_box)
37 |
38 | self.setLayout(main_layout)
39 | self.resize(450, 300)
40 |
41 | self.setWindowTitle('edit val')
42 |
43 | def save_triggered(self):
44 | self.accept()
45 |
46 |
47 | def get_val(self):
48 | val = self.val_text_edit.toPlainText()
49 | try:
50 | val = eval(val)
51 | except Exception as e:
52 | pass
53 | return val
--------------------------------------------------------------------------------
/ryvencore_qt/src/widgets/FlowsListWidget.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtCore import Qt
2 | from qtpy.QtWidgets import QWidget, QMessageBox, QVBoxLayout, QLineEdit, QHBoxLayout, QPushButton, QScrollArea
3 |
4 | from .FlowsList_FlowWidget import FlowsList_FlowWidget
5 |
6 |
7 | class FlowsListWidget(QWidget):
8 | """Convenience class for a QWidget to easily manage the flows of a session."""
9 |
10 | def __init__(self, session_gui):
11 | super().__init__()
12 |
13 | self.session_gui = session_gui
14 | self.list_widgets = []
15 | self.ignore_name_line_edit_signal = False # because disabling causes firing twice otherwise
16 |
17 | self.setup_UI()
18 |
19 | self.session_gui.flow_view_created.connect(self.add_new_flow)
20 | self.session_gui.flow_deleted.connect(self.recreate_list)
21 |
22 |
23 | def setup_UI(self):
24 | main_layout = QVBoxLayout(self)
25 | main_layout.setAlignment(Qt.AlignTop)
26 | self.setLayout(main_layout)
27 |
28 | # list scroll area
29 |
30 | self.list_scroll_area = QScrollArea(self)
31 | self.list_scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
32 | self.list_scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
33 | self.list_scroll_area.setWidgetResizable(True)
34 | self.list_scroll_area.setContentsMargins(0, 0, 0, 0)
35 |
36 | self.scroll_area_widget = QWidget()
37 | self.scroll_area_widget.setContentsMargins(0, 0, 0, 0)
38 | self.list_scroll_area.setWidget(self.scroll_area_widget)
39 |
40 | self.list_layout = QVBoxLayout()
41 | self.list_layout.setContentsMargins(0, 0, 0, 0)
42 | self.list_layout.setAlignment(Qt.AlignTop)
43 | self.scroll_area_widget.setLayout(self.list_layout)
44 |
45 | self.layout().addWidget(self.list_scroll_area)
46 |
47 | # line edit
48 |
49 | self.new_flow_title_lineedit = QLineEdit()
50 | self.new_flow_title_lineedit.setPlaceholderText('new flow\'s title')
51 | self.new_flow_title_lineedit.returnPressed.connect(self.create_flow)
52 |
53 | main_layout.addWidget(self.new_flow_title_lineedit)
54 |
55 |
56 | self.recreate_list()
57 |
58 |
59 | def recreate_list(self):
60 | # remove flow widgets
61 | for i in reversed(range(self.list_layout.count())):
62 | self.list_layout.itemAt(i).widget().setParent(None)
63 |
64 | self.list_widgets.clear()
65 |
66 | for s in self.session_gui.core_session.flows:
67 | new_widget = FlowsList_FlowWidget(self, self.session_gui, s)
68 | self.list_widgets.append(new_widget)
69 |
70 | for w in self.list_widgets:
71 | self.list_layout.addWidget(w)
72 |
73 | def create_flow(self):
74 | title = self.new_flow_title_lineedit.text()
75 |
76 | if self.session_gui.core_session.flow_title_valid(title):
77 | self.session_gui.core_session.create_flow(title=title)
78 |
79 | def add_new_flow(self, flow, flow_view):
80 | self.recreate_list()
81 |
82 | def del_flow(self, flow, flow_widget):
83 | msg_box = QMessageBox(QMessageBox.Warning, 'sure about deleting flow?',
84 | 'You are about to delete a flow. This cannot be undone, all content will be lost. '
85 | 'Do you want to continue?', QMessageBox.Cancel | QMessageBox.Yes, self)
86 | msg_box.setDefaultButton(QMessageBox.Cancel)
87 | ret = msg_box.exec_()
88 | if ret != QMessageBox.Yes:
89 | return
90 |
91 | self.list_widgets.remove(flow_widget)
92 | flow_widget.setParent(None)
93 | self.session_gui.core_session.delete_flow(flow)
94 | # self.recreate_list()
95 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/widgets/FlowsList_FlowWidget.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtWidgets import QWidget, QHBoxLayout, QLabel, QMenu, QAction
2 | from qtpy.QtGui import QIcon, QImage
3 | from qtpy.QtCore import Qt, QEvent, QBuffer, QByteArray
4 |
5 | from ..GlobalAttributes import Location
6 | from .ListWidget_NameLineEdit import ListWidget_NameLineEdit
7 |
8 |
9 | class FlowsList_FlowWidget(QWidget):
10 | """A QWidget representing a single Flow for the FlowsListWidget."""
11 |
12 | def __init__(self, flows_list_widget, session_gui, flow):
13 | super().__init__()
14 |
15 | self.session_gui = session_gui
16 | self.flow = flow
17 | self.flow_view = self.session_gui.flow_views[flow]
18 | self.flows_list_widget = flows_list_widget
19 | self.previous_flow_title = ''
20 | self._thumbnail_source = ''
21 | self.ignore_title_line_edit_signal = False
22 |
23 |
24 | # UI
25 |
26 | main_layout = QHBoxLayout()
27 | main_layout.setContentsMargins(0, 0, 0, 0)
28 |
29 | # create icon
30 |
31 | # TODO: change this icon
32 | flow_icon = QIcon(Location.PACKAGE_PATH + '/resources/pics/script_picture.png')
33 |
34 | icon_label = QLabel()
35 | icon_label.setFixedSize(20, 20)
36 | icon_label.setStyleSheet('border:none;')
37 | icon_label.setPixmap(flow_icon.pixmap(20, 20))
38 | main_layout.addWidget(icon_label)
39 |
40 | # title line edit
41 |
42 | self.title_line_edit = ListWidget_NameLineEdit(flow.title, self)
43 | self.title_line_edit.setPlaceholderText('title')
44 | self.title_line_edit.setEnabled(False)
45 | self.title_line_edit.editingFinished.connect(self.title_line_edit_editing_finished)
46 |
47 | main_layout.addWidget(self.title_line_edit)
48 |
49 | self.setLayout(main_layout)
50 |
51 |
52 |
53 | def mouseDoubleClickEvent(self, event):
54 | if event.button() == Qt.LeftButton:
55 | if self.title_line_edit.geometry().contains(event.pos()):
56 | self.title_line_edit_double_clicked()
57 | return
58 |
59 |
60 | def event(self, event):
61 | if event.type() == QEvent.ToolTip:
62 |
63 | # generate preview img as QImage
64 | img: QImage = self.flow_view.get_viewport_img().scaledToHeight(200)
65 |
66 | # store the img data in QBuffer to load it directly from memory
67 | buffer = QBuffer()
68 | img.save(buffer, 'PNG')
69 |
70 | # generate html from data in memory
71 | html = f""
72 |
73 | # show tooltip
74 | self.setToolTip(html)
75 |
76 | return QWidget.event(self, event)
77 |
78 |
79 | def contextMenuEvent(self, event):
80 | menu: QMenu = QMenu(self)
81 |
82 | delete_action = QAction('delete')
83 | delete_action.triggered.connect(self.action_delete_triggered)
84 |
85 | actions = [delete_action]
86 | for a in actions:
87 | menu.addAction(a)
88 |
89 | menu.exec_(event.globalPos())
90 |
91 |
92 | def action_delete_triggered(self):
93 | self.flows_list_widget.del_flow(self.flow, self)
94 |
95 |
96 | def title_line_edit_double_clicked(self):
97 | self.title_line_edit.setEnabled(True)
98 | self.title_line_edit.setFocus()
99 | self.title_line_edit.selectAll()
100 |
101 | self.previous_flow_title = self.title_line_edit.text()
102 |
103 |
104 | def title_line_edit_editing_finished(self):
105 | if self.ignore_title_line_edit_signal:
106 | return
107 |
108 | title = self.title_line_edit.text()
109 |
110 | self.ignore_title_line_edit_signal = True
111 |
112 | if self.session_gui.core_session.flow_title_valid(title):
113 | self.session_gui.core_session.rename_flow(flow=self.flow, title=title)
114 | else:
115 | self.title_line_edit.setText(self.previous_flow_title)
116 |
117 | self.title_line_edit.setEnabled(False)
118 | self.ignore_title_line_edit_signal = False
119 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/widgets/ListWidget_NameLineEdit.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtWidgets import QLineEdit
2 |
3 |
4 | class ListWidget_NameLineEdit(QLineEdit):
5 |
6 | def __init__(self, text, parent):
7 | super().__init__(text, parent)
8 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/widgets/LogWidget.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtGui import QFont
2 | from qtpy.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QLabel, QPushButton, QPlainTextEdit
3 |
4 | # from ..core_wrapper.WRAPPERS import Logger
5 | from ryvencore.addons.Logging import Logger
6 | import logging
7 |
8 |
9 | class LogWidget(QWidget):
10 | """Convenience class for a QWidget representing a log."""
11 |
12 | def __init__(self, logger: Logger):
13 | super().__init__()
14 |
15 | self.logger = logger
16 | self.logger.addHandler(logging.StreamHandler(self))
17 | self.logger.sig_disabled.sub(self.disable)
18 | self.logger.sig_enabled.sub(self.enable)
19 |
20 | self.main_layout = QVBoxLayout()
21 | self.header_layout = QHBoxLayout()
22 |
23 | title_label = QLabel(self.logger.name)
24 | title_label.setFont(QFont('Poppins', 12))
25 | self.header_layout.addWidget(title_label)
26 |
27 | self.remove_button = QPushButton('x')
28 | self.remove_button.clicked.connect(self.remove_clicked)
29 | self.header_layout.addWidget(self.remove_button)
30 | self.remove_button.hide()
31 |
32 | self.text_edit = QPlainTextEdit()
33 | self.text_edit.setReadOnly(True)
34 |
35 | self.main_layout.addLayout(self.header_layout)
36 | self.main_layout.addWidget(self.text_edit)
37 |
38 | self.setLayout(self.main_layout)
39 |
40 | def write(self, msg: str):
41 | self.text_edit.appendPlainText(msg)
42 |
43 | def flush(self):
44 | pass
45 |
46 | # def clear(self):
47 | # self.text_edit.clear()
48 |
49 | def disable(self):
50 | self.remove_button.show()
51 |
52 | def enable(self):
53 | self.remove_button.hide()
54 | self.show()
55 |
56 | def remove_clicked(self):
57 | self.hide()
58 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/widgets/VariablesListWidget.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtWidgets import QVBoxLayout, QWidget, QLineEdit, QScrollArea
2 | from qtpy.QtCore import Qt
3 |
4 | from .VarsList_VarWidget import VarsList_VarWidget
5 |
6 |
7 | class VariablesListWidget(QWidget):
8 | """Convenience class for a QWidget to easily manage script variables of a script."""
9 |
10 | def __init__(self, vars_addon, flow):
11 | super(VariablesListWidget, self).__init__()
12 |
13 | self.vars_addon = vars_addon
14 | self.flow = flow
15 | self.vars_addon.var_created.sub(self.on_var_created)
16 | self.vars_addon.var_deleted.sub(self.on_var_deleted)
17 | self.widgets = []
18 | self.currently_edited_var = ''
19 | self.ignore_name_line_edit_signal = False # because disabling causes firing twice otherwise
20 | # self.data_type_line_edits = [] # same here
21 |
22 | self.setup_UI()
23 |
24 |
25 | def setup_UI(self):
26 | main_layout = QVBoxLayout()
27 |
28 | self.list_layout = QVBoxLayout()
29 | self.list_layout.setAlignment(Qt.AlignTop)
30 |
31 | # list scroll area
32 |
33 | self.list_scroll_area = QScrollArea()
34 | self.list_scroll_area.setWidgetResizable(True)
35 | self.list_scroll_area.setContentsMargins(0, 0, 0, 0)
36 |
37 | w = QWidget()
38 | w.setContentsMargins(0, 0, 0, 0)
39 | w.setLayout(self.list_layout)
40 |
41 | self.list_scroll_area.setWidget(w)
42 |
43 | main_layout.addWidget(self.list_scroll_area)
44 |
45 | # ------------------
46 |
47 | # controls
48 |
49 | self.new_var_name_lineedit = QLineEdit()
50 | self.new_var_name_lineedit.setPlaceholderText('new var\'s title')
51 | self.new_var_name_lineedit.returnPressed.connect(self.new_var_LE_return_pressed)
52 |
53 | main_layout.addWidget(self.new_var_name_lineedit)
54 |
55 | # ------------------
56 |
57 | self.setContentsMargins(0, 0, 0, 0)
58 | self.setLayout(main_layout)
59 |
60 | self.recreate_list()
61 |
62 |
63 | def on_var_created(self, flow, name, var):
64 | if flow == self.flow:
65 | self.widgets.append(
66 | VarsList_VarWidget(self, self.vars_addon, self.flow, var)
67 | )
68 | self.rebuild_list()
69 |
70 |
71 | def on_var_deleted(self, flow, name):
72 | # because Qt is weird, I cannot remove widgets the same way
73 | # I add them, so I have to do this
74 | self.recreate_list()
75 |
76 |
77 | def recreate_list(self):
78 | for w in self.widgets:
79 | w.hide()
80 | del w
81 |
82 | self.widgets.clear()
83 | # self.data_type_line_edits.clear()
84 |
85 | for var_name, var_info in self.vars_addon.flow_variables[self.flow].items():
86 | new_widget = VarsList_VarWidget(self, self.vars_addon, self.flow, var_info['var'])
87 | # new_widget.name_LE_editing_finished.connect(self.name_line_edit_editing_finished)
88 | self.widgets.append(new_widget)
89 |
90 | self.rebuild_list()
91 |
92 |
93 | def rebuild_list(self):
94 | for i in range(self.list_layout.count()):
95 | self.list_layout.removeItem(self.list_layout.itemAt(0))
96 |
97 | for w in self.widgets:
98 | self.list_layout.addWidget(w)
99 |
100 |
101 | def new_var_LE_return_pressed(self):
102 | name = self.new_var_name_lineedit.text()
103 | if not self.vars_addon.var_name_valid(self.flow, name=name):
104 | return
105 | v = self.vars_addon.create_var(self.flow, name=name)
106 |
107 |
108 | # def name_line_edit_editing_finished(self):
109 | # var_widget: VarsList_VarWidget = self.sender()
110 | # var_widget.name_line_edit.setEnabled(False)
111 | #
112 | # # search for name issues
113 | # new_var_name = var_widget.name_line_edit.text()
114 | # for v in self.vars_manager.variables:
115 | # if v.name == new_var_name:
116 | # var_widget.name_line_edit.setText(self.currently_edited_var.name)
117 | # return
118 | #
119 | # var_widget.var.name = new_var_name
120 |
121 |
122 | def del_var(self, var):
123 | self.vars_addon.delete_var(self.flow, var.name)
124 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/widgets/VarsList_VarWidget.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtWidgets import QWidget, QHBoxLayout, QLabel, QMenu, QAction
2 | from qtpy.QtGui import QIcon, QDrag
3 | from qtpy.QtCore import QMimeData, Qt, QEvent, QByteArray
4 |
5 | import json
6 |
7 | from ..GlobalAttributes import Location
8 | from .ListWidget_NameLineEdit import ListWidget_NameLineEdit
9 | from ..utils import shorten
10 | from .EditVal_Dialog import EditVal_Dialog
11 | from ryvencore.addons.Variables import VarsAddon
12 |
13 |
14 | class VarsList_VarWidget(QWidget):
15 | """A QWidget representing a single script variable for the VariablesListWidget."""
16 |
17 | def __init__(self, vars_list_widget, vars_addon: VarsAddon, flow, var):
18 | super().__init__()
19 |
20 | self.vars_addon = vars_addon
21 | self.flow = flow
22 | self.var = var
23 | self.vars_list_widget = vars_list_widget
24 | self.previous_var_name = '' # for editing
25 |
26 | self.ignore_name_line_edit_signal = False
27 |
28 |
29 | # UI
30 |
31 | main_layout = QHBoxLayout()
32 | main_layout.setContentsMargins(0, 0, 0, 0)
33 |
34 | # create icon
35 |
36 | variable_icon = QIcon(Location.PACKAGE_PATH+'/resources/pics/variable_picture.png')
37 |
38 | icon_label = QLabel()
39 | icon_label.setFixedSize(15, 15)
40 | icon_label.setStyleSheet('border:none;')
41 | icon_label.setPixmap(variable_icon.pixmap(15, 15))
42 | main_layout.addWidget(icon_label)
43 |
44 | # name line edit
45 |
46 | self.name_line_edit = ListWidget_NameLineEdit(self.var.name, self)
47 | self.name_line_edit.setPlaceholderText('name')
48 | self.name_line_edit.setEnabled(False)
49 | self.name_line_edit.editingFinished.connect(self.name_line_edit_editing_finished)
50 |
51 | main_layout.addWidget(self.name_line_edit)
52 |
53 | self.setLayout(main_layout)
54 |
55 |
56 |
57 | def mouseDoubleClickEvent(self, event):
58 | if event.button() == Qt.LeftButton:
59 | if self.name_line_edit.geometry().contains(event.pos()):
60 | self.name_line_edit_double_clicked()
61 | return
62 |
63 |
64 | def mousePressEvent(self, event):
65 | if event.button() == Qt.LeftButton:
66 | drag = QDrag(self)
67 | mime_data = QMimeData()
68 | data_text = self.get_drag_data()
69 | data = QByteArray(bytes(data_text, 'utf-8'))
70 | mime_data.setData('text/plain', data)
71 | drag.setMimeData(mime_data)
72 | drop_action = drag.exec_()
73 | return
74 |
75 |
76 | def event(self, event):
77 | if event.type() == QEvent.ToolTip:
78 | val_str = ''
79 | try:
80 | val_str = str(self.var.get())
81 | except Exception as e:
82 | val_str = "couldn't stringify value"
83 | self.setToolTip('val type: '+str(type(self.var.get()))+'\nval: '+shorten(val_str, 3000, line_break=True))
84 |
85 | return QWidget.event(self, event)
86 |
87 |
88 | def contextMenuEvent(self, event):
89 | menu: QMenu = QMenu(self)
90 |
91 | delete_action = QAction('delete')
92 | delete_action.triggered.connect(self.action_delete_triggered)
93 |
94 | edit_value_action = QAction('edit value')
95 | edit_value_action.triggered.connect(self.action_edit_val_triggered)
96 |
97 | actions = [delete_action, edit_value_action]
98 | for a in actions:
99 | menu.addAction(a)
100 |
101 | menu.exec_(event.globalPos())
102 |
103 |
104 | def action_delete_triggered(self):
105 | self.vars_list_widget.del_var(self.var)
106 |
107 |
108 | def action_edit_val_triggered(self):
109 | edit_var_val_dialog = EditVal_Dialog(self, self.var.get())
110 | accepted = edit_var_val_dialog.exec_()
111 | if accepted:
112 | self.var.set(edit_var_val_dialog.get_val())
113 | # self.vars_addon.create_var(self.flow, self.var.name, edit_var_val_dialog.get_val())
114 |
115 |
116 | def name_line_edit_double_clicked(self):
117 | self.name_line_edit.setEnabled(True)
118 | self.name_line_edit.setFocus()
119 | self.name_line_edit.selectAll()
120 |
121 | self.previous_var_name = self.name_line_edit.text()
122 |
123 |
124 | def get_drag_data(self):
125 | data = {'type': 'variable',
126 | 'name': self.var.name,
127 | 'value': self.var.get()} # value is probably unnecessary
128 | data_text = json.dumps(data)
129 | return data_text
130 |
131 |
132 | def name_line_edit_editing_finished(self):
133 | if self.ignore_name_line_edit_signal:
134 | return
135 |
136 | name = self.name_line_edit.text()
137 |
138 | self.ignore_name_line_edit_signal = True
139 |
140 | if self.vars_addon.var_name_valid(self.flow, name):
141 | self.var.name = name
142 | else:
143 | self.name_line_edit.setText(self.previous_var_name)
144 |
145 | self.name_line_edit.setEnabled(False)
146 | self.ignore_name_line_edit_signal = False
147 |
--------------------------------------------------------------------------------
/ryvencore_qt/src/widgets/__init__.py:
--------------------------------------------------------------------------------
1 | # list widgets
2 | from .VariablesListWidget import VariablesListWidget as VarsList
3 | from .FlowsListWidget import FlowsListWidget as FlowsList
4 | from ..flows.node_list_widget.NodeListWidget import NodeListWidget
5 |
6 | # logging
7 | from .LogWidget import LogWidget
8 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = ryvencore-qt
3 | version = v0.4.0a2
4 | author = Leon Thomm
5 | author_email = l.thomm@mailbox.org
6 | description = Qt frontend for ryvencore; Library for building Visual Node Editors
7 | long_description = file: README.md
8 | long_description_content_type = text/markdown
9 | license_file = LICENSE
10 | url = https://github.com/leon-thomm/ryvencore-qt
11 | project_urls =
12 | Website = https://ryven.org
13 | classifiers =
14 | Programming Language :: Python :: 3
15 | License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)
16 | Operating System :: OS Independent
17 |
18 | [options]
19 | packages = find:
20 | include_package_data = True
21 | python_requires = >=3.6
22 | install_requires =
23 | ryvencore ==0.4.*
24 | PySide2
25 | QtPy
26 | waiting
27 | textdistance
28 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 |
4 | setup()
5 |
--------------------------------------------------------------------------------