├── .gitattributes ├── .github └── workflows │ └── publish-package.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── TODO.md ├── docs ├── .nojekyll ├── README.md ├── img │ ├── function_node.png │ ├── logic_editor_screenshot1.png │ ├── logic_editor_screenshot2.png │ ├── logic_editor_screenshot3.png │ ├── logo.png │ ├── macro.png │ ├── macro2.png │ ├── ryvencore-drawio_.png │ ├── ryvencore_screenshot1.png │ └── stylus_light.png ├── index.html └── unused │ ├── _sidebar.md │ ├── api.md │ ├── features.md │ ├── getting_started.md │ ├── gui.md │ └── threading.md ├── drawio_v0.0.2.xml ├── drawio_v0.1.xml ├── examples ├── README.md └── readme │ ├── main.py │ ├── nodes.py │ └── readme.py ├── pyproject.toml ├── ryvencore_qt ├── __init__.py ├── resources │ ├── __init__.py │ ├── fonts │ │ ├── __init__.py │ │ ├── asap │ │ │ ├── Asap-Bold.ttf │ │ │ ├── Asap-BoldItalic.ttf │ │ │ ├── Asap-Italic.ttf │ │ │ ├── Asap-Medium.ttf │ │ │ ├── Asap-MediumItalic.ttf │ │ │ ├── Asap-Regular.ttf │ │ │ ├── Asap-SemiBold.ttf │ │ │ ├── Asap-SemiBoldItalic.ttf │ │ │ ├── OFL.txt │ │ │ └── __init__.py │ │ ├── poppins │ │ │ ├── OFL.txt │ │ │ ├── Poppins-Black.ttf │ │ │ ├── Poppins-BlackItalic.ttf │ │ │ ├── Poppins-Bold.ttf │ │ │ ├── Poppins-BoldItalic.ttf │ │ │ ├── Poppins-ExtraBold.ttf │ │ │ ├── Poppins-ExtraBoldItalic.ttf │ │ │ ├── Poppins-ExtraLight.ttf │ │ │ ├── Poppins-ExtraLightItalic.ttf │ │ │ ├── Poppins-Italic.ttf │ │ │ ├── Poppins-Light.ttf │ │ │ ├── Poppins-LightItalic.ttf │ │ │ ├── Poppins-Medium.ttf │ │ │ ├── Poppins-MediumItalic.ttf │ │ │ ├── Poppins-Regular.ttf │ │ │ ├── Poppins-SemiBold.ttf │ │ │ ├── Poppins-SemiBoldItalic.ttf │ │ │ ├── Poppins-Thin.ttf │ │ │ ├── Poppins-ThinItalic.ttf │ │ │ └── __init__.py │ │ └── source_code_pro │ │ │ ├── OFL.txt │ │ │ ├── SourceCodePro-Black.ttf │ │ │ ├── SourceCodePro-BlackItalic.ttf │ │ │ ├── SourceCodePro-Bold.ttf │ │ │ ├── SourceCodePro-BoldItalic.ttf │ │ │ ├── SourceCodePro-ExtraLight.ttf │ │ │ ├── SourceCodePro-ExtraLightItalic.ttf │ │ │ ├── SourceCodePro-Italic.ttf │ │ │ ├── SourceCodePro-Light.ttf │ │ │ ├── SourceCodePro-LightItalic.ttf │ │ │ ├── SourceCodePro-Medium.ttf │ │ │ ├── SourceCodePro-MediumItalic.ttf │ │ │ ├── SourceCodePro-Regular.ttf │ │ │ ├── SourceCodePro-SemiBold.ttf │ │ │ ├── SourceCodePro-SemiBoldItalic.ttf │ │ │ └── __init__.py │ ├── node_collapse_icon.svg │ ├── node_expand_icon.svg │ ├── pics │ │ ├── __init__.py │ │ ├── code_block_picture.png │ │ ├── code_block_picture2.png │ │ ├── code_block_picture3.png │ │ ├── function_picture.png │ │ ├── function_picture_.png │ │ ├── logo.png │ │ ├── macro_icon.svg │ │ ├── macro_node_icon.png │ │ ├── macro_script_picture.png │ │ ├── opencv_example.png │ │ ├── script_picture.png │ │ ├── script_picture.svg │ │ ├── script_picture_old.png │ │ ├── variable_picture.png │ │ └── warning.png │ └── warning.svg └── src │ ├── Design.py │ ├── GUIBase.py │ ├── GlobalAttributes.py │ ├── RCQT.py │ ├── README.md │ ├── SessionGUI.py │ ├── __init__.py │ ├── flows │ ├── FlowCommands.py │ ├── FlowTheme.py │ ├── FlowView.py │ ├── FlowViewProxyWidget.py │ ├── FlowViewStylusModesWidget.py │ ├── FlowViewZoomWidget.py │ ├── README.md │ ├── __init__.py │ ├── connections │ │ ├── ConnectionItem.py │ │ └── __init__.py │ ├── drawings │ │ ├── DrawingObject.py │ │ └── __init__.py │ ├── node_list_widget │ │ ├── NodeListWidget.py │ │ ├── NodeWidget.py │ │ ├── __init__.py │ │ └── utils.py │ └── nodes │ │ ├── NodeErrorIndicator.py │ │ ├── NodeGUI.py │ │ ├── NodeItem.py │ │ ├── NodeItemAction.py │ │ ├── NodeItemAnimator.py │ │ ├── NodeItemWidget.py │ │ ├── NodeItem_CollapseButton.py │ │ ├── NodeItem_Icon.py │ │ ├── NodeItem_TitleLabel.py │ │ ├── PortItem.py │ │ ├── PortItemInputWidgets.py │ │ ├── WidgetBaseClasses.py │ │ └── __init__.py │ ├── utils.py │ └── widgets │ ├── EditVal_Dialog.py │ ├── FlowsListWidget.py │ ├── FlowsList_FlowWidget.py │ ├── ListWidget_NameLineEdit.py │ ├── LogWidget.py │ ├── VariablesListWidget.py │ ├── VarsList_VarWidget.py │ └── __init__.py ├── setup.cfg └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/publish-package.yml: -------------------------------------------------------------------------------- 1 | name: Publish package distribution to TestPyPi and PyPi 2 | # only run when a new tag is created 3 | on: 4 | create: 5 | tags: 6 | - '*' 7 | jobs: 8 | Build-and-Publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-python@v4 13 | with: 14 | python-version: 3.9 15 | architecture: x64 16 | - name: Install pypa/build 17 | run: python -m pip install build --user 18 | - name: Build binary wheel and source tarball 19 | run: python -m build --sdist --wheel --outdir dist/ 20 | - name: Publish distribution to TestPyPI 21 | uses: pypa/gh-action-pypi-publish@release/v1 22 | with: 23 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 24 | repository-url: https://test.pypi.org/legacy/ 25 | - name: Publish distribution to PyPI 26 | uses: pypa/gh-action-pypi-publish@master 27 | with: 28 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | __pycache__/ 4 | venv/ 5 | build/ 6 | dist/ 7 | ryvencore_qt.egg-info/ 8 | site/ 9 | reinstall.bat 10 | reinstall_for_blender.bat 11 | upload.bat -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include **/resources * 2 | recursive-include ryvencore-qt * 3 | recursive-include docs * 4 | include LICENSE 5 | include MANIFEST.in 6 | include pyproject.toml 7 | include README.md 8 | include setup.cfg 9 | global-exclude *.py[co] 10 | global-exclude __pycache__ 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | drawing 4 |

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 | ![](./docs/img/stylus_light.png) 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 | 17 | 19 | 38 | 43 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 66 | 70 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /ryvencore_qt/resources/node_expand_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 38 | 43 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 66 | 70 | 74 | 75 | 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 | image/svg+xml 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 | 19 | 40 | 42 | 50 | 54 | 55 | 63 | 67 | 68 | 76 | 80 | 81 | 82 | 86 | 94 | 99 | 112 | 113 | 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 | --------------------------------------------------------------------------------