├── .gitignore ├── LICENSE ├── README.md ├── depthai_pipeline_graph ├── NodeGraphQt │ ├── __init__.py │ ├── base │ │ ├── __init__.py │ │ ├── commands.py │ │ ├── factory.py │ │ ├── graph.py │ │ ├── graph_actions.py │ │ ├── menu.py │ │ ├── model.py │ │ ├── node.py │ │ └── port.py │ ├── constants.py │ ├── custom_widgets │ │ ├── __init__.py │ │ ├── nodes_palette.py │ │ ├── nodes_tree.py │ │ ├── properties.py │ │ └── properties_bin.py │ ├── errors.py │ ├── nodes │ │ ├── __init__.py │ │ ├── backdrop_node.py │ │ ├── base_node.py │ │ ├── group_node.py │ │ └── port_node.py │ ├── pkg_info.py │ ├── qgraphics │ │ ├── __init__.py │ │ ├── node_abstract.py │ │ ├── node_backdrop.py │ │ ├── node_base.py │ │ ├── node_group.py │ │ ├── node_overlay_disabled.py │ │ ├── node_port_in.py │ │ ├── node_port_out.py │ │ ├── node_text_item.py │ │ ├── pipe.py │ │ ├── port.py │ │ └── slicer.py │ ├── trace_event.py │ └── widgets │ │ ├── __init__.py │ │ ├── actions.py │ │ ├── dialogs.py │ │ ├── icons │ │ └── node_base.png │ │ ├── node_graph.py │ │ ├── node_widgets.py │ │ ├── scene.py │ │ ├── tab_search.py │ │ ├── viewer.py │ │ └── viewer_nav.py ├── __init__.py └── pipeline_graph.py ├── media ├── age-gender-demo.jpg ├── pipeline_graph_naming.png └── ports.png ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 geaxgx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DepthAI Pipeline Graph 2 | 3 | A tool that dynamically creates graphs of [DepthAI pipelines](https://docs.luxonis.com/projects/api/en/latest/components/pipeline/). It is an **ideal tool for debugging**, as it provides insight into the pipeline and its inner workings. The original author of this tool is [geaxgx](https://github.com/geaxgx), Luxonis has updated some features and added FPS counting. 4 | 5 | ![Graph of Age-Gender demo](https://github.com/luxonis/depthai_pipeline_graph/assets/18037362/dde3b10f-c006-456c-8927-366b5afce508) 6 | 7 | ## How it works ? 8 | In the DepthAI context, a pipeline is a collection of nodes and links between them. 9 | In your code, after defining your pipeline, you usually pass your pipeline to the device, with a call similar to: 10 | ``` 11 | device.startPipeline(pipeline) 12 | ``` 13 | or 14 | ``` 15 | with dai.Device(pipeline) as device: 16 | ``` 17 | What happens then, is that the pipeline configuration gets serialized to JSON and sent to the OAK device. If the environment variable `DEPTHAI_LEVEL` is set to `debug` before running your code, the content of the JSON config is printed to the console like below: 18 | ``` 19 | [2022-06-01 16:47:33.721] [debug] Schema dump: {"connections":[{"node1Id":8,"node1Output":"passthroughDepth","node1OutputGroup":"","node2Id":10 20 | ,"node2Input":"in","node2InputGroup":""},{"node1Id":8,"node1Output":"out","node1OutputGroup":"","node2Id":9,"node2Input":"in","node2InputGroup":""},{"node1Id":7,"node1Output":"depth","node1OutputGroup":"","node2Id":8,"node2Input":"inputDepth","node2InputGroup":""},{"node1Id":0,"node1Output":"preview","node1OutputGroup":"","node2Id":8,"node2Input":"in","node2InputGroup":""},{"node1Id":0,"node1Output":"preview","node1OutputGroup":"","node2Id":3,"node2Input":"inputImage","node2InputGroup":""}, 21 | ... 22 | ``` 23 | By analyzing the printed schema dump, it is then possible to retrieve the nodes of the pipeline and their connections. That's exactly what the tool `pipeline_graph` is doing: 24 | * it sets `DEPTHAI_LEVEL` to `debug`, 25 | * it runs your code, 26 | * it catches the schema dump in the ouput stream of the code (by default the tool then terminates the process running your code), 27 | * it parses the schema dump and creates the corresponding graph using a modified version of the [NodeGraphQt](https://github.com/jchanvfx/NodeGraphQt) framework. 28 |
29 | 30 | 31 | 32 | 33 | ## Install 34 | 35 | ``` 36 | pip install git+https://github.com/luxonis/depthai_pipeline_graph.git 37 | ``` 38 | If not already present, the command above will install the python module Qt.py. Qt.py enables you to write software that runs on any of the 4 supported bindings - PySide2, PyQt5, PySide and PyQt4. If none of these binding is installed, you will get an error message when running `pipeline_graph`: 39 | ``` 40 | ImportError: No Qt binding were found. 41 | ``` 42 | If you don't have a preference for any particular binding, you can just choose to install PySide2: 43 | ``` 44 | pip install PySide2 45 | ``` 46 | 47 | 48 | ## Run 49 | 50 | Once the installation is done, you have only one command to remember and use: `pipeline_graph` 51 | ``` 52 | > pipeline_graph -h 53 | usage: pipeline_graph [-h] {run,from_file,load} ... 54 | 55 | positional arguments: 56 | {run,from_file,load} Action 57 | run Run your depthai program to create the corresponding 58 | pipeline graph 59 | from_file Create the pipeline graph by parsing a file containing 60 | the schema (log file generated with 61 | DEPTHAI_LEVEL=debug or Json file generated by 62 | pipeline.serializeToJSon()) 63 | load Load a previously saved pipeline graph 64 | 65 | optional arguments: 66 | -h, --help show this help message and exit 67 | ``` 68 | 69 | There are 3 actions/modes available: `run`, `from_file` and `load` 70 | 71 | ### 1) pipeline_graph run 72 | 73 | 74 | ``` 75 | > pipeline_graph run -h 76 | usage: pipeline_graph run [-h] [-dnk] [-var] [-p PIPELINE_NAME] [-v] command 77 | 78 | positional arguments: 79 | command The command with its arguments between ' or " (ex: 80 | python script.py -i file) 81 | 82 | optional arguments: 83 | -h, --help show this help message and exit 84 | -dnk, --do_not_kill Don't terminate the command when the schema string has 85 | been retrieved 86 | -var, --use_variable_names 87 | Use the variable names from the python code to name 88 | the graph nodes 89 | -p PIPELINE_NAME, --pipeline_name PIPELINE_NAME 90 | Name of the pipeline variable in the python code 91 | (default=pipeline) 92 | -v, --verbose Show on the console the command output 93 | 94 | ``` 95 | 96 | As an example, let's say that the usual command to run your program is: 97 | ``` 98 | python main.py -cam 99 | ``` 100 | To build the corresponding pipeline graph, you simply use the following command (the quotes are important): 101 | ``` 102 | pipeline_graph "python main.py -cam" 103 | ``` 104 | Note that as soon as the `pipeline_graph` program has catched the schema dump, your program is forced to be terminated. It means that your program will possibly not have time to display any windows (if it is something it is normally doing). 105 | If you prefer to let your program continue its normal job, use the "-dnk" or "--do_not_kill" argument: 106 | ``` 107 | pipeline_graph "python main.py -cam" -dnk 108 | ``` 109 | Note how the `pipeline_graph` own arguments are placed outside the pair of quotes, whereas your program arguments are inside. 110 |
When using the `-dnk` option, the pipeline graph is displayed only after you quit your program. 111 | 112 | 113 | 114 | **Nodes** 115 | 116 | By default, the node name in the grapth is its type (ColorCamera, ImageManip, NeuralNetwork,...) plus a index between parenthesis that corresponds to the order of creation of the node in the pipeline. For example, if the rgb camera is the first node you create, its name will be `"ColorCamera (0)"`. 117 |
118 | When you have a lot of nodes of the same type, the index is not very helpful to distinguish between nodes. You can then used the `-var` or `--use_variable_names` argument to get a more meaningful name: the index number is replaced by the node variable name used in your code. The tool must know the variable name of the pipeline. "pipeline" is the default. Use the `-p` or `--pipeline_name` argument to specify another name. 119 | 120 |

Graph of the Human Machine Safety demo

121 | 122 | Note that when using this option, `pipeline_graph` will significantly run slower to build the graph (it relies on the python module "trace"). 123 | 124 | **Ports** 125 | 126 | For the input ports, the color represents the blocking state of the port (orange for blocking, green for non-blocking), and the number between [] corresponds to the queue size. 127 | 128 |

Graph of the Human Machine Safety demo

129 |
130 | Once the graph is displayed, a few operations are possible. 131 | You will probably reorganize the nodes by moving them if you are not satisfied with the proposed auto-layout. You can rename a node by directly double-clicking its name. If you double-click in a node (apart the name zone), a window opens which lets you change the node color and its name. 132 | By right-clicking in the graph, a menu appears: you can save your graph in a json file, load a previously save graph or do a few other self-explanatory operations.
133 |
134 | 135 | ### 2) pipeline_graph from_file 136 | Use this command when the "pipeline_graph run" method fails (for instance, if your application is executed as a subprocess) or you don't have access to the application code (so you cannot run it) but the owner of the code send you a log file containing an execution trace.
137 | Two type of files can be parsed by this command: 138 | * a log file generated by setting the environment variable `DEPTHAI_LEVEL` to `debug`, then running the code and saving the standard output: the tool will parse the log file exactly like it does with the "pipeline_graph run" method, looking for the "Schema dump:" string; 139 | * a Json file generated by a call to [pipeline.serializeToJson()](https://docs.luxonis.com/projects/api/en/latest/references/python/#depthai.Pipeline.serializeToJson). 140 | 141 | ``` 142 | > pipeline_graph from_file -h 143 | usage: pipeline_graph from_file [-h] schema_file 144 | 145 | positional arguments: 146 | schema_file Path of the file containing the schema 147 | 148 | optional arguments: 149 | -h, --help show this help message and exit 150 | 151 | ``` 152 | 153 | ### 3) pipeline_graph load 154 | Use this command to open and edit a previously saved pipeline graph. 155 | 156 | ``` 157 | > pipeline_graph load -h 158 | usage: pipeline_graph load [-h] json_file 159 | 160 | positional arguments: 161 | json_file Path of the .json file 162 | 163 | optional arguments: 164 | -h, --help show this help message and exit 165 | 166 | ``` 167 | 168 | ## Credits 169 | * [NodeGraphQt](https://github.com/jchanvfx/NodeGraphQt) by jchanvfx. 170 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # (c) 2017, Johnny Chan and some awesome contributors (^_^) 5 | # https://github.com/jchanvfx/NodeGraphQt/graphs/contributors 6 | 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | 10 | # * Redistributions of source code must retain the above copyright notice, 11 | # this list of conditions and the following disclaimer. 12 | 13 | # * Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | 17 | # * Neither the name Johnny Chan nor the names of its contributors 18 | # may be used to endorse or promote products derived from this software 19 | # without specific prior written permission. 20 | 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 24 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 25 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 26 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 27 | # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 28 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 29 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 30 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 31 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | """ 33 | **NodeGraphQt** is a node graph framework that can be implemented and re purposed 34 | into applications that supports **PySide2**. 35 | 36 | project: https://github.com/jchanvfx/NodeGraphQt 37 | documentation: https://jchanvfx.github.io/NodeGraphQt/api/html/index.html 38 | 39 | example code: 40 | 41 | .. code-block:: python 42 | :linenos: 43 | 44 | from NodeGraphQt import QtWidgets, NodeGraph, BaseNode 45 | 46 | 47 | class MyNode(BaseNode): 48 | 49 | __identifier__ = 'com.chantasticvfx' 50 | NODE_NAME = 'My Node' 51 | 52 | def __init__(self): 53 | super(MyNode, self).__init__() 54 | self.add_input('foo', color=(180, 80, 0)) 55 | self.add_output('bar') 56 | 57 | if __name__ == '__main__': 58 | app = QtWidgets.QApplication([]) 59 | graph = NodeGraph() 60 | 61 | graph.register_node(BaseNode) 62 | graph.register_node(BackdropNode) 63 | 64 | backdrop = graph.create_node('nodeGraphQt.nodes.Backdrop', name='Backdrop') 65 | node_a = graph.create_node('com.chantasticvfx.MyNode', name='Node A') 66 | node_b = graph.create_node('com.chantasticvfx.MyNode', name='Node B', color='#5b162f') 67 | 68 | node_a.set_input(0, node_b.output(0)) 69 | 70 | viewer = graph.viewer() 71 | viewer.show() 72 | 73 | app.exec_() 74 | """ 75 | from .pkg_info import __version__ as VERSION 76 | from .pkg_info import __license__ as LICENSE 77 | 78 | # node graph 79 | from .base.graph import NodeGraph, SubGraph 80 | from .base.menu import NodesMenu, NodeGraphMenu, NodeGraphCommand 81 | 82 | # nodes & ports 83 | from .base.port import Port 84 | from .base.node import NodeObject 85 | from .nodes.base_node import BaseNode 86 | from .nodes.backdrop_node import BackdropNode 87 | from .nodes.group_node import GroupNode 88 | 89 | # widgets 90 | from .widgets.node_widgets import NodeBaseWidget 91 | from .custom_widgets.nodes_tree import NodesTreeWidget 92 | from .custom_widgets.nodes_palette import NodesPaletteWidget 93 | from .custom_widgets.properties_bin import PropertiesBinWidget 94 | 95 | 96 | __version__ = VERSION 97 | __all__ = [ 98 | 'BackdropNode', 99 | 'BaseNode', 100 | 'GroupNode', 101 | 'LICENSE', 102 | 'NodeBaseWidget', 103 | 'NodeGraph', 104 | 'NodeGraphCommand', 105 | 'NodeGraphMenu', 106 | 'NodeObject', 107 | 'NodesPaletteWidget', 108 | 'NodesTreeWidget', 109 | 'NodesMenu', 110 | 'Port', 111 | 'PropertiesBinWidget', 112 | 'SubGraph', 113 | 'VERSION', 114 | 'constants', 115 | 'custom_widgets' 116 | ] 117 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/base/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # (c) 2017, Johnny Chan 4 | # https://github.com/jchanvfx/NodeGraphQt 5 | 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | 9 | # * Redistributions of source code must retain the above copyright notice, 10 | # this list of conditions and the following disclaimer. 11 | 12 | # * Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | 16 | # * Neither the name of the Johnny Chan nor the names of its contributors 17 | # may be used to endorse or promote products derived from this software 18 | # without specific prior written permission. 19 | 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 23 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 24 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 25 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 26 | # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 27 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 28 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 29 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 30 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/base/commands.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from Qt import QtWidgets 3 | 4 | from ..constants import PortTypeEnum 5 | 6 | 7 | class PropertyChangedCmd(QtWidgets.QUndoCommand): 8 | """ 9 | Node property changed command. 10 | 11 | Args: 12 | node (NodeGraphQt.NodeObject): node. 13 | name (str): node property name. 14 | value (object): node property value. 15 | """ 16 | 17 | def __init__(self, node, name, value): 18 | QtWidgets.QUndoCommand.__init__(self) 19 | if name == 'name': 20 | self.setText('renamed "{}" to "{}"'.format(node.name(), value)) 21 | else: 22 | self.setText('property "{}:{}"'.format(node.name(), name)) 23 | self.node = node 24 | self.name = name 25 | self.old_val = node.get_property(name) 26 | self.new_val = value 27 | 28 | def set_node_prop(self, name, value): 29 | """ 30 | updates the node view and model. 31 | """ 32 | # set model data. 33 | model = self.node.model 34 | model.set_property(name, value) 35 | 36 | # set view data. 37 | view = self.node.view 38 | 39 | # view widgets. 40 | if hasattr(view, 'widgets') and name in view.widgets.keys(): 41 | # check if previous value is identical to current value, 42 | # prevent signals from causing a infinite loop. 43 | if view.widgets[name].get_value() != value: 44 | view.widgets[name].set_value(value) 45 | 46 | # view properties. 47 | if name in view.properties.keys(): 48 | # remap "pos" to "xy_pos" node view has pre-existing pos method. 49 | if name == 'pos': 50 | name = 'xy_pos' 51 | setattr(view, name, value) 52 | 53 | def undo(self): 54 | if self.old_val != self.new_val: 55 | self.set_node_prop(self.name, self.old_val) 56 | 57 | # emit property changed signal. 58 | graph = self.node.graph 59 | graph.property_changed.emit(self.node, self.name, self.old_val) 60 | 61 | def redo(self): 62 | if self.old_val != self.new_val: 63 | self.set_node_prop(self.name, self.new_val) 64 | 65 | # emit property changed signal. 66 | graph = self.node.graph 67 | graph.property_changed.emit(self.node, self.name, self.new_val) 68 | 69 | 70 | class NodeMovedCmd(QtWidgets.QUndoCommand): 71 | """ 72 | Node moved command. 73 | 74 | Args: 75 | node (NodeGraphQt.NodeObject): node. 76 | pos (tuple(float, float)): new node position. 77 | prev_pos (tuple(float, float)): previous node position. 78 | """ 79 | 80 | def __init__(self, node, pos, prev_pos): 81 | QtWidgets.QUndoCommand.__init__(self) 82 | self.node = node 83 | self.pos = pos 84 | self.prev_pos = prev_pos 85 | 86 | def undo(self): 87 | self.node.view.xy_pos = self.prev_pos 88 | self.node.model.pos = self.prev_pos 89 | 90 | def redo(self): 91 | if self.pos == self.prev_pos: 92 | return 93 | self.node.view.xy_pos = self.pos 94 | self.node.model.pos = self.pos 95 | 96 | 97 | class NodeAddedCmd(QtWidgets.QUndoCommand): 98 | """ 99 | Node added command. 100 | 101 | Args: 102 | graph (NodeGraphQt.NodeGraph): node graph. 103 | node (NodeGraphQt.NodeObject): node. 104 | pos (tuple(float, float)): initial node position (optional). 105 | """ 106 | 107 | def __init__(self, graph, node, pos=None): 108 | QtWidgets.QUndoCommand.__init__(self) 109 | self.setText('added node') 110 | self.viewer = graph.viewer() 111 | self.model = graph.model 112 | self.node = node 113 | self.pos = pos 114 | 115 | def undo(self): 116 | self.pos = self.pos or self.node.pos() 117 | self.model.nodes.pop(self.node.id) 118 | self.node.view.delete() 119 | 120 | def redo(self): 121 | self.model.nodes[self.node.id] = self.node 122 | self.viewer.add_node(self.node.view, self.pos) 123 | 124 | 125 | class NodeRemovedCmd(QtWidgets.QUndoCommand): 126 | """ 127 | Node deleted command. 128 | 129 | Args: 130 | graph (NodeGraphQt.NodeGraph): node graph. 131 | node (NodeGraphQt.BaseNode or NodeGraphQt.NodeObject): node. 132 | """ 133 | 134 | def __init__(self, graph, node): 135 | QtWidgets.QUndoCommand.__init__(self) 136 | self.setText('deleted node') 137 | self.scene = graph.scene() 138 | self.model = graph.model 139 | self.node = node 140 | 141 | def undo(self): 142 | self.model.nodes[self.node.id] = self.node 143 | self.scene.addItem(self.node.view) 144 | 145 | def redo(self): 146 | self.model.nodes.pop(self.node.id) 147 | self.node.view.delete() 148 | 149 | 150 | class NodeInputConnectedCmd(QtWidgets.QUndoCommand): 151 | """ 152 | "BaseNode.on_input_connected()" command. 153 | 154 | Args: 155 | src_port (NodeGraphQt.Port): source port. 156 | trg_port (NodeGraphQt.Port): target port. 157 | """ 158 | 159 | def __init__(self, src_port, trg_port): 160 | QtWidgets.QUndoCommand.__init__(self) 161 | if src_port.type_() == PortTypeEnum.IN.value: 162 | self.source = src_port 163 | self.target = trg_port 164 | else: 165 | self.source = trg_port 166 | self.target = src_port 167 | 168 | def undo(self): 169 | node = self.source.node() 170 | node.on_input_disconnected(self.source, self.target) 171 | 172 | def redo(self): 173 | node = self.source.node() 174 | node.on_input_connected(self.source, self.target) 175 | 176 | 177 | class NodeInputDisconnectedCmd(QtWidgets.QUndoCommand): 178 | """ 179 | Node "on_input_disconnected()" command. 180 | 181 | Args: 182 | src_port (NodeGraphQt.Port): source port. 183 | trg_port (NodeGraphQt.Port): target port. 184 | """ 185 | 186 | def __init__(self, src_port, trg_port): 187 | QtWidgets.QUndoCommand.__init__(self) 188 | if src_port.type_() == PortTypeEnum.IN.value: 189 | self.source = src_port 190 | self.target = trg_port 191 | else: 192 | self.source = trg_port 193 | self.target = src_port 194 | 195 | def undo(self): 196 | node = self.source.node() 197 | node.on_input_connected(self.source, self.target) 198 | 199 | def redo(self): 200 | node = self.source.node() 201 | node.on_input_disconnected(self.source, self.target) 202 | 203 | 204 | class PortConnectedCmd(QtWidgets.QUndoCommand): 205 | """ 206 | Port connected command. 207 | 208 | Args: 209 | src_port (NodeGraphQt.Port): source port. 210 | trg_port (NodeGraphQt.Port): target port. 211 | """ 212 | 213 | def __init__(self, src_port, trg_port): 214 | QtWidgets.QUndoCommand.__init__(self) 215 | self.source = src_port 216 | self.target = trg_port 217 | 218 | def undo(self): 219 | src_model = self.source.model 220 | trg_model = self.target.model 221 | src_id = self.source.node().id 222 | trg_id = self.target.node().id 223 | 224 | port_names = src_model.connected_ports.get(trg_id) 225 | if port_names is []: 226 | del src_model.connected_ports[trg_id] 227 | if port_names and self.target.name() in port_names: 228 | port_names.remove(self.target.name()) 229 | 230 | port_names = trg_model.connected_ports.get(src_id) 231 | if port_names is []: 232 | del trg_model.connected_ports[src_id] 233 | if port_names and self.source.name() in port_names: 234 | port_names.remove(self.source.name()) 235 | 236 | self.source.view.disconnect_from(self.target.view) 237 | 238 | def redo(self): 239 | src_model = self.source.model 240 | trg_model = self.target.model 241 | src_id = self.source.node().id 242 | trg_id = self.target.node().id 243 | 244 | src_model.connected_ports[trg_id].append(self.target.name()) 245 | trg_model.connected_ports[src_id].append(self.source.name()) 246 | 247 | return self.source.view.connect_to(self.target.view) 248 | 249 | 250 | class PortDisconnectedCmd(QtWidgets.QUndoCommand): 251 | """ 252 | Port disconnected command. 253 | 254 | Args: 255 | src_port (NodeGraphQt.Port): source port. 256 | trg_port (NodeGraphQt.Port): target port. 257 | """ 258 | 259 | def __init__(self, src_port, trg_port): 260 | QtWidgets.QUndoCommand.__init__(self) 261 | self.source = src_port 262 | self.target = trg_port 263 | 264 | def undo(self): 265 | src_model = self.source.model 266 | trg_model = self.target.model 267 | src_id = self.source.node().id 268 | trg_id = self.target.node().id 269 | 270 | src_model.connected_ports[trg_id].append(self.target.name()) 271 | trg_model.connected_ports[src_id].append(self.source.name()) 272 | 273 | self.source.view.connect_to(self.target.view) 274 | 275 | def redo(self): 276 | src_model = self.source.model 277 | trg_model = self.target.model 278 | src_id = self.source.node().id 279 | trg_id = self.target.node().id 280 | 281 | port_names = src_model.connected_ports.get(trg_id) 282 | if port_names is []: 283 | del src_model.connected_ports[trg_id] 284 | if port_names and self.target.name() in port_names: 285 | port_names.remove(self.target.name()) 286 | 287 | port_names = trg_model.connected_ports.get(src_id) 288 | if port_names is []: 289 | del trg_model.connected_ports[src_id] 290 | if port_names and self.source.name() in port_names: 291 | port_names.remove(self.source.name()) 292 | 293 | self.source.view.disconnect_from(self.target.view) 294 | 295 | 296 | class PortLockedCmd(QtWidgets.QUndoCommand): 297 | """ 298 | Port locked command. 299 | 300 | Args: 301 | port (NodeGraphQt.Port): node port. 302 | """ 303 | 304 | def __init__(self, port): 305 | QtWidgets.QUndoCommand.__init__(self) 306 | self.setText('lock port "{}"'.format(port.name())) 307 | self.port = port 308 | 309 | def undo(self): 310 | self.port.model.locked = False 311 | self.port.view.locked = False 312 | 313 | def redo(self): 314 | self.port.model.locked = True 315 | self.port.view.locked = True 316 | 317 | 318 | class PortUnlockedCmd(QtWidgets.QUndoCommand): 319 | """ 320 | Port unlocked command. 321 | 322 | Args: 323 | port (NodeGraphQt.Port): node port. 324 | """ 325 | 326 | def __init__(self, port): 327 | QtWidgets.QUndoCommand.__init__(self) 328 | self.setText('unlock port "{}"'.format(port.name())) 329 | self.port = port 330 | 331 | def undo(self): 332 | self.port.model.locked = True 333 | self.port.view.locked = True 334 | 335 | def redo(self): 336 | self.port.model.locked = False 337 | self.port.view.locked = False 338 | 339 | 340 | class PortVisibleCmd(QtWidgets.QUndoCommand): 341 | """ 342 | Port visibility command. 343 | 344 | Args: 345 | port (NodeGraphQt.Port): node port. 346 | """ 347 | 348 | def __init__(self, port): 349 | QtWidgets.QUndoCommand.__init__(self) 350 | self.port = port 351 | self.visible = port.visible() 352 | 353 | def set_visible(self, visible): 354 | self.port.model.visible = visible 355 | self.port.view.setVisible(visible) 356 | node_view = self.port.node().view 357 | text_item = None 358 | if self.port.type_() == PortTypeEnum.IN.value: 359 | text_item = node_view.get_input_text_item(self.port.view) 360 | elif self.port.type_() == PortTypeEnum.OUT.value: 361 | text_item = node_view.get_output_text_item(self.port.view) 362 | if text_item: 363 | text_item.setVisible(visible) 364 | node_view.post_init() 365 | 366 | def undo(self): 367 | self.set_visible(not self.visible) 368 | 369 | def redo(self): 370 | self.set_visible(self.visible) 371 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/base/factory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from ..errors import NodeRegistrationError 3 | 4 | 5 | class NodeFactory(object): 6 | """ 7 | Node factory that stores all the node types. 8 | """ 9 | 10 | def __init__(self): 11 | self.__aliases = {} 12 | self.__names = {} 13 | self.__nodes = {} 14 | 15 | @property 16 | def names(self): 17 | """ 18 | Return all currently registered node type identifiers. 19 | 20 | Returns: 21 | dict: key='.format( 34 | self.__class__.__name__, self.name(), hex(id(self))) 35 | 36 | @property 37 | def qmenu(self): 38 | """ 39 | The underlying qmenu. 40 | 41 | Returns: 42 | BaseMenu: qmenu object. 43 | """ 44 | return self._qmenu 45 | 46 | def name(self): 47 | """ 48 | Returns the name for the menu. 49 | 50 | Returns: 51 | str: label name. 52 | """ 53 | return self.qmenu.title() 54 | 55 | def get_menu(self, name): 56 | """ 57 | Returns the child menu by name. 58 | 59 | Args: 60 | name (str): name of the menu. 61 | 62 | Returns: 63 | NodeGraphQt.NodeGraphMenu: menu item. 64 | """ 65 | menu = self.qmenu.get_menu(name) 66 | if menu: 67 | return NodeGraphMenu(self._graph, menu) 68 | 69 | def get_command(self, name): 70 | """ 71 | Returns the child menu command by name. 72 | 73 | Args: 74 | name (str): name of the command. 75 | 76 | Returns: 77 | NodeGraphQt.MenuCommand: context menu command. 78 | """ 79 | for action in self.qmenu.actions(): 80 | if not action.menu() and action.text() == name: 81 | return NodeGraphCommand(self._graph, action) 82 | 83 | def all_commands(self): 84 | """ 85 | Returns all child and sub child commands from the current context menu. 86 | 87 | Returns: 88 | list[NodeGraphQt.MenuCommand]: list of commands. 89 | """ 90 | def get_actions(menu): 91 | actions = [] 92 | for action in menu.actions(): 93 | if not action.menu(): 94 | if not action.isSeparator(): 95 | actions.append(action) 96 | else: 97 | actions += get_actions(action.menu()) 98 | return actions 99 | child_actions = get_actions(self.qmenu) 100 | return [NodeGraphCommand(self._graph, a) for a in child_actions] 101 | 102 | def add_menu(self, name): 103 | """ 104 | Adds a child menu to the current menu. 105 | 106 | Args: 107 | name (str): menu name. 108 | 109 | Returns: 110 | NodeGraphQt.NodeGraphMenu: the appended menu item. 111 | """ 112 | menu = BaseMenu(name, self.qmenu) 113 | self.qmenu.addMenu(menu) 114 | return NodeGraphMenu(self._graph, menu) 115 | 116 | def add_command(self, name, func=None, shortcut=None): 117 | """ 118 | Adds a command to the menu. 119 | 120 | Args: 121 | name (str): command name. 122 | func (function): command function eg. "func(``graph``)". 123 | shortcut (str): shotcut key. 124 | 125 | Returns: 126 | NodeGraphQt.NodeGraphCommand: the appended command. 127 | """ 128 | action = GraphAction(name, self._graph.viewer()) 129 | action.graph = self._graph 130 | if LooseVersion(QtCore.qVersion()) >= LooseVersion('5.10'): 131 | action.setShortcutVisibleInContextMenu(True) 132 | if shortcut: 133 | action.setShortcut(shortcut) 134 | if func: 135 | action.executed.connect(func) 136 | qaction = self.qmenu.addAction(action) 137 | return NodeGraphCommand(self._graph, qaction) 138 | 139 | def add_separator(self): 140 | """ 141 | Adds a separator to the menu. 142 | """ 143 | self.qmenu.addSeparator() 144 | 145 | 146 | class NodesMenu(NodeGraphMenu): 147 | """ 148 | The ``NodesMenu`` is the context menu triggered from a node. 149 | 150 | **Inherited from:** :class:`NodeGraphQt.NodeGraphMenu` 151 | 152 | example for accessing the nodes context menu. 153 | 154 | .. code-block:: python 155 | :linenos: 156 | 157 | from NodeGraphQt import NodeGraph 158 | 159 | node_graph = NodeGraph() 160 | 161 | # get the nodes context menu. 162 | nodes_menu = node_graph.get_context_menu('nodes') 163 | """ 164 | 165 | def add_command(self, name, func=None, node_type=None, node_class=None): 166 | """ 167 | Re-implemented to add a command to the specified node type menu. 168 | 169 | Args: 170 | name (str): command name. 171 | func (function): command function eg. "func(``graph``, ``node``)". 172 | node_type (str): specified node type for the command. 173 | node_class (class): specified node class for the command. 174 | 175 | Returns: 176 | NodeGraphQt.NodeGraphCommand: the appended command. 177 | """ 178 | if not node_type and not node_class: 179 | raise NodeMenuError('Node type or Node class not specified!') 180 | 181 | if node_class: 182 | node_type = node_class.__name__ 183 | 184 | node_menu = self.qmenu.get_menu(node_type) 185 | if not node_menu: 186 | node_menu = BaseMenu(node_type, self.qmenu) 187 | 188 | if node_class: 189 | node_menu.node_class = node_class 190 | node_menu.graph = self._graph 191 | 192 | self.qmenu.addMenu(node_menu) 193 | 194 | if not self.qmenu.isEnabled(): 195 | self.qmenu.setDisabled(False) 196 | 197 | action = NodeAction(name, self._graph.viewer()) 198 | action.graph = self._graph 199 | if LooseVersion(QtCore.qVersion()) >= LooseVersion('5.10'): 200 | action.setShortcutVisibleInContextMenu(True) 201 | if func: 202 | action.executed.connect(func) 203 | 204 | if node_class: 205 | node_menus = self.qmenu.get_menus(node_class) 206 | if node_menu in node_menus: 207 | node_menus.remove(node_menu) 208 | for menu in node_menus: 209 | menu.addAction(action) 210 | 211 | qaction = node_menu.addAction(action) 212 | return NodeGraphCommand(self._graph, qaction) 213 | 214 | 215 | class NodeGraphCommand(object): 216 | """ 217 | Node graph menu command. 218 | """ 219 | 220 | def __init__(self, graph, qaction): 221 | self._graph = graph 222 | self._qaction = qaction 223 | 224 | def __repr__(self): 225 | return '<{}("{}") object at {}>'.format( 226 | self.__class__.__name__, self.name(), hex(id(self))) 227 | 228 | @property 229 | def qaction(self): 230 | """ 231 | The underlying qaction. 232 | 233 | Returns: 234 | GraphAction: qaction object. 235 | """ 236 | return self._qaction 237 | 238 | def name(self): 239 | """ 240 | Returns the name for the menu command. 241 | 242 | Returns: 243 | str: label name. 244 | """ 245 | return self.qaction.text() 246 | 247 | def set_shortcut(self, shortcut=None): 248 | """ 249 | Sets the shortcut key combination for the menu command. 250 | 251 | Args: 252 | shortcut (str): shortcut key. 253 | """ 254 | shortcut = shortcut or QtGui.QKeySequence() 255 | self.qaction.setShortcut(shortcut) 256 | 257 | def run_command(self): 258 | """ 259 | execute the menu command. 260 | """ 261 | self.qaction.trigger() 262 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/base/port.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from ..base.commands import ( 3 | PortConnectedCmd, 4 | PortDisconnectedCmd, 5 | PortLockedCmd, 6 | PortUnlockedCmd, 7 | PortVisibleCmd, 8 | NodeInputConnectedCmd, 9 | NodeInputDisconnectedCmd 10 | ) 11 | from ..base.model import PortModel 12 | from ..constants import PortTypeEnum 13 | from ..errors import PortError 14 | 15 | 16 | class Port(object): 17 | """ 18 | The ``Port`` class is used for connecting one node to another. 19 | 20 | .. image:: ../_images/port.png 21 | :width: 50% 22 | 23 | See Also: 24 | For adding a ports into a node see: 25 | :meth:`BaseNode.add_input`, :meth:`BaseNode.add_output` 26 | 27 | Args: 28 | node (NodeGraphQt.NodeObject): parent node. 29 | port (PortItem): graphic item used for drawing. 30 | """ 31 | 32 | def __init__(self, node, port): 33 | self.__view = port 34 | self.__model = PortModel(node) 35 | 36 | def __repr__(self): 37 | port = str(self.__class__.__name__) 38 | return '<{}("{}") object at {}>'.format( 39 | port, self.name(), hex(id(self))) 40 | 41 | @property 42 | def view(self): 43 | """ 44 | Returns the :class:`QtWidgets.QGraphicsItem` used in the scene. 45 | 46 | Returns: 47 | NodeGraphQt.qgraphics.port.PortItem: port item. 48 | """ 49 | return self.__view 50 | 51 | @property 52 | def model(self): 53 | """ 54 | Returns the port model. 55 | 56 | Returns: 57 | NodeGraphQt.base.model.PortModel: port model. 58 | """ 59 | return self.__model 60 | 61 | def type_(self): 62 | """ 63 | Returns the port type. 64 | 65 | Port Types: 66 | - :attr:`NodeGraphQt.constants.IN_PORT` for input port 67 | - :attr:`NodeGraphQt.constants.OUT_PORT` for output port 68 | 69 | Returns: 70 | str: port connection type. 71 | """ 72 | return self.model.type_ 73 | 74 | def multi_connection(self): 75 | """ 76 | Returns if the ports is a single connection or not. 77 | 78 | Returns: 79 | bool: false if port is a single connection port 80 | """ 81 | return self.model.multi_connection 82 | 83 | def node(self): 84 | """ 85 | Return the parent node. 86 | 87 | Returns: 88 | NodeGraphQt.BaseNode: parent node object. 89 | """ 90 | return self.model.node 91 | 92 | def name(self): 93 | """ 94 | Returns the port name. 95 | 96 | Returns: 97 | str: port name. 98 | """ 99 | return self.model.name 100 | 101 | def visible(self): 102 | """ 103 | Port visible in the node graph. 104 | 105 | Returns: 106 | bool: true if visible. 107 | """ 108 | return self.model.visible 109 | 110 | def set_visible(self, visible=True): 111 | """ 112 | Sets weather the port should be visible or not. 113 | 114 | Args: 115 | visible (bool): true if visible. 116 | """ 117 | self.model.visible = visible 118 | label = 'show' if visible else 'hide' 119 | undo_stack = self.node().graph.undo_stack() 120 | undo_stack.beginMacro('{} port {}'.format(label, self.name())) 121 | 122 | for port in self.connected_ports(): 123 | undo_stack.push(PortDisconnectedCmd(self, port)) 124 | 125 | undo_stack.push(PortVisibleCmd(self)) 126 | undo_stack.endMacro() 127 | 128 | def locked(self): 129 | """ 130 | Returns the locked state. 131 | 132 | If ports are locked then new pipe connections can't be connected 133 | and current connected pipes can't be disconnected. 134 | 135 | Returns: 136 | bool: true if locked. 137 | """ 138 | return self.model.locked 139 | 140 | def lock(self): 141 | """ 142 | Lock the port so new pipe connections can't be connected and 143 | current connected pipes can't be disconnected. 144 | 145 | This is the same as calling :meth:`Port.set_locked` with the arg 146 | set to ``True`` 147 | """ 148 | self.set_locked(True, connected_ports=True) 149 | 150 | def unlock(self): 151 | """ 152 | Unlock the port so new pipe connections can be connected and 153 | existing connected pipes can be disconnected. 154 | 155 | This is the same as calling :meth:`Port.set_locked` with the arg 156 | set to ``False`` 157 | """ 158 | self.set_locked(False, connected_ports=True) 159 | 160 | def set_locked(self, state=False, connected_ports=True, push_undo=True): 161 | """ 162 | Sets the port locked state. When locked pipe connections can't be 163 | connected or disconnected from this port. 164 | 165 | Args: 166 | state (Bool): port lock state. 167 | connected_ports (Bool): apply to lock state to connected ports. 168 | push_undo (bool): register the command to the undo stack. (default: True) 169 | 170 | """ 171 | graph = self.node().graph 172 | undo_stack = graph.undo_stack() 173 | if state: 174 | undo_cmd = PortLockedCmd(self) 175 | else: 176 | undo_cmd = PortUnlockedCmd(self) 177 | if push_undo: 178 | undo_stack.push(undo_cmd) 179 | else: 180 | undo_cmd.redo() 181 | if connected_ports: 182 | for port in self.connected_ports(): 183 | port.set_locked(state, 184 | connected_ports=False, 185 | push_undo=push_undo) 186 | 187 | def connected_ports(self): 188 | """ 189 | Returns all connected ports. 190 | 191 | Returns: 192 | list[NodeGraphQt.Port]: list of connected ports. 193 | """ 194 | ports = [] 195 | graph = self.node().graph 196 | for node_id, port_names in self.model.connected_ports.items(): 197 | for port_name in port_names: 198 | node = graph.get_node_by_id(node_id) 199 | if self.type_() == PortTypeEnum.IN.value: 200 | ports.append(node.outputs()[port_name]) 201 | elif self.type_() == PortTypeEnum.OUT.value: 202 | ports.append(node.inputs()[port_name]) 203 | return ports 204 | 205 | def connect_to(self, port=None, push_undo=True): 206 | """ 207 | Create connection to the specified port and emits the 208 | :attr:`NodeGraph.port_connected` signal from the parent node graph. 209 | 210 | Args: 211 | port (NodeGraphQt.Port): port object. 212 | push_undo (bool): register the command to the undo stack. (default: True) 213 | """ 214 | if not port: 215 | return 216 | 217 | if self in port.connected_ports(): 218 | return 219 | 220 | if self.locked() or port.locked(): 221 | name = [p.name() for p in [self, port] if p.locked()][0] 222 | raise PortError( 223 | 'Can\'t connect port because "{}" is locked.'.format(name)) 224 | 225 | graph = self.node().graph 226 | viewer = graph.viewer() 227 | 228 | if push_undo: 229 | undo_stack = graph.undo_stack() 230 | undo_stack.beginMacro('connect port') 231 | 232 | pre_conn_port = None 233 | src_conn_ports = self.connected_ports() 234 | if not self.multi_connection() and src_conn_ports: 235 | pre_conn_port = src_conn_ports[0] 236 | 237 | 238 | if not port: 239 | if pre_conn_port: 240 | if push_undo: 241 | undo_stack.push(PortDisconnectedCmd(self, port)) 242 | undo_stack.push(NodeInputDisconnectedCmd(self, port)) 243 | undo_stack.endMacro() 244 | else: 245 | PortDisconnectedCmd(self, port).redo() 246 | NodeInputDisconnectedCmd(self, port).redo() 247 | return 248 | 249 | if graph.acyclic() and viewer.acyclic_check(self.view, port.view): 250 | if pre_conn_port: 251 | if push_undo: 252 | undo_stack.push(PortDisconnectedCmd(self, pre_conn_port)) 253 | undo_stack.push(NodeInputDisconnectedCmd( 254 | self, pre_conn_port)) 255 | undo_stack.endMacro() 256 | else: 257 | PortDisconnectedCmd(self, pre_conn_port).redo() 258 | NodeInputDisconnectedCmd(self, pre_conn_port).redo() 259 | return 260 | 261 | trg_conn_ports = port.connected_ports() 262 | if not port.multi_connection() and trg_conn_ports: 263 | dettached_port = trg_conn_ports[0] 264 | if push_undo: 265 | undo_stack.push(PortDisconnectedCmd(port, dettached_port)) 266 | undo_stack.push(NodeInputDisconnectedCmd(port, dettached_port)) 267 | else: 268 | PortDisconnectedCmd(port, dettached_port).redo() 269 | NodeInputDisconnectedCmd(port, dettached_port).redo() 270 | if pre_conn_port: 271 | if push_undo: 272 | undo_stack.push(PortDisconnectedCmd(self, pre_conn_port)) 273 | undo_stack.push(NodeInputDisconnectedCmd(self, pre_conn_port)) 274 | else: 275 | PortDisconnectedCmd(self, pre_conn_port).redo() 276 | NodeInputDisconnectedCmd(self, pre_conn_port).redo() 277 | 278 | 279 | pipe = None 280 | if push_undo: 281 | undo_stack.push(PortConnectedCmd(self, port)) 282 | undo_stack.push(NodeInputConnectedCmd(self, port)) 283 | undo_stack.endMacro() 284 | else: 285 | pipe = PortConnectedCmd(self, port).redo() 286 | NodeInputConnectedCmd(self, port).redo() 287 | 288 | ports = {p.type_(): p for p in [self, port]} 289 | graph.port_connected.emit(ports[PortTypeEnum.IN.value], 290 | ports[PortTypeEnum.OUT.value]) 291 | 292 | return pipe 293 | 294 | def disconnect_from(self, port=None, push_undo=True): 295 | """ 296 | Disconnect from the specified port and emits the 297 | :attr:`NodeGraph.port_disconnected` signal from the parent node graph. 298 | 299 | Args: 300 | port (NodeGraphQt.Port): port object. 301 | push_undo (bool): register the command to the undo stack. (default: True) 302 | """ 303 | if not port: 304 | return 305 | 306 | if self.locked() or port.locked(): 307 | name = [p.name() for p in [self, port] if p.locked()][0] 308 | raise PortError( 309 | 'Can\'t disconnect port because "{}" is locked.'.format(name)) 310 | 311 | graph = self.node().graph 312 | if push_undo: 313 | graph.undo_stack().beginMacro('disconnect port') 314 | graph.undo_stack().push(PortDisconnectedCmd(self, port)) 315 | graph.undo_stack().push(NodeInputDisconnectedCmd(self, port)) 316 | graph.undo_stack().endMacro() 317 | else: 318 | PortDisconnectedCmd(self, port).redo() 319 | NodeInputDisconnectedCmd(self, port).redo() 320 | 321 | # emit "port_disconnected" signal from the parent graph. 322 | ports = {p.type_(): p for p in [self, port]} 323 | graph.port_disconnected.emit(ports[PortTypeEnum.IN.value], 324 | ports[PortTypeEnum.OUT.value]) 325 | 326 | def clear_connections(self, push_undo=True): 327 | """ 328 | Disconnect from all port connections and emit the 329 | :attr:`NodeGraph.port_disconnected` signals from the node graph. 330 | 331 | See Also: 332 | :meth:`Port.disconnect_from`, 333 | :meth:`Port.connect_to`, 334 | :meth:`Port.connected_ports` 335 | 336 | Args: 337 | push_undo (bool): register the command to the undo stack. (default: True) 338 | """ 339 | if self.locked(): 340 | err = 'Can\'t clear connections because port "{}" is locked.' 341 | raise PortError(err.format(self.name())) 342 | 343 | if not self.connected_ports(): 344 | return 345 | 346 | if push_undo: 347 | graph = self.node().graph 348 | undo_stack = graph.undo_stack() 349 | undo_stack.beginMacro('"{}" clear connections') 350 | for cp in self.connected_ports(): 351 | self.disconnect_from(cp) 352 | undo_stack.endMacro() 353 | else: 354 | for cp in self.connected_ports(): 355 | self.disconnect_from(cp, push_undo=False) 356 | 357 | @property 358 | def color(self): 359 | return self.__view.color 360 | 361 | @color.setter 362 | def color(self, color=(0, 0, 0, 255)): 363 | self.__view.color = color 364 | 365 | @property 366 | def border_color(self): 367 | return self.__view.border_color 368 | 369 | @border_color.setter 370 | def border_color(self, color=(0, 0, 0, 255)): 371 | self.__view.border_color = color 372 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/constants.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | 5 | from Qt import QtWidgets 6 | from enum import Enum 7 | 8 | from .pkg_info import __version__ as _v 9 | 10 | __doc__ = """ 11 | | The :py:mod:`NodeGraphQt.constants` namespace contains variables and enums 12 | used throughout the NodeGraphQt library. 13 | """ 14 | 15 | # ================================== PRIVATE =================================== 16 | 17 | URI_SCHEME = 'nodegraphqt://' 18 | URN_SCHEME = 'nodegraphqt::' 19 | 20 | # === PATHS === 21 | 22 | BASE_PATH = os.path.dirname(os.path.abspath(__file__)) 23 | ICON_PATH = os.path.join(BASE_PATH, 'widgets', 'icons') 24 | ICON_DOWN_ARROW = os.path.join(ICON_PATH, 'down_arrow.png') 25 | ICON_NODE_BASE = os.path.join(ICON_PATH, 'node_base.png') 26 | 27 | # === DRAW STACK ORDER === 28 | 29 | Z_VAL_PIPE = -1 30 | Z_VAL_NODE = 1 31 | Z_VAL_PORT = 2 32 | Z_VAL_NODE_WIDGET = 3 33 | 34 | # === ITEM CACHE MODE === 35 | 36 | # QGraphicsItem.NoCache 37 | # QGraphicsItem.DeviceCoordinateCache 38 | # QGraphicsItem.ItemCoordinateCache 39 | 40 | ITEM_CACHE_MODE = QtWidgets.QGraphicsItem.DeviceCoordinateCache 41 | 42 | # === NODE LAYOUT DIRECTION === 43 | 44 | #: Mode for vertical node layout. 45 | NODE_LAYOUT_VERTICAL = 0 46 | #: Mode for horizontal node layout. 47 | NODE_LAYOUT_HORIZONTAL = 1 48 | #: Variable for setting the node layout direction. 49 | # NODE_LAYOUT_DIRECTION = NODE_LAYOUT_VERTICAL 50 | NODE_LAYOUT_DIRECTION = NODE_LAYOUT_HORIZONTAL 51 | 52 | # =================================== GLOBAL =================================== 53 | 54 | 55 | class VersionEnum(Enum): 56 | """ 57 | Current framework version. 58 | :py:mod:`NodeGraphQt.constants.VersionEnum` 59 | """ 60 | #: 61 | VERSION = _v 62 | #: 63 | MAJOR = int(_v.split('.')[0]) 64 | #: 65 | MINOR = int(_v.split('.')[1]) 66 | #: 67 | PATCH = int(_v.split('.')[2]) 68 | 69 | # =================================== VIEWER =================================== 70 | 71 | 72 | class ViewerEnum(Enum): 73 | """ 74 | Node graph viewer styling layout: 75 | :py:mod:`NodeGraphQt.constants.ViewerEnum` 76 | """ 77 | #: default background color for the node graph. 78 | # BACKGROUND_COLOR = (35, 35, 35) 79 | BACKGROUND_COLOR = (155, 155, 200) 80 | #: style node graph background with no grid or dots. 81 | GRID_DISPLAY_NONE = 0 82 | #: style node graph background with dots. 83 | GRID_DISPLAY_DOTS = 1 84 | #: style node graph background with grid lines. 85 | GRID_DISPLAY_LINES = 2 86 | #: grid size when styled with grid lines. 87 | GRID_SIZE = 50 88 | #: grid line color. 89 | GRID_COLOR = (45, 45, 45) 90 | 91 | 92 | class ViewerNavEnum(Enum): 93 | """ 94 | Node graph viewer navigation styling layout: 95 | :py:mod:`NodeGraphQt.constants.ViewerNavEnum` 96 | """ 97 | #: default background color. 98 | BACKGROUND_COLOR = (25, 25, 25) 99 | #: default item color. 100 | ITEM_COLOR = (35, 35, 35) 101 | 102 | # ==================================== NODE ==================================== 103 | 104 | 105 | class NodeEnum(Enum): 106 | """ 107 | Node styling layout: 108 | :py:mod:`NodeGraphQt.constants.NodeEnum` 109 | """ 110 | #: default node width. 111 | WIDTH = 160 112 | #: default node height. 113 | HEIGHT = 60 114 | #: default node icon size (WxH). 115 | ICON_SIZE = 18 116 | #: default node overlay color when selected. 117 | SELECTED_COLOR = (255, 255, 255, 30) 118 | #: default node border color when selected. 119 | # SELECTED_BORDER_COLOR = (254, 207, 42, 255) 120 | SELECTED_BORDER_COLOR = (52, 152, 219, 255 ) 121 | 122 | # ==================================== PORT ==================================== 123 | 124 | 125 | class PortEnum(Enum): 126 | """ 127 | Port styling layout: 128 | :py:mod:`NodeGraphQt.constants.PortEnum` 129 | """ 130 | #: default port size. 131 | SIZE = 22.0 132 | #: default port color. (r, g, b, a) 133 | COLOR = (49, 115, 100, 255) 134 | #: default port border color. 135 | BORDER_COLOR = (29, 202, 151, 255) 136 | #: port color when selected. 137 | ACTIVE_COLOR = (14, 45, 59, 255) 138 | #: port border color when selected. 139 | ACTIVE_BORDER_COLOR = (107, 166, 193, 255) 140 | #: port color on mouse over. 141 | HOVER_COLOR = (17, 43, 82, 255) 142 | #: port border color on mouse over. 143 | HOVER_BORDER_COLOR = (136, 255, 35, 255) 144 | #: threshold for selecting a port. 145 | CLICK_FALLOFF = 15.0 146 | 147 | 148 | class PortTypeEnum(Enum): 149 | """ 150 | Port connection types: 151 | :py:mod:`NodeGraphQt.constants.PortTypeEnum` 152 | """ 153 | #: Connection type for input ports. 154 | IN = 'in' 155 | #: Connection type for output ports. 156 | OUT = 'out' 157 | 158 | # ==================================== PIPE ==================================== 159 | 160 | 161 | class PipeEnum(Enum): 162 | """ 163 | Pipe styling layout: 164 | :py:mod:`NodeGraphQt.constants.PipeEnum` 165 | """ 166 | #: default width. 167 | WIDTH = 1.2 168 | ACTIVE_WIDTH = 2 169 | HIGHLIGHT_WIDTH = 2 170 | #: default color. 171 | COLOR = (175, 95, 30, 255) 172 | #: pipe color to a node when it's disabled. 173 | DISABLED_COLOR = (190, 20, 20, 255) 174 | #: pipe color when selected or mouse over. 175 | # ACTIVE_COLOR = (70, 255, 220, 255) 176 | ACTIVE_COLOR = (175, 95, 30, 255) 177 | #: pipe color to a node when it's selected. 178 | # HIGHLIGHT_COLOR = (232, 184, 13, 255) 179 | HIGHLIGHT_COLOR = (52, 152, 219 ) 180 | #: draw connection as a line. 181 | DRAW_TYPE_DEFAULT = 0 182 | #: draw connection as dashed lines. 183 | DRAW_TYPE_DASHED = 1 184 | #: draw connection as a dotted line. 185 | DRAW_TYPE_DOTTED = 2 186 | 187 | 188 | class PipeSlicerEnum(Enum): 189 | """ 190 | Slicer Pipe styling layout: 191 | :py:mod:`NodeGraphQt.constants.PipeSlicerEnum` 192 | """ 193 | #: default width. 194 | WIDTH = 1.5 195 | #: default color. 196 | COLOR = (255, 50, 75) 197 | 198 | 199 | class PipeLayoutEnum(Enum): 200 | """ 201 | Pipe connection drawing layout: 202 | :py:mod:`NodeGraphQt.constants.PipeLayoutEnum` 203 | """ 204 | #: draw straight lines for pipe connections. 205 | STRAIGHT = 0 206 | #: draw curved lines for pipe connections. 207 | CURVED = 1 208 | #: draw angled lines for pipe connections. 209 | ANGLE = 2 210 | 211 | 212 | # === PROPERTY BIN WIDGET === 213 | 214 | #: Property type will hidden in the properties bin (default). 215 | NODE_PROP = 0 216 | #: Property type represented with a QLabel widget in the properties bin. 217 | NODE_PROP_QLABEL = 2 218 | #: Property type represented with a QLineEdit widget in the properties bin. 219 | NODE_PROP_QLINEEDIT = 3 220 | #: Property type represented with a QTextEdit widget in the properties bin. 221 | NODE_PROP_QTEXTEDIT = 4 222 | #: Property type represented with a QComboBox widget in the properties bin. 223 | NODE_PROP_QCOMBO = 5 224 | #: Property type represented with a QCheckBox widget in the properties bin. 225 | NODE_PROP_QCHECKBOX = 6 226 | #: Property type represented with a QSpinBox widget in the properties bin. 227 | NODE_PROP_QSPINBOX = 7 228 | #: Property type represented with a ColorPicker widget in the properties bin. 229 | NODE_PROP_COLORPICKER = 8 230 | #: Property type represented with a Slider widget in the properties bin. 231 | NODE_PROP_SLIDER = 9 232 | #: Property type represented with a file selector widget in the properties bin. 233 | NODE_PROP_FILE = 10 234 | #: Property type represented with a file save widget in the properties bin. 235 | NODE_PROP_FILE_SAVE = 11 236 | #: Property type represented with a vector2 widget in the properties bin. 237 | NODE_PROP_VECTOR2 = 12 238 | #: Property type represented with vector3 widget in the properties bin. 239 | NODE_PROP_VECTOR3 = 13 240 | #: Property type represented with vector4 widget in the properties bin. 241 | NODE_PROP_VECTOR4 = 14 242 | #: Property type represented with float widget in the properties bin. 243 | NODE_PROP_FLOAT = 15 244 | #: Property type represented with int widget in the properties bin. 245 | NODE_PROP_INT = 16 246 | #: Property type represented with button widget in the properties bin. 247 | NODE_PROP_BUTTON = 17 248 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/custom_widgets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luxonis/depthai_pipeline_graph/673f7673a37aac5dc3980adbc0f87fd6eb02683c/depthai_pipeline_graph/NodeGraphQt/custom_widgets/__init__.py -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/custom_widgets/nodes_palette.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from collections import defaultdict 4 | 5 | from Qt import QtWidgets, QtCore, QtGui 6 | 7 | from ..constants import URN_SCHEME 8 | 9 | 10 | class NodesGridDelagate(QtWidgets.QStyledItemDelegate): 11 | 12 | def paint(self, painter, option, index): 13 | """ 14 | Args: 15 | painter (QtGui.QPainter): 16 | option (QtGui.QStyleOptionViewItem): 17 | index (QtCore.QModelIndex): 18 | """ 19 | if index.column() != 0: 20 | super(NodesGridDelagate, self).paint(painter, option, index) 21 | return 22 | 23 | model = index.model().sourceModel() 24 | item = model.item(index.row(), index.column()) 25 | 26 | sub_margin = 2 27 | radius = 5 28 | 29 | base_rect = QtCore.QRectF( 30 | option.rect.x() + sub_margin, 31 | option.rect.y() + sub_margin, 32 | option.rect.width() - (sub_margin * 2), 33 | option.rect.height() - (sub_margin * 2) 34 | ) 35 | 36 | painter.save() 37 | painter.setRenderHint(QtGui.QPainter.Antialiasing, True) 38 | 39 | # background. 40 | bg_color = option.palette.window().color() 41 | pen_color = option.palette.midlight().color().lighter(120) 42 | if option.state & QtWidgets.QStyle.State_Selected: 43 | bg_color = bg_color.lighter(120) 44 | pen_color = pen_color.lighter(160) 45 | 46 | pen = QtGui.QPen(pen_color, 3.0) 47 | pen.setCapStyle(QtCore.Qt.RoundCap) 48 | painter.setPen(pen) 49 | painter.setBrush(QtGui.QBrush(bg_color)) 50 | painter.drawRoundRect(base_rect, 51 | int(base_rect.height()/radius), 52 | int(base_rect.width()/radius)) 53 | 54 | if option.state & QtWidgets.QStyle.State_Selected: 55 | pen_color = option.palette.highlight().color() 56 | else: 57 | pen_color = option.palette.midlight().color().darker(130) 58 | pen = QtGui.QPen(pen_color, 1.0) 59 | pen.setCapStyle(QtCore.Qt.RoundCap) 60 | painter.setPen(pen) 61 | painter.setBrush(QtCore.Qt.NoBrush) 62 | 63 | sub_margin = 6 64 | sub_rect = QtCore.QRectF( 65 | base_rect.x() + sub_margin, 66 | base_rect.y() + sub_margin, 67 | base_rect.width() - (sub_margin * 2), 68 | base_rect.height() - (sub_margin * 2) 69 | ) 70 | painter.drawRoundRect(sub_rect, 71 | int(sub_rect.height() / radius), 72 | int(sub_rect.width() / radius)) 73 | 74 | painter.setBrush(QtGui.QBrush(pen_color)) 75 | edge_size = 2, sub_rect.height() - 6 76 | left_x = sub_rect.left() 77 | right_x = sub_rect.right() - edge_size[0] 78 | pos_y = sub_rect.center().y() - (edge_size[1] / 2) 79 | 80 | for pos_x in [left_x, right_x]: 81 | painter.drawRect(QtCore.QRectF( 82 | pos_x, pos_y, edge_size[0], edge_size[1] 83 | )) 84 | 85 | # painter.setPen(QtCore.Qt.NoPen) 86 | painter.setBrush(QtGui.QBrush(bg_color)) 87 | dot_size = 4 88 | left_x = sub_rect.left() - 1 89 | right_x = sub_rect.right() - (dot_size - 1) 90 | pos_y = sub_rect.center().y() - (dot_size / 2) 91 | for pos_x in [left_x, right_x]: 92 | painter.drawEllipse(QtCore.QRectF( 93 | pos_x, pos_y, dot_size, dot_size 94 | )) 95 | pos_x -= dot_size + 2 96 | 97 | # text 98 | pen_color = option.palette.text().color() 99 | pen = QtGui.QPen(pen_color, 0.5) 100 | pen.setCapStyle(QtCore.Qt.RoundCap) 101 | painter.setPen(pen) 102 | 103 | font = painter.font() 104 | font_metrics = QtGui.QFontMetrics(font) 105 | font_width = font_metrics.horizontalAdvance(item.text().replace(' ', '_')) 106 | font_height = font_metrics.height() 107 | text_rect = QtCore.QRectF( 108 | sub_rect.center().x() - (font_width / 2), 109 | sub_rect.center().y() - (font_height * 0.55), 110 | font_width, font_height) 111 | painter.drawText(text_rect, item.text()) 112 | painter.restore() 113 | 114 | 115 | class NodesGridProxyModel(QtCore.QSortFilterProxyModel): 116 | 117 | def __init__(self, parent=None): 118 | super(NodesGridProxyModel, self).__init__(parent) 119 | 120 | def mimeData(self, indexes): 121 | node_ids = ['node:{}'.format(i.data(QtCore.Qt.ToolTipRole)) 122 | for i in indexes] 123 | node_urn = URN_SCHEME + ';'.join(node_ids) 124 | mime_data = super(NodesGridProxyModel, self).mimeData(indexes) 125 | mime_data.setUrls([node_urn]) 126 | return mime_data 127 | 128 | 129 | class NodesGridView(QtWidgets.QListView): 130 | 131 | def __init__(self, parent=None): 132 | super(NodesGridView, self).__init__(parent) 133 | self.setSelectionMode(self.ExtendedSelection) 134 | self.setUniformItemSizes(True) 135 | self.setResizeMode(self.Adjust) 136 | self.setViewMode(self.IconMode) 137 | self.setDragDropMode(self.DragOnly) 138 | self.setDragEnabled(True) 139 | self.setMinimumSize(450, 300) 140 | self.setSpacing(4) 141 | 142 | model = QtGui.QStandardItemModel() 143 | proxy_model = NodesGridProxyModel() 144 | proxy_model.setSourceModel(model) 145 | self.setModel(proxy_model) 146 | self.setItemDelegate(NodesGridDelagate(self)) 147 | 148 | def clear(self): 149 | self.model().sourceMode().clear() 150 | 151 | def add_item(self, label, tooltip=''): 152 | item = QtGui.QStandardItem(label) 153 | item.setSizeHint(QtCore.QSize(130, 40)) 154 | item.setToolTip(tooltip) 155 | model = self.model().sourceModel() 156 | model.appendRow(item) 157 | 158 | 159 | class NodesPaletteWidget(QtWidgets.QWidget): 160 | """ 161 | The :class:`NodeGraphQt.NodesPaletteWidget` is a widget for displaying all 162 | registered nodes from the node graph in a grid layout with this widget a 163 | user can create nodes by dragging and dropping. 164 | 165 | | *Implemented on NodeGraphQt:* ``v0.1.7`` 166 | 167 | .. image:: _images/nodes_palette.png 168 | :width: 400px 169 | 170 | .. code-block:: python 171 | :linenos: 172 | 173 | from NodeGraphQt import NodeGraph, NodesPaletteWidget 174 | 175 | # create node graph. 176 | graph = NodeGraph() 177 | 178 | # create nodes palette widget. 179 | nodes_palette = NodesPaletteWidget(parent=None, node_graph=graph) 180 | nodes_palette.show() 181 | 182 | Args: 183 | parent (QtWidgets.QWidget): parent of the new widget. 184 | node_graph (NodeGraphQt.NodeGraph): node graph. 185 | """ 186 | 187 | def __init__(self, parent=None, node_graph=None): 188 | super(NodesPaletteWidget, self).__init__(parent) 189 | self.setWindowTitle('Nodes') 190 | 191 | self._category_tabs = {} 192 | self._custom_labels = {} 193 | self._factory = node_graph.node_factory if node_graph else None 194 | 195 | self._tab_widget = QtWidgets.QTabWidget() 196 | self._tab_widget.setMovable(True) 197 | 198 | layout = QtWidgets.QVBoxLayout(self) 199 | layout.addWidget(self._tab_widget) 200 | 201 | self._build_ui() 202 | 203 | def __repr__(self): 204 | return '<{} object at {}>'.format( 205 | self.__class__.__name__, hex(id(self)) 206 | ) 207 | 208 | def _build_ui(self): 209 | """ 210 | populate the ui 211 | """ 212 | categories = set() 213 | node_types = defaultdict(list) 214 | for name, node_ids in self._factory.names.items(): 215 | for nid in node_ids: 216 | category = '.'.join(nid.split('.')[:-1]) 217 | categories.add(category) 218 | node_types[category].append((nid, name)) 219 | 220 | for category, nodes_list in node_types.items(): 221 | grid_view = self._add_category_tab(category) 222 | for node_id, node_name in nodes_list: 223 | grid_view.add_item(node_name, node_id) 224 | 225 | def _set_node_factory(self, factory): 226 | """ 227 | Set current node factory. 228 | 229 | Args: 230 | factory (NodeFactory): node factory. 231 | """ 232 | self._factory = factory 233 | 234 | def _add_category_tab(self, category): 235 | """ 236 | Adds a new tab to the node palette widget. 237 | 238 | Args: 239 | category (str): node identifier category eg. ``"nodes.widgets"`` 240 | 241 | Returns: 242 | NodesGridView: nodes grid view widget. 243 | """ 244 | if category not in self._category_tabs: 245 | grid_widget = NodesGridView(self) 246 | self._tab_widget.addTab(grid_widget, category) 247 | self._category_tabs[category] = grid_widget 248 | return self._category_tabs[category] 249 | 250 | def set_category_label(self, category, label): 251 | """ 252 | Override tab label for a node category tab. 253 | 254 | Args: 255 | category (str): node identifier category eg. ``"nodes.widgets"`` 256 | label (str): custom display label. eg. ``"Node Widgets"`` 257 | """ 258 | if label in self._custom_labels.values(): 259 | labels = {v: k for k, v in self._custom_labels.items()} 260 | raise ValueError('label "{}" already in use for "{}"' 261 | .format(label, labels[label])) 262 | previous_label = self._custom_labels.get(category, '') 263 | for idx in range(self._tab_widget.count()): 264 | tab_text = self._tab_widget.tabText(idx) 265 | if tab_text in [category, previous_label]: 266 | self._tab_widget.setTabText(idx, label) 267 | break 268 | self._custom_labels[category] = label 269 | 270 | def update(self): 271 | """ 272 | Update and refresh the node palette widget. 273 | """ 274 | self._build_tree() 275 | 276 | 277 | 278 | 279 | 280 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/custom_widgets/nodes_tree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from Qt import QtWidgets, QtCore, QtGui 4 | 5 | from ..constants import URN_SCHEME 6 | 7 | TYPE_NODE = QtWidgets.QTreeWidgetItem.UserType + 1 8 | TYPE_CATEGORY = QtWidgets.QTreeWidgetItem.UserType + 2 9 | 10 | 11 | class BaseNodeTreeItem(QtWidgets.QTreeWidgetItem): 12 | 13 | def __eq__(self, other): 14 | """ 15 | Workaround fix for QTreeWidgetItem "operator not implemented error". 16 | see link: https://bugreports.qt.io/browse/PYSIDE-74 17 | """ 18 | return id(self) == id(other) 19 | 20 | 21 | class NodesTreeWidget(QtWidgets.QTreeWidget): 22 | """ 23 | The :class:`NodeGraphQt.NodesTreeWidget` is a widget for displaying all 24 | registered nodes from the node graph with this widget a user can create 25 | nodes by dragging and dropping. 26 | 27 | .. image:: _images/nodes_tree.png 28 | :width: 300px 29 | 30 | .. code-block:: python 31 | :linenos: 32 | 33 | from NodeGraphQt import NodeGraph, NodesTreeWidget 34 | 35 | # create node graph. 36 | graph = NodeGraph() 37 | 38 | # create node tree widget. 39 | nodes_tree = NodesTreeWidget(parent=None, node_graph=graph) 40 | nodes_tree.show() 41 | 42 | Args: 43 | parent (QtWidgets.QWidget): parent of the new widget. 44 | node_graph (NodeGraphQt.NodeGraph): node graph. 45 | """ 46 | 47 | def __init__(self, parent=None, node_graph=None): 48 | super(NodesTreeWidget, self).__init__(parent) 49 | self.setDragDropMode(QtWidgets.QAbstractItemView.DragOnly) 50 | self.setSelectionMode(self.ExtendedSelection) 51 | self.setHeaderHidden(True) 52 | self.setWindowTitle('Nodes') 53 | 54 | self._factory = node_graph.node_factory if node_graph else None 55 | self._custom_labels = {} 56 | self._category_items = {} 57 | 58 | self._build_tree() 59 | 60 | def __repr__(self): 61 | return '<{} object at {}>'.format( 62 | self.__class__.__name__, hex(id(self)) 63 | ) 64 | 65 | def mimeData(self, items): 66 | node_ids = ['node:{}'.format(i.toolTip(0)) for i in items] 67 | node_urn = URN_SCHEME + ';'.join(node_ids) 68 | mime_data = super(NodesTreeWidget, self).mimeData(items) 69 | mime_data.setUrls([node_urn]) 70 | return mime_data 71 | 72 | def _build_tree(self): 73 | """ 74 | Populate the node tree. 75 | """ 76 | self.clear() 77 | palette = QtGui.QPalette() 78 | categories = set() 79 | node_types = {} 80 | for name, node_ids in self._factory.names.items(): 81 | for nid in node_ids: 82 | categories.add('.'.join(nid.split('.')[:-1])) 83 | node_types[nid] = name 84 | 85 | self._category_items = {} 86 | for category in sorted(categories): 87 | if category in self._custom_labels.keys(): 88 | label = self._custom_labels[category] 89 | else: 90 | label = '{}'.format(category) 91 | cat_item = BaseNodeTreeItem(self, [label], type=TYPE_CATEGORY) 92 | cat_item.setFirstColumnSpanned(True) 93 | cat_item.setFlags(QtCore.Qt.ItemIsEnabled) 94 | cat_item.setBackground(0, QtGui.QBrush(palette.midlight().color())) 95 | cat_item.setSizeHint(0, QtCore.QSize(100, 26)) 96 | self.addTopLevelItem(cat_item) 97 | cat_item.setExpanded(True) 98 | self._category_items[category] = cat_item 99 | 100 | for node_id, node_name in node_types.items(): 101 | category = '.'.join(node_id.split('.')[:-1]) 102 | category_item = self._category_items[category] 103 | 104 | item = BaseNodeTreeItem(category_item, [node_name], type=TYPE_NODE) 105 | item.setToolTip(0, node_id) 106 | item.setSizeHint(0, QtCore.QSize(100, 26)) 107 | 108 | category_item.addChild(item) 109 | 110 | def _set_node_factory(self, factory): 111 | """ 112 | Set current node factory. 113 | 114 | Args: 115 | factory (NodeFactory): node factory. 116 | """ 117 | self._factory = factory 118 | 119 | def set_category_label(self, category, label): 120 | """ 121 | Override the label for a node category root item. 122 | 123 | .. image:: _images/nodes_tree_category_label.png 124 | :width: 70% 125 | 126 | Args: 127 | category (str): node identifier category eg. ``"nodes.widgets"`` 128 | label (str): custom display label. eg. ``"Node Widgets"`` 129 | """ 130 | self._custom_labels[category] = label 131 | if category in self._category_items: 132 | item = self._category_items[category] 133 | item.setText(0, label) 134 | 135 | def update(self): 136 | """ 137 | Update and refresh the node tree widget. 138 | """ 139 | self._build_tree() 140 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/custom_widgets/properties_bin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from typing import Dict 3 | 4 | from Qt import QtWidgets, QtCore, QtGui, QtCompat 5 | 6 | from ..custom_widgets.properties import NodePropWidget, PropLabel 7 | 8 | 9 | class PropertiesDelegate(QtWidgets.QStyledItemDelegate): 10 | 11 | def paint(self, painter, option, index): 12 | """ 13 | Args: 14 | painter (QtGui.QPainter): 15 | option (QtGui.QStyleOptionViewItem): 16 | index (QtCore.QModelIndex): 17 | """ 18 | painter.save() 19 | painter.setRenderHint(QtGui.QPainter.Antialiasing, False) 20 | painter.setPen(QtCore.Qt.NoPen) 21 | 22 | # draw background. 23 | bg_clr = option.palette.midlight().color() 24 | painter.setBrush(QtGui.QBrush(bg_clr)) 25 | painter.drawRect(option.rect) 26 | 27 | # draw border. 28 | border_width = 1 29 | if option.state & QtWidgets.QStyle.State_Selected: 30 | bdr_clr = option.palette.highlight().color() 31 | painter.setPen(QtGui.QPen(bdr_clr, 1.5)) 32 | else: 33 | bdr_clr = option.palette.alternateBase().color() 34 | painter.setPen(QtGui.QPen(bdr_clr, 1)) 35 | 36 | painter.setBrush(QtCore.Qt.NoBrush) 37 | painter.drawRect(QtCore.QRect( 38 | option.rect.x() + border_width, 39 | option.rect.y() + border_width, 40 | option.rect.width() - (border_width * 2), 41 | option.rect.height() - (border_width * 2)) 42 | ) 43 | painter.restore() 44 | 45 | 46 | class PropertiesList(QtWidgets.QTableWidget): 47 | 48 | def __init__(self, parent=None): 49 | super(PropertiesList, self).__init__(parent) 50 | self.setItemDelegate(PropertiesDelegate()) 51 | self.setColumnCount(1) 52 | self.setShowGrid(False) 53 | self.verticalHeader().hide() 54 | self.horizontalHeader().hide() 55 | 56 | QtCompat.QHeaderView.setSectionResizeMode( 57 | self.verticalHeader(), QtWidgets.QHeaderView.ResizeToContents) 58 | QtCompat.QHeaderView.setSectionResizeMode( 59 | self.horizontalHeader(), 0, QtWidgets.QHeaderView.Stretch) 60 | self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) 61 | 62 | def wheelEvent(self, event): 63 | delta = event.delta() * 0.2 64 | self.verticalScrollBar().setValue( 65 | self.verticalScrollBar().value() - delta 66 | ) 67 | 68 | 69 | class PropertiesBinWidget(QtWidgets.QWidget): 70 | """ 71 | The :class:`NodeGraphQt.PropertiesBinWidget` is a list widget for displaying 72 | and editing a nodes properties. 73 | 74 | .. image:: _images/prop_bin.png 75 | :width: 950px 76 | 77 | .. code-block:: python 78 | :linenos: 79 | 80 | from NodeGraphQt import NodeGraph, PropertiesBinWidget 81 | 82 | # create node graph. 83 | graph = NodeGraph() 84 | 85 | # create properties bin widget. 86 | properties_bin = PropertiesBinWidget(parent=None, node_graph=graph) 87 | properties_bin.show() 88 | 89 | Args: 90 | parent (QtWidgets.QWidget): parent of the new widget. 91 | node_graph (NodeGraphQt.NodeGraph): node graph. 92 | """ 93 | 94 | #: Signal emitted (node_id, prop_name, prop_value) 95 | property_changed = QtCore.Signal(str, str, object) 96 | 97 | def __init__(self, parent=None, node_graph=None): 98 | super(PropertiesBinWidget, self).__init__(parent) 99 | self.setWindowTitle('Node Properties') 100 | self._prop_list = PropertiesList() 101 | 102 | self.resize(450, 400) 103 | self._block_signal = False 104 | self._lock = False 105 | 106 | btn_clr = QtWidgets.QPushButton('Close') 107 | btn_clr.setToolTip('Close the properties window.') 108 | btn_clr.clicked.connect(self.close) 109 | 110 | top_layout = QtWidgets.QHBoxLayout() 111 | top_layout.setSpacing(2) 112 | # top_layout.addWidget(self._limit) 113 | top_layout.addStretch(1) 114 | # top_layout.addWidget(self.btn_lock) 115 | top_layout.addWidget(btn_clr) 116 | 117 | self.layout = QtWidgets.QVBoxLayout(self) 118 | self.layout.addLayout(top_layout) 119 | 120 | self.text = QtWidgets.QTextEdit() 121 | self.text.setAcceptRichText(True) 122 | self.text.setReadOnly(True) 123 | 124 | self.layout.addWidget(self.text, stretch=0) 125 | 126 | # layout.addWidget(self._prop_list, 1) 127 | 128 | # wire up node graph. 129 | node_graph.add_properties_bin(self) 130 | node_graph.node_double_clicked.connect(self.add_node) 131 | node_graph.nodes_deleted.connect(self.__on_nodes_deleted) 132 | node_graph.property_changed.connect(self.__on_graph_property_changed) 133 | 134 | def node_clicked(self, dai_node_json: str): 135 | html = dai_node_json.replace('\n', '
').replace(' ', ' ') 136 | self.text.setHtml(html) 137 | 138 | def __repr__(self): 139 | return '<{} object at {}>'.format(self.__class__.__name__, hex(id(self))) 140 | 141 | def __on_prop_close(self, node_id): 142 | items = self._prop_list.findItems(node_id, QtCore.Qt.MatchExactly) 143 | [self._prop_list.removeRow(i.row()) for i in items] 144 | 145 | def __on_limit_changed(self, value): 146 | rows = self._prop_list.rowCount() 147 | if rows > value: 148 | self._prop_list.removeRow(rows - 1) 149 | 150 | def __on_nodes_deleted(self, nodes): 151 | """ 152 | Slot function when a node has been deleted. 153 | 154 | Args: 155 | nodes (list[str]): list of node ids. 156 | """ 157 | [self.__on_prop_close(n) for n in nodes] 158 | 159 | def __on_graph_property_changed(self, node, prop_name, prop_value): 160 | """ 161 | Slot function that updates the property bin from the node graph signal. 162 | 163 | Args: 164 | node (NodeGraphQt.NodeObject): 165 | prop_name (str): node property name. 166 | prop_value (object): node property value. 167 | """ 168 | # print("-- grap> _on_graph_property_changed", node, prop_name, prop_value ) 169 | properties_widget = self.prop_widget(node) 170 | if not properties_widget: 171 | return 172 | 173 | property_window = properties_widget.get_widget(prop_name) 174 | 175 | if property_window and prop_value != property_window.get_value(): 176 | self._block_signal = True 177 | property_window.set_value(prop_value) 178 | self._block_signal = False 179 | 180 | def __on_property_widget_changed(self, node_id, prop_name, prop_value): 181 | """ 182 | Slot function triggered when a property widget value has changed. 183 | 184 | Args: 185 | node_id (str): node id. 186 | prop_name (str): node property name. 187 | prop_value (object): node property value. 188 | """ 189 | if not self._block_signal: 190 | self.property_changed.emit(node_id, prop_name, prop_value) 191 | 192 | def limit(self): 193 | """ 194 | Returns the limit for how many nodes can be loaded into the bin. 195 | 196 | Returns: 197 | int: node limit. 198 | """ 199 | # return int(self._limit.value()) 200 | return 1 201 | 202 | def set_limit(self, limit): 203 | """ 204 | Set limit of nodes to display. 205 | 206 | Args: 207 | limit (int): node limit. 208 | """ 209 | self._limit.setValue(limit) 210 | 211 | def add_node(self, node): 212 | """ 213 | Add node to the properties bin. 214 | 215 | Args: 216 | node (NodeGraphQt.NodeObject): node object. 217 | """ 218 | if self.limit() == 0 or self._lock: 219 | return 220 | 221 | rows = self._prop_list.rowCount() 222 | if rows >= self.limit(): 223 | self._prop_list.removeRow(rows - 1) 224 | 225 | itm_find = self._prop_list.findItems(node.id, QtCore.Qt.MatchExactly) 226 | if itm_find: 227 | self._prop_list.removeRow(itm_find[0].row()) 228 | 229 | self._prop_list.insertRow(0) 230 | prop_widget = NodePropWidget(node=node) 231 | prop_widget.property_changed.connect(self.__on_property_widget_changed) 232 | prop_widget.property_closed.connect(self.__on_prop_close) 233 | self._prop_list.setCellWidget(0, 0, prop_widget) 234 | 235 | item = QtWidgets.QTableWidgetItem(node.id) 236 | self._prop_list.setItem(0, 0, item) 237 | self._prop_list.selectRow(0) 238 | 239 | def remove_node(self, node): 240 | """ 241 | Remove node from the properties bin. 242 | 243 | Args: 244 | node (str or NodeGraphQt.BaseNode): node id or node object. 245 | """ 246 | node_id = node if isinstance(node, str) else node.id 247 | self.__on_prop_close(node_id) 248 | 249 | def lock_bin(self): 250 | """ 251 | Lock/UnLock the properties bin. 252 | """ 253 | self._lock = not self._lock 254 | if self._lock: 255 | self.btn_lock.setText('UnLock') 256 | else: 257 | self.btn_lock.setText('Lock') 258 | 259 | def clear_bin(self): 260 | """ 261 | Clear the properties bin. 262 | """ 263 | self._prop_list.setRowCount(0) 264 | 265 | def prop_widget(self, node): 266 | """ 267 | Returns the node property widget. 268 | 269 | Args: 270 | node (str or NodeGraphQt.NodeObject): node id or node object. 271 | 272 | Returns: 273 | NodePropWidget: node property widget. 274 | """ 275 | node_id = node if isinstance(node, str) else node.id 276 | itm_find = self._prop_list.findItems(node_id, QtCore.Qt.MatchExactly) 277 | if itm_find: 278 | item = itm_find[0] 279 | return self._prop_list.cellWidget(item.row(), 0) 280 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class NodeMenuError(Exception): pass 6 | 7 | 8 | class NodePropertyError(Exception): pass 9 | 10 | 11 | class NodeWidgetError(Exception): pass 12 | 13 | 14 | class NodeRegistrationError(Exception): pass 15 | 16 | 17 | class PortError(Exception): pass 18 | 19 | 20 | class PortRegistrationError(Exception): pass 21 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/nodes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luxonis/depthai_pipeline_graph/673f7673a37aac5dc3980adbc0f87fd6eb02683c/depthai_pipeline_graph/NodeGraphQt/nodes/__init__.py -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/nodes/backdrop_node.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from ..base.node import NodeObject 3 | from ..constants import (NODE_PROP_QTEXTEDIT, 4 | NODE_LAYOUT_HORIZONTAL, 5 | NODE_LAYOUT_VERTICAL) 6 | from ..qgraphics.node_backdrop import BackdropNodeItem 7 | 8 | 9 | class BackdropNode(NodeObject): 10 | """ 11 | The ``NodeGraphQt.BackdropNode`` class allows other node object to be 12 | nested inside, it's mainly good for grouping nodes together. 13 | 14 | **Inherited from:** :class:`NodeGraphQt.NodeObject` 15 | 16 | .. image:: ../_images/backdrop.png 17 | :width: 250px 18 | 19 | - 20 | """ 21 | 22 | NODE_NAME = 'Backdrop' 23 | 24 | def __init__(self, qgraphics_views=None): 25 | qgraphics_views = qgraphics_views or { 26 | NODE_LAYOUT_HORIZONTAL: BackdropNodeItem, 27 | NODE_LAYOUT_VERTICAL: BackdropNodeItem 28 | } 29 | super(BackdropNode, self).__init__(qgraphics_views) 30 | # override base default color. 31 | self.model.color = (5, 129, 138, 255) 32 | self.create_property('backdrop_text', '', 33 | widget_type=NODE_PROP_QTEXTEDIT, 34 | tab='Backdrop') 35 | 36 | def on_backdrop_updated(self, update_prop, value=None): 37 | """ 38 | Slot triggered by the "on_backdrop_updated" signal from 39 | the node graph. 40 | 41 | Args: 42 | update_prop (str): update property type. 43 | value (object): update value (optional) 44 | """ 45 | if update_prop == 'sizer_mouse_release': 46 | self.graph.begin_undo('resized "{}"'.format(self.name())) 47 | self.set_property('width', value['width']) 48 | self.set_property('height', value['height']) 49 | self.set_pos(*value['pos']) 50 | self.graph.end_undo() 51 | elif update_prop == 'sizer_double_clicked': 52 | self.graph.begin_undo('"{}" auto resize'.format(self.name())) 53 | self.set_property('width', value['width']) 54 | self.set_property('height', value['height']) 55 | self.set_pos(*value['pos']) 56 | self.graph.end_undo() 57 | 58 | def auto_size(self): 59 | """ 60 | Auto resize the backdrop node to fit around the intersecting nodes. 61 | """ 62 | self.graph.begin_undo('"{}" auto resize'.format(self.name())) 63 | size = self.view.calc_backdrop_size() 64 | self.set_property('width', size['width']) 65 | self.set_property('height', size['height']) 66 | self.set_pos(*size['pos']) 67 | self.graph.end_undo() 68 | 69 | def wrap_nodes(self, nodes): 70 | """ 71 | Set the backdrop size to fit around specified nodes. 72 | 73 | Args: 74 | nodes (list[NodeGraphQt.NodeObject]): list of nodes. 75 | """ 76 | if not nodes: 77 | return 78 | self.graph.begin_undo('"{}" wrap nodes'.format(self.name())) 79 | size = self.view.calc_backdrop_size([n.view for n in nodes]) 80 | self.set_property('width', size['width']) 81 | self.set_property('height', size['height']) 82 | self.set_pos(*size['pos']) 83 | self.graph.end_undo() 84 | 85 | def nodes(self): 86 | """ 87 | Returns nodes wrapped within the backdrop node. 88 | 89 | Returns: 90 | list[NodeGraphQt.BaseNode]: list of node under the backdrop. 91 | """ 92 | node_ids = [n.id for n in self.view.get_nodes()] 93 | return [self.graph.get_node_by_id(nid) for nid in node_ids] 94 | 95 | def set_text(self, text=''): 96 | """ 97 | Sets the text to be displayed in the backdrop node. 98 | 99 | Args: 100 | text (str): text string. 101 | """ 102 | self.set_property('backdrop_text', text) 103 | 104 | def text(self): 105 | """ 106 | Returns the text on the backdrop node. 107 | 108 | Returns: 109 | str: text string. 110 | """ 111 | return self.get_property('backdrop_text') 112 | 113 | def set_size(self, width, height): 114 | """ 115 | Sets the backdrop size. 116 | 117 | Args: 118 | width (float): backdrop width size. 119 | height (float): backdrop height size. 120 | """ 121 | if self.graph: 122 | self.graph.begin_undo('backdrop size') 123 | self.set_property('width', width) 124 | self.set_property('height', height) 125 | self.graph.end_undo() 126 | return 127 | self.view.width, self.view.height = width, height 128 | self.model.width, self.model.height = width, height 129 | 130 | def size(self): 131 | """ 132 | Returns the current size of the node. 133 | 134 | Returns: 135 | tuple: node width, height 136 | """ 137 | self.model.width = self.view.width 138 | self.model.height = self.view.height 139 | return self.model.width, self.model.height 140 | 141 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/nodes/group_node.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from ..constants import (NODE_LAYOUT_VERTICAL, 3 | NODE_LAYOUT_HORIZONTAL) 4 | 5 | from ..nodes.base_node import BaseNode 6 | from ..nodes.port_node import PortInputNode, PortOutputNode 7 | from ..qgraphics.node_group import (GroupNodeItem, 8 | GroupNodeVerticalItem) 9 | 10 | 11 | class GroupNode(BaseNode): 12 | """ 13 | The ``NodeGraphQt.GroupNode`` class extends from the 14 | :class:``NodeGraphQt.BaseNode`` class with the ability to nest other nodes 15 | inside of it. 16 | 17 | **Inherited from:** :class:`NodeGraphQt.BaseNode` 18 | 19 | .. image:: ../_images/group_node.png 20 | :width: 250px 21 | 22 | - 23 | """ 24 | 25 | NODE_NAME = 'Group' 26 | 27 | def __init__(self, qgraphics_views=None): 28 | qgraphics_views = qgraphics_views or { 29 | NODE_LAYOUT_HORIZONTAL: GroupNodeItem, 30 | NODE_LAYOUT_VERTICAL: GroupNodeVerticalItem 31 | } 32 | super(GroupNode, self).__init__(qgraphics_views) 33 | self._input_port_nodes = {} 34 | self._output_port_nodes = {} 35 | 36 | @property 37 | def is_expanded(self): 38 | """ 39 | Returns if the group node is expanded or collapsed. 40 | 41 | Returns: 42 | bool: true if the node is expanded. 43 | """ 44 | if not self.graph: 45 | return False 46 | return bool(self.id in self.graph.sub_graphs) 47 | 48 | def get_sub_graph(self): 49 | """ 50 | Returns the sub graph controller to the group node if initialized 51 | or returns None. 52 | 53 | Returns: 54 | NodeGraphQt.SubGraph or None: sub graph controller. 55 | """ 56 | return self.graph.sub_graphs.get(self.id) 57 | 58 | def get_sub_graph_session(self): 59 | """ 60 | Returns the serialized sub graph session. 61 | 62 | Returns: 63 | dict: serialized sub graph session. 64 | """ 65 | return self.model.subgraph_session 66 | 67 | def set_sub_graph_session(self, serialized_session): 68 | """ 69 | Sets the sub graph session data to the group node. 70 | 71 | Args: 72 | serialized_session (dict): serialized session. 73 | """ 74 | serialized_session = serialized_session or {} 75 | self.model.subgraph_session = serialized_session 76 | 77 | def expand(self): 78 | """ 79 | Expand the group node session. 80 | 81 | See Also: 82 | :meth:`NodeGraph.expand_group_node`, 83 | :meth:`SubGraph.expand_group_node`. 84 | """ 85 | self.graph.expand_group_node(self) 86 | 87 | def collapse(self): 88 | """ 89 | Collapse the group node session it's expanded child sub graphs. 90 | 91 | See Also: 92 | :meth:`NodeGraph.collapse_group_node`, 93 | :meth:`SubGraph.collapse_group_node`. 94 | """ 95 | self.graph.collapse_group_node(self) 96 | 97 | def add_input(self, name='input', multi_input=False, display_name=True, 98 | color=None, locked=False, painter_func=None): 99 | port = super(GroupNode, self).add_input( 100 | name=name, 101 | multi_input=multi_input, 102 | display_name=display_name, 103 | color=color, 104 | locked=locked, 105 | painter_func=painter_func 106 | ) 107 | if self.is_expanded: 108 | input_node = PortInputNode(parent_port=port) 109 | input_node.NODE_NAME = port.name() 110 | input_node.model.set_property('name', port.name()) 111 | input_node.add_output(port.name()) 112 | sub_graph = self.get_sub_graph() 113 | sub_graph.add_node(input_node, selected=False, push_undo=False) 114 | 115 | return port 116 | 117 | def add_output(self, name='output', multi_output=True, display_name=True, 118 | color=None, locked=False, painter_func=None): 119 | port = super(GroupNode, self).add_output( 120 | name=name, 121 | multi_output=multi_output, 122 | display_name=display_name, 123 | color=color, 124 | locked=locked, 125 | painter_func=painter_func 126 | ) 127 | if self.is_expanded: 128 | output_port = PortOutputNode(parent_port=port) 129 | output_port.NODE_NAME = port.name() 130 | output_port.model.set_property('name', port.name()) 131 | output_port.add_input(port.name()) 132 | sub_graph = self.get_sub_graph() 133 | sub_graph.add_node(output_port, selected=False, push_undo=False) 134 | 135 | return port 136 | 137 | def delete_input(self, port): 138 | if type(port) in [int, str]: 139 | port = self.get_output(port) 140 | if port is None: 141 | return 142 | 143 | if self.is_expanded: 144 | sub_graph = self.get_sub_graph() 145 | port_node = sub_graph.get_node_by_port(port) 146 | if port_node: 147 | sub_graph.remove_node(port_node, push_undo=False) 148 | 149 | super(GroupNode, self).delete_input(port) 150 | 151 | def delete_output(self, port): 152 | if type(port) in [int, str]: 153 | port = self.get_output(port) 154 | if port is None: 155 | return 156 | 157 | if self.is_expanded: 158 | sub_graph = self.get_sub_graph() 159 | port_node = sub_graph.get_node_by_port(port) 160 | if port_node: 161 | sub_graph.remove_node(port_node, push_undo=False) 162 | 163 | super(GroupNode, self).delete_output(port) 164 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/nodes/port_node.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from ..constants import (NODE_LAYOUT_VERTICAL, 3 | NODE_LAYOUT_HORIZONTAL) 4 | 5 | from ..errors import PortRegistrationError 6 | from ..nodes.base_node import BaseNode 7 | from ..qgraphics.node_port_in import (PortInputNodeItem, 8 | PortInputNodeVerticalItem) 9 | from ..qgraphics.node_port_out import (PortOutputNodeItem, 10 | PortOutputNodeVerticalItem) 11 | 12 | 13 | class PortInputNode(BaseNode): 14 | """ 15 | The ``PortInputNode`` class is the node object that represents a port from a 16 | :class:`NodeGraphQt.GroupNode` when expanded in a 17 | :class:`NodeGraphQt.SubGraph`. 18 | 19 | **Inherited from:** :class:`NodeGraphQt.BaseNode` 20 | 21 | .. image:: ../_images/port_in_node.png 22 | :width: 150px 23 | 24 | - 25 | """ 26 | 27 | NODE_NAME = 'InputPort' 28 | 29 | def __init__(self, qgraphics_views=None, parent_port=None): 30 | qgraphics_views = qgraphics_views or { 31 | NODE_LAYOUT_HORIZONTAL: PortInputNodeItem, 32 | NODE_LAYOUT_VERTICAL: PortInputNodeVerticalItem 33 | } 34 | super(PortInputNode, self).__init__(qgraphics_views) 35 | self._parent_port = parent_port 36 | 37 | @property 38 | def parent_port(self): 39 | """ 40 | The parent group node port representing this node. 41 | 42 | Returns: 43 | NodeGraphQt.Port: port object. 44 | """ 45 | return self._parent_port 46 | 47 | def add_input(self, name='input', multi_input=False, display_name=True, 48 | color=None, locked=False, painter_func=None): 49 | """ 50 | This is not available for the `PortInputNode` class. 51 | """ 52 | raise PortRegistrationError( 53 | '"{}.add_input()" is not available for {}.' 54 | .format(self.__class__.__name__, self) 55 | ) 56 | 57 | def add_output(self, name='output', multi_output=True, display_name=True, 58 | color=None, locked=False, painter_func=None): 59 | if self._outputs: 60 | raise PortRegistrationError( 61 | '"{}.add_output()" only ONE output is allowed for this node.' 62 | .format(self.__class__.__name__, self) 63 | ) 64 | super(PortInputNode, self).add_output( 65 | name=name, 66 | multi_output=multi_output, 67 | display_name=False, 68 | color=color, 69 | locked=locked, 70 | painter_func=None 71 | ) 72 | 73 | 74 | class PortOutputNode(BaseNode): 75 | """ 76 | The ``PortOutputNode`` class is the node object that represents a port from a 77 | :class:`NodeGraphQt.GroupNode` when expanded in a 78 | :class:`NodeGraphQt.SubGraph`. 79 | 80 | **Inherited from:** :class:`NodeGraphQt.BaseNode` 81 | 82 | .. image:: ../_images/port_out_node.png 83 | :width: 150px 84 | 85 | - 86 | """ 87 | 88 | NODE_NAME = 'OutputPort' 89 | 90 | def __init__(self, qgraphics_views=None, parent_port=None): 91 | qgraphics_views = qgraphics_views or { 92 | NODE_LAYOUT_HORIZONTAL: PortOutputNodeItem, 93 | NODE_LAYOUT_VERTICAL: PortOutputNodeVerticalItem 94 | } 95 | super(PortOutputNode, self).__init__(qgraphics_views) 96 | self._parent_port = parent_port 97 | 98 | @property 99 | def parent_port(self): 100 | """ 101 | The parent group node port representing this node. 102 | 103 | Returns: 104 | NodeGraphQt.Port: port object. 105 | """ 106 | return self._parent_port 107 | 108 | def add_input(self, name='input', multi_input=False, display_name=True, 109 | color=None, locked=False, painter_func=None): 110 | if self._inputs: 111 | raise PortRegistrationError( 112 | '"{}.add_input()" only ONE input is allowed for this node.' 113 | .format(self.__class__.__name__, self) 114 | ) 115 | super(PortOutputNode, self).add_input( 116 | name=name, 117 | multi_input=multi_input, 118 | display_name=False, 119 | color=color, 120 | locked=locked, 121 | painter_func=None 122 | ) 123 | 124 | def add_output(self, name='output', multi_output=True, display_name=True, 125 | color=None, locked=False, painter_func=None): 126 | """ 127 | This is not available for the `PortOutputNode` class. 128 | """ 129 | raise PortRegistrationError( 130 | '"{}.add_output()" is not available for {}.' 131 | .format(self.__class__.__name__, self) 132 | ) 133 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/pkg_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | __version__ = '0.2.2' 4 | __status__ = 'Work in Progress' 5 | __license__ = 'MIT' 6 | 7 | __author__ = 'Johnny Chan' 8 | 9 | __module_name__ = 'NodeGraphQt' 10 | __url__ = 'https://github.com/jchanvfx/NodeGraphQt' 11 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/qgraphics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luxonis/depthai_pipeline_graph/673f7673a37aac5dc3980adbc0f87fd6eb02683c/depthai_pipeline_graph/NodeGraphQt/qgraphics/__init__.py -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/qgraphics/node_abstract.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from Qt import QtCore, QtWidgets 3 | 4 | from ..constants import Z_VAL_NODE, NodeEnum, ITEM_CACHE_MODE 5 | 6 | 7 | class AbstractNodeItem(QtWidgets.QGraphicsItem): 8 | """ 9 | The base class of all node qgraphics item. 10 | """ 11 | 12 | def __init__(self, name='node', parent=None): 13 | super(AbstractNodeItem, self).__init__(parent) 14 | self.setFlags(self.ItemIsSelectable | self.ItemIsMovable) 15 | self.setCacheMode(ITEM_CACHE_MODE) 16 | self.setZValue(Z_VAL_NODE) 17 | self._properties = { 18 | 'id': None, 19 | 'name': name.strip(), 20 | 'color': (13, 18, 23, 255), 21 | 'border_color': (46, 57, 66, 255), 22 | 'text_color': (255, 255, 255, 180), 23 | 'type_': 'AbstractBaseNode', 24 | 'selected': False, 25 | 'disabled': False, 26 | 'visible': False, 27 | } 28 | self._width = NodeEnum.WIDTH.value 29 | self._height = NodeEnum.HEIGHT.value 30 | 31 | def __repr__(self): 32 | return '{}.{}(\'{}\')'.format( 33 | self.__module__, self.__class__.__name__, self.name) 34 | 35 | def boundingRect(self): 36 | return QtCore.QRectF(0.0, 0.0, self._width, self._height) 37 | 38 | def mousePressEvent(self, event): 39 | """ 40 | Re-implemented to update "self._properties['selected']" attribute. 41 | 42 | Args: 43 | event (QtWidgets.QGraphicsSceneMouseEvent): mouse event. 44 | """ 45 | self._properties['selected'] = True 46 | super(AbstractNodeItem, self).mousePressEvent(event) 47 | 48 | def setSelected(self, selected): 49 | self._properties['selected'] = selected 50 | super(AbstractNodeItem, self).setSelected(selected) 51 | 52 | def pre_init(self, viewer, pos=None): 53 | """ 54 | Called before node has been added into the scene. 55 | 56 | Args: 57 | viewer (NodeGraphQt.widgets.viewer.NodeViewer): main viewer. 58 | pos (tuple): the cursor pos if node is called with tab search. 59 | """ 60 | pass 61 | 62 | def post_init(self, viewer, pos=None): 63 | """ 64 | Called after node has been added into the scene. 65 | 66 | Args: 67 | viewer (NodeGraphQt.widgets.viewer.NodeViewer): main viewer 68 | pos (tuple): the cursor pos if node is called with tab search. 69 | """ 70 | pass 71 | 72 | @property 73 | def id(self): 74 | return self._properties['id'] 75 | 76 | @id.setter 77 | def id(self, unique_id=''): 78 | self._properties['id'] = unique_id 79 | 80 | @property 81 | def type_(self): 82 | return self._properties['type_'] 83 | 84 | @type_.setter 85 | def type_(self, node_type='NODE'): 86 | self._properties['type_'] = node_type 87 | 88 | @property 89 | def size(self): 90 | return self._width, self._height 91 | 92 | @property 93 | def width(self): 94 | return self._width 95 | 96 | @width.setter 97 | def width(self, width=0.0): 98 | self._width = width 99 | 100 | @property 101 | def height(self): 102 | return self._height 103 | 104 | @height.setter 105 | def height(self, height=0.0): 106 | self._height = height 107 | 108 | @property 109 | def color(self): 110 | return self._properties['color'] 111 | 112 | @color.setter 113 | def color(self, color=(0, 0, 0, 255)): 114 | self._properties['color'] = color 115 | 116 | @property 117 | def text_color(self): 118 | return self._properties['text_color'] 119 | 120 | @text_color.setter 121 | def text_color(self, color=(100, 100, 100, 255)): 122 | self._properties['text_color'] = color 123 | 124 | @property 125 | def border_color(self): 126 | return self._properties['border_color'] 127 | 128 | @border_color.setter 129 | def border_color(self, color=(0, 0, 0, 255)): 130 | self._properties['border_color'] = color 131 | 132 | @property 133 | def disabled(self): 134 | return self._properties['disabled'] 135 | 136 | @disabled.setter 137 | def disabled(self, state=False): 138 | self._properties['disabled'] = state 139 | 140 | @property 141 | def selected(self): 142 | if self._properties['selected'] != self.isSelected(): 143 | self._properties['selected'] = self.isSelected() 144 | return self._properties['selected'] 145 | 146 | @selected.setter 147 | def selected(self, selected=False): 148 | self.setSelected(selected) 149 | 150 | @property 151 | def visible(self): 152 | return self._properties['visible'] 153 | 154 | @visible.setter 155 | def visible(self, visible=False): 156 | self._properties['visible'] = visible 157 | self.setVisible(visible) 158 | 159 | @property 160 | def xy_pos(self): 161 | """ 162 | return the item scene postion. 163 | ("node.pos" conflicted with "QGraphicsItem.pos()" 164 | so it was refactored to "xy_pos".) 165 | 166 | Returns: 167 | list[float]: x, y scene position. 168 | """ 169 | return [float(self.scenePos().x()), float(self.scenePos().y())] 170 | 171 | @xy_pos.setter 172 | def xy_pos(self, pos=None): 173 | """ 174 | set the item scene postion. 175 | ("node.pos" conflicted with "QGraphicsItem.pos()" 176 | so it was refactored to "xy_pos".) 177 | 178 | Args: 179 | pos (list[float]): x, y scene position. 180 | """ 181 | pos = pos or [0.0, 0.0] 182 | self.setPos(pos[0], pos[1]) 183 | 184 | @property 185 | def name(self): 186 | return self._properties['name'] 187 | 188 | @name.setter 189 | def name(self, name=''): 190 | self._properties['name'] = name 191 | self.setToolTip('node: {}'.format(name)) 192 | 193 | @property 194 | def properties(self): 195 | """ 196 | return the node view attributes. 197 | 198 | Returns: 199 | dict: {property_name: property_value} 200 | """ 201 | props = {'width': self.width, 202 | 'height': self.height, 203 | 'pos': self.xy_pos} 204 | props.update(self._properties) 205 | return props 206 | 207 | def viewer(self): 208 | """ 209 | return the main viewer. 210 | 211 | Returns: 212 | NodeGraphQt.widgets.viewer.NodeViewer: viewer object. 213 | """ 214 | if self.scene(): 215 | return self.scene().viewer() 216 | 217 | def delete(self): 218 | """ 219 | remove node view from the scene. 220 | """ 221 | if self.scene(): 222 | self.scene().removeItem(self) 223 | 224 | def from_dict(self, node_dict): 225 | """ 226 | set the node view attributes from the dictionary. 227 | 228 | Args: 229 | node_dict (dict): serialized node dict. 230 | """ 231 | node_attrs = list(self._properties.keys()) + ['width', 'height', 'pos'] 232 | for name, value in node_dict.items(): 233 | if name in node_attrs: 234 | # "node.pos" conflicted with "QGraphicsItem.pos()" 235 | # so it's refactored to "xy_pos". 236 | if name == 'pos': 237 | name = 'xy_pos' 238 | setattr(self, name, value) 239 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/qgraphics/node_backdrop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from Qt import QtGui, QtCore, QtWidgets 3 | 4 | from ..constants import Z_VAL_PIPE, NodeEnum 5 | from ..qgraphics.node_abstract import AbstractNodeItem 6 | from ..qgraphics.pipe import PipeItem 7 | from ..qgraphics.port import PortItem 8 | 9 | 10 | class BackdropSizer(QtWidgets.QGraphicsItem): 11 | """ 12 | Sizer item for resizing a backdrop item. 13 | 14 | Args: 15 | parent (BackdropNodeItem): the parent node item. 16 | size (float): sizer size. 17 | """ 18 | 19 | def __init__(self, parent=None, size=6.0): 20 | super(BackdropSizer, self).__init__(parent) 21 | self.setFlag(self.ItemIsSelectable, True) 22 | self.setFlag(self.ItemIsMovable, True) 23 | self.setFlag(self.ItemSendsScenePositionChanges, True) 24 | self.setCursor(QtGui.QCursor(QtCore.Qt.SizeFDiagCursor)) 25 | self.setToolTip('double-click auto resize') 26 | self._size = size 27 | 28 | @property 29 | def size(self): 30 | return self._size 31 | 32 | def set_pos(self, x, y): 33 | x -= self._size 34 | y -= self._size 35 | self.setPos(x, y) 36 | 37 | def boundingRect(self): 38 | return QtCore.QRectF(0.5, 0.5, self._size, self._size) 39 | 40 | def itemChange(self, change, value): 41 | if change == self.ItemPositionChange: 42 | item = self.parentItem() 43 | mx, my = item.minimum_size 44 | x = mx if value.x() < mx else value.x() 45 | y = my if value.y() < my else value.y() 46 | value = QtCore.QPointF(x, y) 47 | item.on_sizer_pos_changed(value) 48 | return value 49 | return super(BackdropSizer, self).itemChange(change, value) 50 | 51 | def mouseDoubleClickEvent(self, event): 52 | item = self.parentItem() 53 | item.on_sizer_double_clicked() 54 | super(BackdropSizer, self).mouseDoubleClickEvent(event) 55 | 56 | def mousePressEvent(self, event): 57 | self.__prev_xy = (self.pos().x(), self.pos().y()) 58 | super(BackdropSizer, self).mousePressEvent(event) 59 | 60 | def mouseReleaseEvent(self, event): 61 | current_xy = (self.pos().x(), self.pos().y()) 62 | if current_xy != self.__prev_xy: 63 | item = self.parentItem() 64 | item.on_sizer_pos_mouse_release() 65 | del self.__prev_xy 66 | super(BackdropSizer, self).mouseReleaseEvent(event) 67 | 68 | def paint(self, painter, option, widget): 69 | """ 70 | Draws the backdrop sizer on the bottom right corner. 71 | 72 | Args: 73 | painter (QtGui.QPainter): painter used for drawing the item. 74 | option (QtGui.QStyleOptionGraphicsItem): 75 | used to describe the parameters needed to draw. 76 | widget (QtWidgets.QWidget): not used. 77 | """ 78 | painter.save() 79 | 80 | margin = 1.0 81 | rect = self.boundingRect() 82 | rect = QtCore.QRectF(rect.left() + margin, 83 | rect.top() + margin, 84 | rect.width() - (margin * 2), 85 | rect.height() - (margin * 2)) 86 | 87 | item = self.parentItem() 88 | if item and item.selected: 89 | color = QtGui.QColor(*NodeEnum.SELECTED_BORDER_COLOR.value) 90 | else: 91 | color = QtGui.QColor(*item.color) 92 | color = color.darker(110) 93 | path = QtGui.QPainterPath() 94 | path.moveTo(rect.topRight()) 95 | path.lineTo(rect.bottomRight()) 96 | path.lineTo(rect.bottomLeft()) 97 | painter.setBrush(color) 98 | painter.setPen(QtCore.Qt.NoPen) 99 | painter.fillPath(path, painter.brush()) 100 | 101 | painter.restore() 102 | 103 | 104 | class BackdropNodeItem(AbstractNodeItem): 105 | """ 106 | Base Backdrop item. 107 | 108 | Args: 109 | name (str): name displayed on the node. 110 | text (str): backdrop text. 111 | parent (QtWidgets.QGraphicsItem): parent item. 112 | """ 113 | 114 | def __init__(self, name='backdrop', text='', parent=None): 115 | super(BackdropNodeItem, self).__init__(name, parent) 116 | self.setZValue(Z_VAL_PIPE - 1) 117 | self._properties['backdrop_text'] = text 118 | self._min_size = 80, 80 119 | self._sizer = BackdropSizer(self, 26.0) 120 | self._sizer.set_pos(*self._min_size) 121 | self._nodes = [self] 122 | 123 | def _combined_rect(self, nodes): 124 | group = self.scene().createItemGroup(nodes) 125 | rect = group.boundingRect() 126 | self.scene().destroyItemGroup(group) 127 | return rect 128 | 129 | def mouseDoubleClickEvent(self, event): 130 | viewer = self.viewer() 131 | if viewer: 132 | viewer.node_double_clicked.emit(self.id) 133 | super(BackdropNodeItem, self).mouseDoubleClickEvent(event) 134 | 135 | def mousePressEvent(self, event): 136 | if event.button() == QtCore.Qt.LeftButton: 137 | pos = event.scenePos() 138 | rect = QtCore.QRectF(pos.x() - 5, pos.y() - 5, 10, 10) 139 | item = self.scene().items(rect)[0] 140 | 141 | if isinstance(item, (PortItem, PipeItem)): 142 | self.setFlag(self.ItemIsMovable, False) 143 | return 144 | if self.selected: 145 | return 146 | 147 | viewer = self.viewer() 148 | [n.setSelected(False) for n in viewer.selected_nodes()] 149 | 150 | self._nodes += self.get_nodes(False) 151 | [n.setSelected(True) for n in self._nodes] 152 | 153 | def mouseReleaseEvent(self, event): 154 | super(BackdropNodeItem, self).mouseReleaseEvent(event) 155 | self.setFlag(self.ItemIsMovable, True) 156 | [n.setSelected(True) for n in self._nodes] 157 | self._nodes = [self] 158 | 159 | def on_sizer_pos_changed(self, pos): 160 | self._width = pos.x() + self._sizer.size 161 | self._height = pos.y() + self._sizer.size 162 | 163 | def on_sizer_pos_mouse_release(self): 164 | size = { 165 | 'pos': self.xy_pos, 166 | 'width': self._width, 167 | 'height': self._height} 168 | self.viewer().node_backdrop_updated.emit( 169 | self.id, 'sizer_mouse_release', size) 170 | 171 | def on_sizer_double_clicked(self): 172 | size = self.calc_backdrop_size() 173 | self.viewer().node_backdrop_updated.emit( 174 | self.id, 'sizer_double_clicked', size) 175 | 176 | def paint(self, painter, option, widget): 177 | """ 178 | Draws the backdrop rect. 179 | 180 | Args: 181 | painter (QtGui.QPainter): painter used for drawing the item. 182 | option (QtGui.QStyleOptionGraphicsItem): 183 | used to describe the parameters needed to draw. 184 | widget (QtWidgets.QWidget): not used. 185 | """ 186 | painter.save() 187 | painter.setPen(QtCore.Qt.NoPen) 188 | painter.setBrush(QtCore.Qt.NoBrush) 189 | 190 | margin = 1.0 191 | rect = self.boundingRect() 192 | rect = QtCore.QRectF(rect.left() + margin, 193 | rect.top() + margin, 194 | rect.width() - (margin * 2), 195 | rect.height() - (margin * 2)) 196 | 197 | radius = 2.6 198 | color = (self.color[0], self.color[1], self.color[2], 50) 199 | painter.setBrush(QtGui.QColor(*color)) 200 | painter.setPen(QtCore.Qt.NoPen) 201 | painter.drawRoundedRect(rect, radius, radius) 202 | 203 | top_rect = QtCore.QRectF(rect.x(), rect.y(), rect.width(), 26.0) 204 | painter.setBrush(QtGui.QBrush(QtGui.QColor(*self.color))) 205 | painter.setPen(QtCore.Qt.NoPen) 206 | painter.drawRoundedRect(top_rect, radius, radius) 207 | for pos in [top_rect.left(), top_rect.right() - 5.0]: 208 | painter.drawRect( 209 | QtCore.QRectF(pos, top_rect.bottom() - 5.0, 5.0, 5.0)) 210 | 211 | if self.backdrop_text: 212 | painter.setPen(QtGui.QColor(*self.text_color)) 213 | txt_rect = QtCore.QRectF( 214 | top_rect.x() + 5.0, top_rect.height() + 3.0, 215 | rect.width() - 5.0, rect.height()) 216 | painter.setPen(QtGui.QColor(*self.text_color)) 217 | painter.drawText(txt_rect, 218 | QtCore.Qt.AlignLeft | QtCore.Qt.TextWordWrap, 219 | self.backdrop_text) 220 | 221 | if self.selected: 222 | sel_color = [x for x in NodeEnum.SELECTED_COLOR.value] 223 | sel_color[-1] = 15 224 | painter.setBrush(QtGui.QColor(*sel_color)) 225 | painter.setPen(QtCore.Qt.NoPen) 226 | painter.drawRoundedRect(rect, radius, radius) 227 | 228 | txt_rect = QtCore.QRectF(top_rect.x(), top_rect.y(), 229 | rect.width(), top_rect.height()) 230 | painter.setPen(QtGui.QColor(*self.text_color)) 231 | painter.drawText(txt_rect, QtCore.Qt.AlignCenter, self.name) 232 | 233 | border = 0.8 234 | border_color = self.color 235 | if self.selected and NodeEnum.SELECTED_BORDER_COLOR.value: 236 | border = 1.0 237 | border_color = NodeEnum.SELECTED_BORDER_COLOR.value 238 | painter.setBrush(QtCore.Qt.NoBrush) 239 | painter.setPen(QtGui.QPen(QtGui.QColor(*border_color), border)) 240 | painter.drawRoundedRect(rect, radius, radius) 241 | 242 | painter.restore() 243 | 244 | def get_nodes(self, inc_intersects=False): 245 | mode = {True: QtCore.Qt.IntersectsItemShape, 246 | False: QtCore.Qt.ContainsItemShape} 247 | nodes = [] 248 | if self.scene(): 249 | polygon = self.mapToScene(self.boundingRect()) 250 | rect = polygon.boundingRect() 251 | items = self.scene().items(rect, mode=mode[inc_intersects]) 252 | for item in items: 253 | if item == self or item == self._sizer: 254 | continue 255 | if isinstance(item, AbstractNodeItem): 256 | nodes.append(item) 257 | return nodes 258 | 259 | def calc_backdrop_size(self, nodes=None): 260 | nodes = nodes or self.get_nodes(True) 261 | padding = 40 262 | nodes_rect = self._combined_rect(nodes) 263 | return { 264 | 'pos': [ 265 | nodes_rect.x() - padding, nodes_rect.y() - padding 266 | ], 267 | 'width': nodes_rect.width() + (padding * 2), 268 | 'height': nodes_rect.height() + (padding * 2) 269 | } 270 | 271 | def draw_node(self): 272 | """ 273 | Re-draw the node item in the scene. 274 | (re-implemented for vertical layout design) 275 | """ 276 | # height = self._text_item.boundingRect().height() + 4.0 277 | 278 | # # setup initial base size. 279 | # self._set_base_size(add_h=height) 280 | # # set text color when node is initialized. 281 | # self._set_text_color(self.text_color) 282 | # # set the tooltip 283 | # self._tooltip_disable(self.disabled) 284 | 285 | # # --- set the initial node layout --- 286 | # # (do all the graphic item layout offsets here) 287 | 288 | # # align label text 289 | # self.align_label() 290 | # # align icon 291 | # self.align_icon(h_offset=2.0, v_offset=1.0) 292 | # # arrange input and output ports. 293 | # self.align_ports(v_offset=height) 294 | # # arrange node widgets 295 | # self.align_widgets(v_offset=height) 296 | 297 | self.update() 298 | 299 | @property 300 | def minimum_size(self): 301 | return self._min_size 302 | 303 | @minimum_size.setter 304 | def minimum_size(self, size=(50, 50)): 305 | self._min_size = size 306 | 307 | @property 308 | def backdrop_text(self): 309 | return self._properties['backdrop_text'] 310 | 311 | @backdrop_text.setter 312 | def backdrop_text(self, text): 313 | self._properties['backdrop_text'] = text 314 | self.update(self.boundingRect()) 315 | 316 | @AbstractNodeItem.width.setter 317 | def width(self, width=0.0): 318 | AbstractNodeItem.width.fset(self, width) 319 | self._sizer.set_pos(self._width, self._height) 320 | 321 | @AbstractNodeItem.height.setter 322 | def height(self, height=0.0): 323 | AbstractNodeItem.height.fset(self, height) 324 | self._sizer.set_pos(self._width, self._height) 325 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/qgraphics/node_overlay_disabled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from Qt import QtGui, QtCore, QtWidgets 3 | 4 | from ..constants import Z_VAL_NODE_WIDGET 5 | 6 | 7 | class XDisabledItem(QtWidgets.QGraphicsItem): 8 | """ 9 | Node disabled overlay item. 10 | 11 | Args: 12 | parent (NodeItem): the parent node item. 13 | text (str): disable overlay text. 14 | """ 15 | 16 | def __init__(self, parent=None, text=None): 17 | super(XDisabledItem, self).__init__(parent) 18 | self.setZValue(Z_VAL_NODE_WIDGET + 2) 19 | self.setVisible(False) 20 | self.proxy_mode = False 21 | self.color = (0, 0, 0, 255) 22 | self.text = text 23 | 24 | def boundingRect(self): 25 | return self.parentItem().boundingRect() 26 | 27 | def paint(self, painter, option, widget): 28 | """ 29 | Draws the overlay disabled X item on top of a node item. 30 | 31 | Args: 32 | painter (QtGui.QPainter): painter used for drawing the item. 33 | option (QtGui.QStyleOptionGraphicsItem): 34 | used to describe the parameters needed to draw. 35 | widget (QtWidgets.QWidget): not used. 36 | """ 37 | painter.save() 38 | 39 | margin = 20 40 | rect = self.boundingRect() 41 | dis_rect = QtCore.QRectF(rect.left() - (margin / 2), 42 | rect.top() - (margin / 2), 43 | rect.width() + margin, 44 | rect.height() + margin) 45 | if not self.proxy_mode: 46 | pen = QtGui.QPen(QtGui.QColor(*self.color), 8) 47 | pen.setCapStyle(QtCore.Qt.RoundCap) 48 | painter.setPen(pen) 49 | painter.drawLine(dis_rect.topLeft(), dis_rect.bottomRight()) 50 | painter.drawLine(dis_rect.topRight(), dis_rect.bottomLeft()) 51 | 52 | bg_color = QtGui.QColor(*self.color) 53 | bg_color.setAlpha(100) 54 | bg_margin = -0.5 55 | bg_rect = QtCore.QRectF(dis_rect.left() - (bg_margin / 2), 56 | dis_rect.top() - (bg_margin / 2), 57 | dis_rect.width() + bg_margin, 58 | dis_rect.height() + bg_margin) 59 | painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, 0))) 60 | painter.setBrush(bg_color) 61 | painter.drawRoundedRect(bg_rect, 5, 5) 62 | 63 | if not self.proxy_mode: 64 | point_size = 4.0 65 | pen = QtGui.QPen(QtGui.QColor(155, 0, 0, 255), 0.7) 66 | else: 67 | point_size = 8.0 68 | pen = QtGui.QPen(QtGui.QColor(155, 0, 0, 255), 4.0) 69 | 70 | painter.setPen(pen) 71 | painter.drawLine(dis_rect.topLeft(), dis_rect.bottomRight()) 72 | painter.drawLine(dis_rect.topRight(), dis_rect.bottomLeft()) 73 | 74 | point_pos = (dis_rect.topLeft(), dis_rect.topRight(), 75 | dis_rect.bottomLeft(), dis_rect.bottomRight()) 76 | painter.setBrush(QtGui.QColor(255, 0, 0, 255)) 77 | for p in point_pos: 78 | p.setX(p.x() - (point_size / 2)) 79 | p.setY(p.y() - (point_size / 2)) 80 | point_rect = QtCore.QRectF( 81 | p, QtCore.QSizeF(point_size, point_size)) 82 | painter.drawEllipse(point_rect) 83 | 84 | if self.text and not self.proxy_mode: 85 | font = painter.font() 86 | font.setPointSize(10) 87 | 88 | painter.setFont(font) 89 | font_metrics = QtGui.QFontMetrics(font) 90 | font_width = font_metrics.width(self.text) 91 | font_height = font_metrics.height() 92 | txt_w = font_width * 1.25 93 | txt_h = font_height * 2.25 94 | text_bg_rect = QtCore.QRectF((rect.width() / 2) - (txt_w / 2), 95 | (rect.height() / 2) - (txt_h / 2), 96 | txt_w, txt_h) 97 | painter.setPen(QtGui.QPen(QtGui.QColor(255, 0, 0), 0.5)) 98 | painter.setBrush(QtGui.QColor(*self.color)) 99 | painter.drawRoundedRect(text_bg_rect, 2, 2) 100 | 101 | text_rect = QtCore.QRectF((rect.width() / 2) - (font_width / 2), 102 | (rect.height() / 2) - (font_height / 2), 103 | txt_w * 2, font_height * 2) 104 | 105 | painter.setPen(QtGui.QPen(QtGui.QColor(255, 0, 0), 1)) 106 | painter.drawText(text_rect, self.text) 107 | 108 | painter.restore() 109 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/qgraphics/node_port_in.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from Qt import QtCore, QtGui, QtWidgets 3 | 4 | from ..constants import NodeEnum 5 | from ..qgraphics.node_base import NodeItem, NodeItemVertical 6 | 7 | 8 | class PortInputNodeItem(NodeItem): 9 | """ 10 | Input Port Node item. 11 | 12 | Args: 13 | name (str): name displayed on the node. 14 | parent (QtWidgets.QGraphicsItem): parent item. 15 | """ 16 | 17 | def __init__(self, name='group port', parent=None): 18 | super(PortInputNodeItem, self).__init__(name, parent) 19 | self._icon_item.setVisible(False) 20 | self._text_item.set_locked(True) 21 | self._x_item.text = 'Port Locked' 22 | 23 | def _set_base_size(self, add_w=0.0, add_h=0.0): 24 | width, height = self.calc_size(add_w, add_h) 25 | self._width = width + 60 26 | self._height = height if height >= 60 else 60 27 | 28 | def paint(self, painter, option, widget): 29 | """ 30 | Draws the node base not the ports or text. 31 | 32 | Args: 33 | painter (QtGui.QPainter): painter used for drawing the item. 34 | option (QtGui.QStyleOptionGraphicsItem): 35 | used to describe the parameters needed to draw. 36 | widget (QtWidgets.QWidget): not used. 37 | """ 38 | self.auto_switch_mode() 39 | 40 | painter.save() 41 | painter.setBrush(QtCore.Qt.NoBrush) 42 | painter.setPen(QtCore.Qt.NoPen) 43 | 44 | margin = 2.0 45 | rect = self.boundingRect() 46 | rect = QtCore.QRectF(rect.left() + margin, 47 | rect.top() + margin, 48 | rect.width() - (margin * 2), 49 | rect.height() - (margin * 2)) 50 | 51 | text_rect = self._text_item.boundingRect() 52 | text_rect = QtCore.QRectF( 53 | rect.center().x() - (text_rect.width() / 2) - 5, 54 | rect.center().y() - (text_rect.height() / 2), 55 | text_rect.width() + 10, 56 | text_rect.height() 57 | ) 58 | 59 | painter.setBrush(QtGui.QColor(255, 255, 255, 20)) 60 | painter.drawRoundedRect(rect, 20, 20) 61 | 62 | painter.setBrush(QtGui.QColor(0, 0, 0, 100)) 63 | painter.drawRoundedRect(text_rect, 3, 3) 64 | 65 | size = int(rect.height() / 4) 66 | triangle = QtGui.QPolygonF() 67 | triangle.append(QtCore.QPointF(-size, size)) 68 | triangle.append(QtCore.QPointF(0.0, 0.0)) 69 | triangle.append(QtCore.QPointF(size, size)) 70 | 71 | transform = QtGui.QTransform() 72 | transform.translate(rect.width() - (size / 6), rect.center().y()) 73 | transform.rotate(90) 74 | poly = transform.map(triangle) 75 | 76 | if self.selected: 77 | pen = QtGui.QPen( 78 | QtGui.QColor(*NodeEnum.SELECTED_BORDER_COLOR.value), 1.3 79 | ) 80 | painter.setBrush(QtGui.QColor(*NodeEnum.SELECTED_COLOR.value)) 81 | else: 82 | pen = QtGui.QPen(QtGui.QColor(*self.border_color), 1.2) 83 | painter.setBrush(QtGui.QColor(0, 0, 0, 50)) 84 | 85 | pen.setJoinStyle(QtCore.Qt.MiterJoin) 86 | painter.setPen(pen) 87 | painter.drawPolygon(poly) 88 | 89 | edge_size = 30 90 | edge_rect = QtCore.QRectF(rect.width() - (size * 1.7), 91 | rect.center().y() - (edge_size / 2), 92 | 4, edge_size) 93 | painter.drawRect(edge_rect) 94 | 95 | painter.restore() 96 | 97 | def set_proxy_mode(self, mode): 98 | """ 99 | Set whether to draw the node with proxy mode. 100 | (proxy mode toggles visibility for some qgraphic items in the node.) 101 | 102 | Args: 103 | mode (bool): true to enable proxy mode. 104 | """ 105 | if mode is self._proxy_mode: 106 | return 107 | self._proxy_mode = mode 108 | 109 | visible = not mode 110 | 111 | # disable overlay item. 112 | self._x_item.proxy_mode = self._proxy_mode 113 | 114 | # node widget visibility. 115 | for w in self._widgets.values(): 116 | w.widget().setVisible(visible) 117 | 118 | # input port text visibility. 119 | for port, text in self._input_items.items(): 120 | if port.display_name: 121 | text.setVisible(visible) 122 | 123 | # output port text visibility. 124 | for port, text in self._output_items.items(): 125 | if port.display_name: 126 | text.setVisible(visible) 127 | 128 | self._text_item.setVisible(visible) 129 | 130 | def align_label(self, h_offset=0.0, v_offset=0.0): 131 | """ 132 | Center node label text to the top of the node. 133 | 134 | Args: 135 | v_offset (float): vertical offset. 136 | h_offset (float): horizontal offset. 137 | """ 138 | rect = self.boundingRect() 139 | text_rect = self._text_item.boundingRect() 140 | x = rect.center().x() - (text_rect.width() / 2) 141 | y = rect.center().y() - (text_rect.height() / 2) 142 | self._text_item.setPos(x + h_offset, y + v_offset) 143 | 144 | def align_ports(self, v_offset=0.0): 145 | """ 146 | Align input, output ports in the node layout. 147 | """ 148 | v_offset = self.boundingRect().height() / 2 149 | if self.inputs or self.outputs: 150 | for ports in [self.inputs, self.outputs]: 151 | if ports: 152 | v_offset -= ports[0].boundingRect().height() / 2 153 | break 154 | super(PortInputNodeItem, self).align_ports(v_offset=v_offset) 155 | 156 | def draw_node(self): 157 | """ 158 | Re-draw the node item in the scene. 159 | (re-implemented for vertical layout design) 160 | """ 161 | # setup initial base size. 162 | self._set_base_size() 163 | # set text color when node is initialized. 164 | self._set_text_color(self.text_color) 165 | # set the tooltip 166 | self._tooltip_disable(self.disabled) 167 | 168 | # --- set the initial node layout --- 169 | # (do all the graphic item layout offsets here) 170 | 171 | # align label text 172 | self.align_label() 173 | # arrange icon 174 | self.align_icon() 175 | # arrange input and output ports. 176 | self.align_ports() 177 | # arrange node widgets 178 | self.align_widgets() 179 | 180 | self.update() 181 | 182 | 183 | class PortInputNodeVerticalItem(PortInputNodeItem): 184 | 185 | def paint(self, painter, option, widget): 186 | self.auto_switch_mode() 187 | 188 | painter.save() 189 | painter.setBrush(QtCore.Qt.NoBrush) 190 | painter.setPen(QtCore.Qt.NoPen) 191 | 192 | margin = 2.0 193 | rect = self.boundingRect() 194 | rect = QtCore.QRectF(rect.left() + margin, 195 | rect.top() + margin, 196 | rect.width() - (margin * 2), 197 | rect.height() - (margin * 2)) 198 | 199 | text_rect = self._text_item.boundingRect() 200 | text_rect = QtCore.QRectF( 201 | rect.center().x() - (text_rect.width() / 2) - 5, 202 | rect.top() + margin, 203 | text_rect.width() + 10, 204 | text_rect.height() 205 | ) 206 | 207 | painter.setBrush(QtGui.QColor(255, 255, 255, 20)) 208 | painter.drawRoundedRect(rect, 20, 20) 209 | 210 | painter.setBrush(QtGui.QColor(0, 0, 0, 100)) 211 | painter.drawRoundedRect(text_rect, 3, 3) 212 | 213 | size = int(rect.height() / 4) 214 | triangle = QtGui.QPolygonF() 215 | triangle.append(QtCore.QPointF(-size, size)) 216 | triangle.append(QtCore.QPointF(0.0, 0.0)) 217 | triangle.append(QtCore.QPointF(size, size)) 218 | 219 | transform = QtGui.QTransform() 220 | transform.translate(rect.center().x(), rect.bottom() - (size / 3)) 221 | transform.rotate(180) 222 | poly = transform.map(triangle) 223 | 224 | if self.selected: 225 | pen = QtGui.QPen( 226 | QtGui.QColor(*NodeEnum.SELECTED_BORDER_COLOR.value), 1.3 227 | ) 228 | painter.setBrush(QtGui.QColor(*NodeEnum.SELECTED_COLOR.value)) 229 | else: 230 | pen = QtGui.QPen(QtGui.QColor(*self.border_color), 1.2) 231 | painter.setBrush(QtGui.QColor(0, 0, 0, 50)) 232 | 233 | pen.setJoinStyle(QtCore.Qt.MiterJoin) 234 | painter.setPen(pen) 235 | painter.drawPolygon(poly) 236 | 237 | edge_size = 30 238 | edge_rect = QtCore.QRectF(rect.center().x() - (edge_size / 2), 239 | rect.bottom() - (size * 1.9), 240 | edge_size, 4) 241 | painter.drawRect(edge_rect) 242 | 243 | painter.restore() 244 | 245 | def align_label(self, h_offset=0.0, v_offset=0.0): 246 | """ 247 | Center node label text to the center of the node. 248 | 249 | Args: 250 | v_offset (float): vertical offset. 251 | h_offset (float): horizontal offset. 252 | """ 253 | rect = self.boundingRect() 254 | text_rect = self._text_item.boundingRect() 255 | x = rect.center().x() - (text_rect.width() / 2) 256 | y = rect.center().y() - text_rect.height() - 2.0 257 | self._text_item.setPos(x + h_offset, y + v_offset) 258 | 259 | def align_ports(self, v_offset=0.0): 260 | """ 261 | Align input, output ports in the node layout. 262 | """ 263 | NodeItemVertical.align_ports(self, v_offset=v_offset) 264 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/qgraphics/node_port_out.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from Qt import QtCore, QtGui, QtWidgets 3 | 4 | from ..constants import NodeEnum 5 | from ..qgraphics.node_base import NodeItem, NodeItemVertical 6 | 7 | 8 | class PortOutputNodeItem(NodeItem): 9 | """ 10 | Output Port Node item. 11 | 12 | Args: 13 | name (str): name displayed on the node. 14 | parent (QtWidgets.QGraphicsItem): parent item. 15 | """ 16 | 17 | def __init__(self, name='group port', parent=None): 18 | super(PortOutputNodeItem, self).__init__(name, parent) 19 | self._icon_item.setVisible(False) 20 | self._text_item.set_locked(True) 21 | self._x_item.text = 'Port Locked' 22 | 23 | def _set_base_size(self, add_w=0.0, add_h=0.0): 24 | width, height = self.calc_size(add_w, add_h) 25 | self._width = width + 60 26 | self._height = height if height >= 60 else 60 27 | 28 | def paint(self, painter, option, widget): 29 | """ 30 | Draws the node base not the ports or text. 31 | 32 | Args: 33 | painter (QtGui.QPainter): painter used for drawing the item. 34 | option (QtGui.QStyleOptionGraphicsItem): 35 | used to describe the parameters needed to draw. 36 | widget (QtWidgets.QWidget): not used. 37 | """ 38 | self.auto_switch_mode() 39 | 40 | painter.save() 41 | painter.setBrush(QtCore.Qt.NoBrush) 42 | painter.setPen(QtCore.Qt.NoPen) 43 | 44 | margin = 2.0 45 | rect = self.boundingRect() 46 | rect = QtCore.QRectF(rect.left() + margin, 47 | rect.top() + margin, 48 | rect.width() - (margin * 2), 49 | rect.height() - (margin * 2)) 50 | 51 | text_rect = self._text_item.boundingRect() 52 | text_rect = QtCore.QRectF( 53 | rect.center().x() - (text_rect.width() / 2) - 5, 54 | rect.center().y() - (text_rect.height() / 2), 55 | text_rect.width() + 10, 56 | text_rect.height() 57 | ) 58 | 59 | painter.setBrush(QtGui.QColor(255, 255, 255, 20)) 60 | painter.drawRoundedRect(rect, 20, 20) 61 | 62 | painter.setBrush(QtGui.QColor(0, 0, 0, 100)) 63 | painter.drawRoundedRect(text_rect, 3, 3) 64 | 65 | size = int(rect.height() / 4) 66 | triangle = QtGui.QPolygonF() 67 | triangle.append(QtCore.QPointF(-size, size)) 68 | triangle.append(QtCore.QPointF(0.0, 0.0)) 69 | triangle.append(QtCore.QPointF(size, size)) 70 | 71 | transform = QtGui.QTransform() 72 | transform.translate(rect.x() + (size / 3), rect.center().y()) 73 | transform.rotate(-90) 74 | poly = transform.map(triangle) 75 | 76 | if self.selected: 77 | pen = QtGui.QPen( 78 | QtGui.QColor(*NodeEnum.SELECTED_BORDER_COLOR.value), 1.3 79 | ) 80 | painter.setBrush(QtGui.QColor(*NodeEnum.SELECTED_COLOR.value)) 81 | else: 82 | pen = QtGui.QPen(QtGui.QColor(*self.border_color), 1.2) 83 | painter.setBrush(QtGui.QColor(0, 0, 0, 50)) 84 | 85 | pen.setJoinStyle(QtCore.Qt.MiterJoin) 86 | painter.setPen(pen) 87 | painter.drawPolygon(poly) 88 | 89 | edge_size = 30 90 | edge_rect = QtCore.QRectF(rect.x() + (size * 1.6), 91 | rect.center().y() - (edge_size / 2), 92 | 4, edge_size) 93 | painter.drawRect(edge_rect) 94 | 95 | painter.restore() 96 | 97 | def set_proxy_mode(self, mode): 98 | """ 99 | Set whether to draw the node with proxy mode. 100 | (proxy mode toggles visibility for some qgraphic items in the node.) 101 | 102 | Args: 103 | mode (bool): true to enable proxy mode. 104 | """ 105 | if mode is self._proxy_mode: 106 | return 107 | self._proxy_mode = mode 108 | 109 | visible = not mode 110 | 111 | # disable overlay item. 112 | self._x_item.proxy_mode = self._proxy_mode 113 | 114 | # node widget visibility. 115 | for w in self._widgets.values(): 116 | w.widget().setVisible(visible) 117 | 118 | # input port text visibility. 119 | for port, text in self._input_items.items(): 120 | if port.display_name: 121 | text.setVisible(visible) 122 | 123 | # output port text visibility. 124 | for port, text in self._output_items.items(): 125 | if port.display_name: 126 | text.setVisible(visible) 127 | 128 | self._text_item.setVisible(visible) 129 | 130 | def align_label(self, h_offset=0.0, v_offset=0.0): 131 | """ 132 | Center node label text to the center of the node. 133 | 134 | Args: 135 | v_offset (float): vertical offset. 136 | h_offset (float): horizontal offset. 137 | """ 138 | rect = self.boundingRect() 139 | text_rect = self._text_item.boundingRect() 140 | x = rect.center().x() - (text_rect.width() / 2) 141 | y = rect.center().y() - (text_rect.height() / 2) 142 | self._text_item.setPos(x + h_offset, y + v_offset) 143 | 144 | def align_ports(self, v_offset=0.0): 145 | """ 146 | Align input, output ports in the node layout. 147 | """ 148 | v_offset = self.boundingRect().height() / 2 149 | if self.inputs or self.outputs: 150 | for ports in [self.inputs, self.outputs]: 151 | if ports: 152 | v_offset -= ports[0].boundingRect().height() / 2 153 | break 154 | super(PortOutputNodeItem, self).align_ports(v_offset=v_offset) 155 | 156 | def draw_node(self): 157 | """ 158 | Re-draw the node item in the scene. 159 | (re-implemented for vertical layout design) 160 | """ 161 | # setup initial base size. 162 | self._set_base_size() 163 | # set text color when node is initialized. 164 | self._set_text_color(self.text_color) 165 | # set the tooltip 166 | self._tooltip_disable(self.disabled) 167 | 168 | # --- set the initial node layout --- 169 | # (do all the graphic item layout offsets here) 170 | 171 | # align label text 172 | self.align_label() 173 | # align icon 174 | self.align_icon() 175 | # arrange input and output ports. 176 | self.align_ports() 177 | # arrange node widgets 178 | self.align_widgets() 179 | 180 | self.update() 181 | 182 | 183 | class PortOutputNodeVerticalItem(PortOutputNodeItem): 184 | 185 | def paint(self, painter, option, widget): 186 | """ 187 | Draws the node base not the ports or text. 188 | 189 | Args: 190 | painter (QtGui.QPainter): painter used for drawing the item. 191 | option (QtGui.QStyleOptionGraphicsItem): 192 | used to describe the parameters needed to draw. 193 | widget (QtWidgets.QWidget): not used. 194 | """ 195 | self.auto_switch_mode() 196 | 197 | painter.save() 198 | painter.setBrush(QtCore.Qt.NoBrush) 199 | painter.setPen(QtCore.Qt.NoPen) 200 | 201 | margin = 2.0 202 | rect = self.boundingRect() 203 | rect = QtCore.QRectF(rect.left() + margin, 204 | rect.top() + margin, 205 | rect.width() - (margin * 2), 206 | rect.height() - (margin * 2)) 207 | 208 | text_rect = self._text_item.boundingRect() 209 | text_rect = QtCore.QRectF( 210 | rect.center().x() - (text_rect.width() / 2) - 5, 211 | rect.height() - text_rect.height(), 212 | text_rect.width() + 10, 213 | text_rect.height() 214 | ) 215 | 216 | painter.setBrush(QtGui.QColor(255, 255, 255, 20)) 217 | painter.drawRoundedRect(rect, 20, 20) 218 | 219 | painter.setBrush(QtGui.QColor(0, 0, 0, 100)) 220 | painter.drawRoundedRect(text_rect, 3, 3) 221 | 222 | size = int(rect.height() / 4) 223 | triangle = QtGui.QPolygonF() 224 | triangle.append(QtCore.QPointF(-size, size)) 225 | triangle.append(QtCore.QPointF(0.0, 0.0)) 226 | triangle.append(QtCore.QPointF(size, size)) 227 | 228 | transform = QtGui.QTransform() 229 | transform.translate(rect.center().x(), rect.y() + (size / 3)) 230 | # transform.rotate(-90) 231 | poly = transform.map(triangle) 232 | 233 | if self.selected: 234 | pen = QtGui.QPen( 235 | QtGui.QColor(*NodeEnum.SELECTED_BORDER_COLOR.value), 1.3 236 | ) 237 | painter.setBrush(QtGui.QColor(*NodeEnum.SELECTED_COLOR.value)) 238 | else: 239 | pen = QtGui.QPen(QtGui.QColor(*self.border_color), 1.2) 240 | painter.setBrush(QtGui.QColor(0, 0, 0, 50)) 241 | 242 | pen.setJoinStyle(QtCore.Qt.MiterJoin) 243 | painter.setPen(pen) 244 | painter.drawPolygon(poly) 245 | 246 | edge_size = 30 247 | edge_rect = QtCore.QRectF(rect.center().x() - (edge_size / 2), 248 | rect.y() + (size * 1.6), 249 | edge_size, 4) 250 | painter.drawRect(edge_rect) 251 | 252 | painter.restore() 253 | 254 | def align_label(self, h_offset=0.0, v_offset=0.0): 255 | """ 256 | Center node label text to the center of the node. 257 | 258 | Args: 259 | v_offset (float): vertical offset. 260 | h_offset (float): horizontal offset. 261 | """ 262 | rect = self.boundingRect() 263 | text_rect = self._text_item.boundingRect() 264 | x = rect.center().x() - (text_rect.width() / 2) 265 | y = rect.height() - text_rect.height() - 4.0 266 | self._text_item.setPos(x + h_offset, y + v_offset) 267 | 268 | def align_ports(self, v_offset=0.0): 269 | """ 270 | Align input, output ports in the node layout. 271 | """ 272 | NodeItemVertical.align_ports(self, v_offset=v_offset) 273 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/qgraphics/node_text_item.py: -------------------------------------------------------------------------------- 1 | from Qt import QtWidgets, QtCore, QtGui 2 | 3 | 4 | class NodeTextItem(QtWidgets.QGraphicsTextItem): 5 | """ 6 | NodeTextItem class used to display and edit the name of a NodeItem. 7 | """ 8 | 9 | def __init__(self, text, parent=None): 10 | super(NodeTextItem, self).__init__(text, parent) 11 | self._locked = False 12 | self.set_locked(False) 13 | self.set_editable(False) 14 | 15 | def mouseDoubleClickEvent(self, event): 16 | """ 17 | Re-implemented to jump into edit mode when user clicks on node text. 18 | 19 | Args: 20 | event (QtWidgets.QGraphicsSceneMouseEvent): mouse event. 21 | """ 22 | if not self._locked: 23 | if event.button() == QtCore.Qt.LeftButton: 24 | self.set_editable(True) 25 | event.ignore() 26 | return 27 | super(NodeTextItem, self).mouseDoubleClickEvent(event) 28 | 29 | def keyPressEvent(self, event): 30 | """ 31 | Re-implemented to catch the Return & Escape keys when in edit mode. 32 | 33 | Args: 34 | event (QtGui.QKeyEvent): key event. 35 | """ 36 | if event.key() == QtCore.Qt.Key_Return: 37 | current_text = self.toPlainText() 38 | self.set_node_name(current_text) 39 | self.set_editable(False) 40 | elif event.key() == QtCore.Qt.Key_Escape: 41 | self.setPlainText(self.node.name) 42 | self.set_editable(False) 43 | super(NodeTextItem, self).keyPressEvent(event) 44 | 45 | def focusOutEvent(self, event): 46 | """ 47 | Re-implemented to jump out of edit mode. 48 | 49 | Args: 50 | event (QtGui.QFocusEvent): 51 | """ 52 | current_text = self.toPlainText() 53 | self.set_node_name(current_text) 54 | self.set_editable(False) 55 | super(NodeTextItem, self).focusOutEvent(event) 56 | 57 | def set_editable(self, value=False): 58 | """ 59 | Set the edit mode for the text item. 60 | 61 | Args: 62 | value (bool): true in edit mode. 63 | """ 64 | if self._locked: 65 | return 66 | if value: 67 | self.setTextInteractionFlags( 68 | QtCore.Qt.TextEditable | 69 | QtCore.Qt.TextSelectableByMouse | 70 | QtCore.Qt.TextSelectableByKeyboard 71 | ) 72 | else: 73 | self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) 74 | cursor = self.textCursor() 75 | cursor.clearSelection() 76 | self.setTextCursor(cursor) 77 | 78 | def set_node_name(self, name): 79 | """ 80 | Updates the node name through the node "NodeViewer().node_name_changed" 81 | signal which then updates the node name through the BaseNode object this 82 | will register it as an undo command. 83 | 84 | Args: 85 | name (str): new node name. 86 | """ 87 | name = name.strip() 88 | if name != self.node.name: 89 | viewer = self.node.viewer() 90 | viewer.node_name_changed.emit(self.node.id, name) 91 | 92 | def set_locked(self, state=False): 93 | """ 94 | Locks the text item so it can not be editable. 95 | 96 | Args: 97 | state (bool): lock state. 98 | """ 99 | self._locked = state 100 | if self._locked: 101 | self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, False) 102 | self.setCursor(QtCore.Qt.ArrowCursor) 103 | self.setToolTip('') 104 | else: 105 | self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, True) 106 | self.setToolTip('double-click to edit node name.') 107 | self.setCursor(QtCore.Qt.IBeamCursor) 108 | 109 | @property 110 | def node(self): 111 | """ 112 | Get the parent node item. 113 | 114 | Returns: 115 | NodeItem: parent node qgraphics item. 116 | """ 117 | return self.parentItem() 118 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/qgraphics/port.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from Qt import QtGui, QtCore, QtWidgets 3 | from ..constants import ( 4 | PortTypeEnum, PortEnum, 5 | Z_VAL_PORT, 6 | ITEM_CACHE_MODE) 7 | 8 | import time 9 | from ..trace_event import * 10 | 11 | class PortItem(QtWidgets.QGraphicsItem): 12 | """ 13 | Base Port Item. 14 | """ 15 | text: QtWidgets.QGraphicsTextItem # Text/name of the port 16 | 17 | def __init__(self, parent=None): 18 | super(PortItem, self).__init__(parent) 19 | self.setAcceptHoverEvents(True) 20 | self.setCacheMode(ITEM_CACHE_MODE) 21 | self.setFlag(self.ItemIsSelectable, False) 22 | self.setFlag(self.ItemSendsScenePositionChanges, True) 23 | self.setZValue(Z_VAL_PORT) 24 | self._pipes = [] 25 | self._width = PortEnum.SIZE.value 26 | self._height = PortEnum.SIZE.value 27 | self._hovered = False 28 | self._name = 'port' 29 | self._display_name = True 30 | self._color = PortEnum.COLOR.value 31 | self._border_color = PortEnum.BORDER_COLOR.value 32 | self._border_size = 1 33 | self._port_type = None 34 | self._multi_connection = False 35 | self._locked = False 36 | 37 | self.fps_arr = [] 38 | 39 | 40 | def __str__(self): 41 | return '{}.PortItem("{}")'.format(self.__module__, self.name) 42 | 43 | def __repr__(self): 44 | return '{}.PortItem("{}")'.format(self.__module__, self.name) 45 | 46 | def boundingRect(self): 47 | return QtCore.QRectF(0.0, 0.0, 48 | self._width + PortEnum.CLICK_FALLOFF.value, 49 | self._height) 50 | 51 | def set_graphic_text(self, text: QtWidgets.QGraphicsTextItem, port_name: str): 52 | self.fps_text = text 53 | self.port_name = port_name 54 | 55 | def paint(self, painter, option, widget): 56 | """ 57 | Draws the circular port. 58 | 59 | Args: 60 | painter (QtGui.QPainter): painter used for drawing the item. 61 | option (QtGui.QStyleOptionGraphicsItem): 62 | used to describe the parameters needed to draw. 63 | widget (QtWidgets.QWidget): not used. 64 | """ 65 | painter.save() 66 | 67 | # display falloff collision for debugging 68 | # ---------------------------------------------------------------------- 69 | # pen = QtGui.QPen(QtGui.QColor(255, 255, 255, 80), 0.8) 70 | # pen.setStyle(QtCore.Qt.DotLine) 71 | # painter.setPen(pen) 72 | # painter.drawRect(self.boundingRect()) 73 | # ---------------------------------------------------------------------- 74 | 75 | rect_w = self._width / 1.8 76 | rect_h = self._height / 1.8 77 | rect_x = self.boundingRect().center().x() - (rect_w / 2) 78 | rect_y = self.boundingRect().center().y() - (rect_h / 2) 79 | port_rect = QtCore.QRectF(rect_x, rect_y, rect_w, rect_h) 80 | 81 | # GX 82 | # if self._hovered: 83 | # color = QtGui.QColor(*PortEnum.HOVER_COLOR.value) 84 | # border_color = QtGui.QColor(*PortEnum.HOVER_BORDER_COLOR.value) 85 | if self.connected_pipes: 86 | color = QtGui.QColor(*PortEnum.ACTIVE_COLOR.value) 87 | border_color = QtGui.QColor(*PortEnum.ACTIVE_BORDER_COLOR.value) 88 | else: 89 | color = QtGui.QColor(*self.color) 90 | border_color = QtGui.QColor(*self.border_color) 91 | 92 | pen = QtGui.QPen(border_color, 1.8) 93 | painter.setPen(pen) 94 | painter.setBrush(color) 95 | painter.drawEllipse(port_rect) 96 | 97 | if self.connected_pipes: # and not self._hovered: 98 | painter.setBrush(border_color) 99 | w = port_rect.width() / 2.5 100 | h = port_rect.height() / 2.5 101 | rect = QtCore.QRectF(port_rect.center().x() - w / 2, 102 | port_rect.center().y() - h / 2, 103 | w, h) 104 | border_color = QtGui.QColor(*self.border_color) 105 | pen = QtGui.QPen(border_color, 1.6) 106 | painter.setPen(pen) 107 | painter.setBrush(border_color) 108 | painter.drawEllipse(rect) 109 | # elif self._hovered: 110 | # if self.multi_connection: 111 | # pen = QtGui.QPen(border_color, 1.4) 112 | # painter.setPen(pen) 113 | # painter.setBrush(color) 114 | # w = port_rect.width() / 1.8 115 | # h = port_rect.height() / 1.8 116 | # else: 117 | # painter.setBrush(border_color) 118 | # w = port_rect.width() / 3.5 119 | # h = port_rect.height() / 3.5 120 | # rect = QtCore.QRectF(port_rect.center().x() - w / 2, 121 | # port_rect.center().y() - h / 2, 122 | # w, h) 123 | # painter.drawEllipse(rect) 124 | painter.restore() 125 | 126 | def new_event(self, trace: TraceEvent): 127 | self.fps_arr.append(trace.host_timestamp) 128 | 129 | def update_fps(self): # Gets called from main thread 130 | for i, ts in enumerate(self.fps_arr): 131 | if ts + 2.0 > time.time(): 132 | self.fps_arr = self.fps_arr[i:] 133 | break 134 | 135 | self.fps_text.setPlainText("FPS: {:.0f}".format(len(self.fps_arr) / 2.0)) 136 | 137 | def itemChange(self, change, value): 138 | if change == self.ItemScenePositionHasChanged: 139 | self.redraw_connected_pipes() 140 | return super(PortItem, self).itemChange(change, value) 141 | 142 | def mousePressEvent(self, event): 143 | super(PortItem, self).mousePressEvent(event) 144 | 145 | def mouseReleaseEvent(self, event): 146 | super(PortItem, self).mouseReleaseEvent(event) 147 | 148 | def hoverEnterEvent(self, event): 149 | self._hovered = True 150 | super(PortItem, self).hoverEnterEvent(event) 151 | 152 | def hoverLeaveEvent(self, event): 153 | self._hovered = False 154 | super(PortItem, self).hoverLeaveEvent(event) 155 | 156 | def viewer_start_connection(self): 157 | return # Don't allow configuring DepthAI pipeline 158 | viewer = self.scene().viewer() 159 | viewer.start_live_connection(self) 160 | 161 | def redraw_connected_pipes(self): 162 | if not self.connected_pipes: 163 | return 164 | for pipe in self.connected_pipes: 165 | if self.port_type == PortTypeEnum.IN.value: 166 | pipe.draw_path(self, pipe.output_port) 167 | elif self.port_type == PortTypeEnum.OUT.value: 168 | pipe.draw_path(pipe.input_port, self) 169 | 170 | def add_pipe(self, pipe): 171 | self._pipes.append(pipe) 172 | 173 | def remove_pipe(self, pipe): 174 | self._pipes.remove(pipe) 175 | 176 | @property 177 | def connected_pipes(self): 178 | return self._pipes 179 | 180 | @property 181 | def connected_ports(self): 182 | ports = [] 183 | port_types = { 184 | PortTypeEnum.IN.value: 'output_port', 185 | PortTypeEnum.OUT.value: 'input_port' 186 | } 187 | for pipe in self.connected_pipes: 188 | ports.append(getattr(pipe, port_types[self.port_type])) 189 | return ports 190 | 191 | @property 192 | def hovered(self): 193 | return self._hovered 194 | 195 | @hovered.setter 196 | def hovered(self, value=False): 197 | self._hovered = value 198 | 199 | @property 200 | def node(self): 201 | return self.parentItem() 202 | 203 | @property 204 | def name(self): 205 | return self._name 206 | 207 | @name.setter 208 | def name(self, name=''): 209 | self._name = name.strip() 210 | 211 | @property 212 | def display_name(self): 213 | return self._display_name 214 | 215 | @display_name.setter 216 | def display_name(self, display=True): 217 | self._display_name = display 218 | 219 | @property 220 | def color(self): 221 | return self._color 222 | 223 | @color.setter 224 | def color(self, color=(0, 0, 0, 255)): 225 | self._color = color 226 | self.update() 227 | 228 | @property 229 | def border_color(self): 230 | return self._border_color 231 | 232 | @border_color.setter 233 | def border_color(self, color=(0, 0, 0, 255)): 234 | self._border_color = color 235 | 236 | @property 237 | def border_size(self): 238 | return self._border_size 239 | 240 | @border_size.setter 241 | def border_size(self, size=2): 242 | self._border_size = size 243 | 244 | @property 245 | def locked(self): 246 | return self._locked 247 | 248 | @locked.setter 249 | def locked(self, value=False): 250 | self._locked = value 251 | conn_type = 'multi' if self.multi_connection else 'single' 252 | tooltip = '{}: ({})'.format(self.name, conn_type) 253 | if value: 254 | tooltip += ' (L)' 255 | self.setToolTip(tooltip) 256 | 257 | @property 258 | def multi_connection(self): 259 | return self._multi_connection 260 | 261 | @multi_connection.setter 262 | def multi_connection(self, mode=False): 263 | conn_type = 'multi' if mode else 'single' 264 | self.setToolTip('{}: ({})'.format(self.name, conn_type)) 265 | self._multi_connection = mode 266 | 267 | @property 268 | def port_type(self): 269 | return self._port_type 270 | 271 | @port_type.setter 272 | def port_type(self, port_type): 273 | self._port_type = port_type 274 | 275 | def connect_to(self, port): 276 | if not port: 277 | for pipe in self.connected_pipes: 278 | pipe.delete() 279 | return 280 | if self.scene(): 281 | viewer = self.scene().viewer() 282 | pipe_item = viewer.establish_connection(self, port) 283 | 284 | # redraw the ports. 285 | port.update() 286 | self.update() 287 | return pipe_item 288 | 289 | def disconnect_from(self, port): 290 | port_types = { 291 | PortTypeEnum.IN.value: 'output_port', 292 | PortTypeEnum.OUT.value: 'input_port' 293 | } 294 | for pipe in self.connected_pipes: 295 | connected_port = getattr(pipe, port_types[self.port_type]) 296 | if connected_port == port: 297 | pipe.delete() 298 | break 299 | # redraw the ports. 300 | port.update() 301 | self.update() 302 | 303 | 304 | class CustomPortItem(PortItem): 305 | """ 306 | Custom port item for drawing custom shape port. 307 | """ 308 | 309 | def __init__(self, parent=None, paint_func=None): 310 | super(CustomPortItem, self).__init__(parent) 311 | self._port_painter = paint_func 312 | 313 | def set_painter(self, func=None): 314 | """ 315 | Set custom paint function for drawing. 316 | 317 | Args: 318 | func (function): paint function. 319 | """ 320 | self._port_painter = func 321 | 322 | def paint(self, painter, option, widget): 323 | """ 324 | Draws the port item. 325 | 326 | Args: 327 | painter (QtGui.QPainter): painter used for drawing the item. 328 | option (QtGui.QStyleOptionGraphicsItem): 329 | used to describe the parameters needed to draw. 330 | widget (QtWidgets.QWidget): not used. 331 | """ 332 | if self._port_painter: 333 | rect_w = self._width / 1.8 334 | rect_h = self._height / 1.8 335 | rect_x = self.boundingRect().center().x() - (rect_w / 2) 336 | rect_y = self.boundingRect().center().y() - (rect_h / 2) 337 | port_rect = QtCore.QRectF(rect_x, rect_y, rect_w, rect_h) 338 | port_info = { 339 | 'port_type': self.port_type, 340 | 'color': self.color, 341 | 'border_color': self.border_color, 342 | 'multi_connection': self.multi_connection, 343 | 'connected': bool(self.connected_pipes), 344 | 'hovered': self.hovered, 345 | 'locked': self.locked, 346 | } 347 | self._port_painter(painter, port_rect, port_info) 348 | else: 349 | super(CustomPortItem, self).paint(painter, option, widget) 350 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/qgraphics/slicer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import math 3 | 4 | from Qt import QtCore, QtGui, QtWidgets 5 | 6 | from ..constants import Z_VAL_NODE_WIDGET, PipeSlicerEnum 7 | 8 | 9 | class SlicerPipeItem(QtWidgets.QGraphicsPathItem): 10 | """ 11 | Base item used for drawing the pipe connection slicer. 12 | """ 13 | 14 | def __init__(self): 15 | super(SlicerPipeItem, self).__init__() 16 | self.setZValue(Z_VAL_NODE_WIDGET + 2) 17 | 18 | def paint(self, painter, option, widget): 19 | """ 20 | Draws the slicer pipe. 21 | 22 | Args: 23 | painter (QtGui.QPainter): painter used for drawing the item. 24 | option (QtGui.QStyleOptionGraphicsItem): 25 | used to describe the parameters needed to draw. 26 | widget (QtWidgets.QWidget): not used. 27 | """ 28 | color = QtGui.QColor(*PipeSlicerEnum.COLOR.value) 29 | p1 = self.path().pointAtPercent(0) 30 | p2 = self.path().pointAtPercent(1) 31 | size = 6.0 32 | offset = size / 2 33 | arrow_size = 4.0 34 | 35 | painter.save() 36 | painter.setRenderHint(painter.Antialiasing, True) 37 | 38 | font = painter.font() 39 | font.setPointSize(12) 40 | painter.setFont(font) 41 | text = 'slice' 42 | text_x = painter.fontMetrics().width(text) / 2 43 | text_y = painter.fontMetrics().height() / 1.5 44 | text_pos = QtCore.QPointF(p1.x() - text_x, p1.y() - text_y) 45 | text_color = QtGui.QColor(*PipeSlicerEnum.COLOR.value) 46 | text_color.setAlpha(80) 47 | painter.setPen(QtGui.QPen( 48 | text_color, PipeSlicerEnum.WIDTH.value, QtCore.Qt.SolidLine 49 | )) 50 | painter.drawText(text_pos, text) 51 | 52 | painter.setPen(QtGui.QPen( 53 | color, PipeSlicerEnum.WIDTH.value, QtCore.Qt.DashDotLine 54 | )) 55 | painter.drawPath(self.path()) 56 | 57 | pen = QtGui.QPen( 58 | color, PipeSlicerEnum.WIDTH.value, QtCore.Qt.SolidLine 59 | ) 60 | pen.setCapStyle(QtCore.Qt.RoundCap) 61 | pen.setJoinStyle(QtCore.Qt.MiterJoin) 62 | painter.setPen(pen) 63 | painter.setBrush(color) 64 | 65 | rect = QtCore.QRectF(p1.x() - offset, p1.y() - offset, size, size) 66 | painter.drawEllipse(rect) 67 | 68 | arrow = QtGui.QPolygonF() 69 | arrow.append(QtCore.QPointF(-arrow_size, arrow_size)) 70 | arrow.append(QtCore.QPointF(0.0, -arrow_size * 0.9)) 71 | arrow.append(QtCore.QPointF(arrow_size, arrow_size)) 72 | 73 | transform = QtGui.QTransform() 74 | transform.translate(p2.x(), p2.y()) 75 | radians = math.atan2(p2.y() - p1.y(), 76 | p2.x() - p1.x()) 77 | degrees = math.degrees(radians) - 90 78 | transform.rotate(degrees) 79 | 80 | painter.drawPolygon(transform.map(arrow)) 81 | painter.restore() 82 | 83 | def draw_path(self, p1, p2): 84 | path = QtGui.QPainterPath() 85 | path.moveTo(p1) 86 | path.lineTo(p2) 87 | self.setPath(path) 88 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/trace_event.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class EventEnum(Enum): 4 | SEND = 0 5 | RECEIVE = 1 6 | PULL = 2 7 | 8 | class StatusEnum(Enum): 9 | START = 0 10 | END = 1 11 | TIMEOUT = 2 12 | 13 | class TraceEvent(): 14 | event: EventEnum 15 | status: StatusEnum 16 | src_id: str 17 | dst_id: str 18 | timestamp = 0.0 19 | host_timestamp = 0.0 20 | 21 | 22 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # (c) 2017, Johnny Chan 4 | # https://github.com/jchanvfx/NodeGraphQt 5 | 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | 9 | # * Redistributions of source code must retain the above copyright notice, 10 | # this list of conditions and the following disclaimer. 11 | 12 | # * Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | 16 | # * Neither the name of the Johnny Chan nor the names of its contributors 17 | # may be used to endorse or promote products derived from this software 18 | # without specific prior written permission. 19 | 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 23 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 24 | # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 25 | # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 26 | # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 27 | # OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 28 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 29 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 30 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/widgets/actions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from Qt import QtCore, QtWidgets 3 | 4 | from ..constants import ViewerEnum 5 | 6 | 7 | class BaseMenu(QtWidgets.QMenu): 8 | 9 | def __init__(self, *args, **kwargs): 10 | super(BaseMenu, self).__init__(*args, **kwargs) 11 | text_color = self.palette().text().color().getRgb() 12 | selected_color = self.palette().highlight().color().getRgb() 13 | style_dict = { 14 | 'QMenu': { 15 | 'color': 'rgb({0},{1},{2})'.format(*text_color), 16 | 'background-color': 'rgb({0},{1},{2})'.format( 17 | *ViewerEnum.BACKGROUND_COLOR.value 18 | ), 19 | 'border': '1px solid rgba({0},{1},{2},30)'.format(*text_color), 20 | 'border-radius': '3px', 21 | }, 22 | 'QMenu::item': { 23 | 'padding': '5px 18px 2px', 24 | 'background-color': 'transparent', 25 | }, 26 | 'QMenu::item:selected': { 27 | 'color': 'rgb({0},{1},{2})'.format(*text_color), 28 | 'background-color': 'rgba({0},{1},{2},200)' 29 | .format(*selected_color), 30 | }, 31 | 'QMenu::separator': { 32 | 'height': '1px', 33 | 'background': 'rgba({0},{1},{2}, 50)'.format(*text_color), 34 | 'margin': '4px 8px', 35 | } 36 | } 37 | stylesheet = '' 38 | for css_class, css in style_dict.items(): 39 | style = '{} {{\n'.format(css_class) 40 | for elm_name, elm_val in css.items(): 41 | style += ' {}:{};\n'.format(elm_name, elm_val) 42 | style += '}\n' 43 | stylesheet += style 44 | self.setStyleSheet(stylesheet) 45 | self.node_class = None 46 | self.graph = None 47 | 48 | # disable for issue #142 49 | # def hideEvent(self, event): 50 | # super(BaseMenu, self).hideEvent(event) 51 | # for a in self.actions(): 52 | # if hasattr(a, 'node_id'): 53 | # a.node_id = None 54 | 55 | def get_menu(self, name, node_id=None): 56 | for action in self.actions(): 57 | menu = action.menu() 58 | if not menu: 59 | continue 60 | if menu.title() == name: 61 | return menu 62 | if node_id and menu.node_class: 63 | node = menu.graph.get_node_by_id(node_id) 64 | if isinstance(node, menu.node_class): 65 | return menu 66 | 67 | def get_menus(self, node_class): 68 | menus = [] 69 | for action in self.actions(): 70 | menu = action.menu() 71 | if menu.node_class: 72 | if issubclass(menu.node_class, node_class): 73 | menus.append(menu) 74 | return menus 75 | 76 | 77 | class GraphAction(QtWidgets.QAction): 78 | 79 | executed = QtCore.Signal(object) 80 | 81 | def __init__(self, *args, **kwargs): 82 | super(GraphAction, self).__init__(*args, **kwargs) 83 | self.graph = None 84 | self.triggered.connect(self._on_triggered) 85 | 86 | def _on_triggered(self): 87 | self.executed.emit(self.graph) 88 | 89 | def get_action(self, name): 90 | for action in self.qmenu.actions(): 91 | if not action.menu() and action.text() == name: 92 | return action 93 | 94 | 95 | class NodeAction(GraphAction): 96 | 97 | executed = QtCore.Signal(object, object) 98 | 99 | def __init__(self, *args, **kwargs): 100 | super(NodeAction, self).__init__(*args, **kwargs) 101 | self.node_id = None 102 | 103 | def _on_triggered(self): 104 | node = self.graph.get_node_by_id(self.node_id) 105 | self.executed.emit(self.graph, node) 106 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/widgets/dialogs.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from Qt import QtWidgets 4 | 5 | _current_user_directory = os.path.expanduser('~') 6 | 7 | 8 | def set_dir(file): 9 | global _current_user_directory 10 | if os.path.isdir(file): 11 | _current_user_directory = file 12 | elif os.path.isfile(file): 13 | _current_user_directory = os.path.split(file)[0] 14 | 15 | 16 | class FileDialog(object): 17 | 18 | @staticmethod 19 | def getSaveFileName(parent=None, title='Save File', file_dir=None, 20 | ext_filter='*'): 21 | if not file_dir: 22 | file_dir = _current_user_directory 23 | file_dlg = QtWidgets.QFileDialog.getSaveFileName( 24 | parent, title, file_dir, ext_filter) 25 | file = file_dlg[0] or None 26 | if file: 27 | set_dir(file) 28 | return file_dlg 29 | 30 | @staticmethod 31 | def getOpenFileName(parent=None, title='Open File', file_dir=None, 32 | ext_filter='*'): 33 | if not file_dir: 34 | file_dir = _current_user_directory 35 | file_dlg = QtWidgets.QFileDialog.getOpenFileName( 36 | parent, title, file_dir, ext_filter) 37 | file = file_dlg[0] or None 38 | if file: 39 | set_dir(file) 40 | return file_dlg 41 | 42 | 43 | class BaseDialog(object): 44 | 45 | @staticmethod 46 | def message_dialog(text='', title='Message'): 47 | dlg = QtWidgets.QMessageBox() 48 | dlg.setWindowTitle(title) 49 | dlg.setInformativeText(text) 50 | dlg.setStandardButtons(QtWidgets.QMessageBox.Ok) 51 | return dlg.exec() 52 | 53 | @staticmethod 54 | def question_dialog(text='', title='Are you sure?'): 55 | dlg = QtWidgets.QMessageBox() 56 | dlg.setWindowTitle(title) 57 | dlg.setInformativeText(text) 58 | dlg.setStandardButtons( 59 | QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No 60 | ) 61 | result = dlg.exec() 62 | return bool(result == QtWidgets.QMessageBox.Yes) 63 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/widgets/icons/node_base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luxonis/depthai_pipeline_graph/673f7673a37aac5dc3980adbc0f87fd6eb02683c/depthai_pipeline_graph/NodeGraphQt/widgets/icons/node_base.png -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/widgets/node_graph.py: -------------------------------------------------------------------------------- 1 | from Qt import QtWidgets, QtGui 2 | 3 | from ..constants import ( 4 | NodeEnum, ViewerEnum, ViewerNavEnum 5 | ) 6 | 7 | from ..widgets.viewer_nav import NodeNavigationWidget 8 | 9 | 10 | class NodeGraphWidget(QtWidgets.QTabWidget): 11 | 12 | def __init__(self, parent=None): 13 | super(NodeGraphWidget, self).__init__(parent) 14 | self.setTabsClosable(True) 15 | self.setTabBarAutoHide(True) 16 | text_color = self.palette().text().color().getRgb() 17 | bg_color = QtGui.QColor( 18 | *ViewerEnum.BACKGROUND_COLOR.value).darker(120).getRgb() 19 | style_dict = { 20 | 'QWidget': { 21 | 'background-color': 'rgb({0},{1},{2})'.format( 22 | *ViewerEnum.BACKGROUND_COLOR.value 23 | ), 24 | }, 25 | 'QTabWidget::pane': { 26 | 'background': 'rgb({0},{1},{2})'.format( 27 | *ViewerEnum.BACKGROUND_COLOR.value 28 | ), 29 | 'border': '0px', 30 | 'border-top': '0px solid rgb({0},{1},{2})'.format(*bg_color), 31 | }, 32 | 'QTabBar::tab': { 33 | 'background': 'rgb({0},{1},{2})'.format(*bg_color), 34 | 'border': '0px solid black', 35 | 'color': 'rgba({0},{1},{2},30)'.format(*text_color), 36 | 'min-width': '10px', 37 | 'padding': '10px 20px', 38 | }, 39 | 'QTabBar::tab:selected': { 40 | 'color': 'rgb({0},{1},{2})'.format(*text_color), 41 | 'background': 'rgb({0},{1},{2})'.format( 42 | *ViewerNavEnum.BACKGROUND_COLOR.value 43 | ), 44 | 'border-top': '1px solid rgb({0},{1},{2})' 45 | .format(*NodeEnum.SELECTED_BORDER_COLOR.value), 46 | }, 47 | 'QTabBar::tab:hover': { 48 | 'color': 'rgb({0},{1},{2})'.format(*text_color), 49 | 'border-top': '1px solid rgb({0},{1},{2})' 50 | .format(*NodeEnum.SELECTED_BORDER_COLOR.value), 51 | } 52 | } 53 | stylesheet = '' 54 | for css_class, css in style_dict.items(): 55 | style = '{} {{\n'.format(css_class) 56 | for elm_name, elm_val in css.items(): 57 | style += ' {}:{};\n'.format(elm_name, elm_val) 58 | style += '}\n' 59 | stylesheet += style 60 | self.setStyleSheet(stylesheet) 61 | 62 | def add_viewer(self, viewer, name, node_id): 63 | self.addTab(viewer, name) 64 | index = self.indexOf(viewer) 65 | self.setTabToolTip(index, node_id) 66 | self.setCurrentIndex(index) 67 | 68 | def remove_viewer(self, viewer): 69 | index = self.indexOf(viewer) 70 | self.removeTab(index) 71 | 72 | 73 | class SubGraphWidget(QtWidgets.QWidget): 74 | 75 | def __init__(self, parent=None, graph=None): 76 | super(SubGraphWidget, self).__init__(parent) 77 | self._graph = graph 78 | self._navigator = NodeNavigationWidget() 79 | self._layout = QtWidgets.QVBoxLayout(self) 80 | self._layout.setContentsMargins(0, 0, 0, 0) 81 | self._layout.setSpacing(1) 82 | self._layout.addWidget(self._navigator) 83 | 84 | self._viewer_widgets = {} 85 | self._viewer_current = None 86 | 87 | @property 88 | def navigator(self): 89 | return self._navigator 90 | 91 | def add_viewer(self, viewer, name, node_id): 92 | if viewer in self._viewer_widgets: 93 | return 94 | 95 | if self._viewer_current: 96 | self.hide_viewer(self._viewer_current) 97 | 98 | self._navigator.add_label_item(name, node_id) 99 | self._layout.addWidget(viewer) 100 | self._viewer_widgets[viewer] = node_id 101 | self._viewer_current = viewer 102 | self._viewer_current.show() 103 | 104 | def remove_viewer(self, viewer=None): 105 | if viewer is None and self._viewer_current: 106 | viewer = self._viewer_current 107 | node_id = self._viewer_widgets.pop(viewer) 108 | self._navigator.remove_label_item(node_id) 109 | self._layout.removeWidget(viewer) 110 | viewer.deleteLater() 111 | 112 | def hide_viewer(self, viewer): 113 | self._layout.removeWidget(viewer) 114 | viewer.hide() 115 | 116 | def show_viewer(self, viewer): 117 | if viewer == self._viewer_current: 118 | self._viewer_current.show() 119 | return 120 | if viewer in self._viewer_widgets: 121 | if self._viewer_current: 122 | self.hide_viewer(self._viewer_current) 123 | self._layout.addWidget(viewer) 124 | self._viewer_current = viewer 125 | self._viewer_current.show() 126 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/widgets/scene.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from Qt import QtGui, QtCore, QtWidgets 3 | 4 | from ..constants import ViewerEnum 5 | 6 | 7 | class NodeScene(QtWidgets.QGraphicsScene): 8 | 9 | def __init__(self, parent=None): 10 | super(NodeScene, self).__init__(parent) 11 | self._grid_mode = ViewerEnum.GRID_DISPLAY_LINES.value 12 | self._grid_color = ViewerEnum.GRID_COLOR.value 13 | self._bg_color = ViewerEnum.BACKGROUND_COLOR.value 14 | self.setBackgroundBrush(QtGui.QColor(*self._bg_color)) 15 | 16 | def __repr__(self): 17 | cls_name = str(self.__class__.__name__) 18 | return '<{}("{}") object at {}>'.format( 19 | cls_name, self.viewer(), hex(id(self))) 20 | 21 | # def _draw_text(self, painter, pen): 22 | # font = QtGui.QFont() 23 | # font.setPixelSize(48) 24 | # painter.setFont(font) 25 | # parent = self.viewer() 26 | # pos = QtCore.QPoint(20, parent.height() - 20) 27 | # painter.setPen(pen) 28 | # painter.drawText(parent.mapToScene(pos), 'Not Editable') 29 | 30 | def _draw_grid(self, painter, rect, pen, grid_size): 31 | """ 32 | draws the grid lines in the scene. 33 | 34 | Args: 35 | painter (QtGui.QPainter): painter object. 36 | rect (QtCore.QRectF): rect object. 37 | pen (QtGui.QPen): pen object. 38 | grid_size (int): grid size. 39 | """ 40 | left = int(rect.left()) 41 | right = int(rect.right()) 42 | top = int(rect.top()) 43 | bottom = int(rect.bottom()) 44 | 45 | first_left = left - (left % grid_size) 46 | first_top = top - (top % grid_size) 47 | 48 | lines = [] 49 | lines.extend([ 50 | QtCore.QLineF(x, top, x, bottom) 51 | for x in range(first_left, right, grid_size) 52 | ]) 53 | lines.extend([ 54 | QtCore.QLineF(left, y, right, y) 55 | for y in range(first_top, bottom, grid_size)] 56 | ) 57 | 58 | painter.setPen(pen) 59 | painter.drawLines(lines) 60 | 61 | def _draw_dots(self, painter, rect, pen, grid_size): 62 | """ 63 | draws the grid dots in the scene. 64 | 65 | Args: 66 | painter (QtGui.QPainter): painter object. 67 | rect (QtCore.QRectF): rect object. 68 | pen (QtGui.QPen): pen object. 69 | grid_size (int): grid size. 70 | """ 71 | zoom = self.viewer().get_zoom() 72 | if zoom < 0: 73 | grid_size = int(abs(zoom) / 0.3 + 1) * grid_size 74 | 75 | left = int(rect.left()) 76 | right = int(rect.right()) 77 | top = int(rect.top()) 78 | bottom = int(rect.bottom()) 79 | 80 | first_left = left - (left % grid_size) 81 | first_top = top - (top % grid_size) 82 | 83 | pen.setWidth(grid_size / 10) 84 | painter.setPen(pen) 85 | 86 | [painter.drawPoint(int(x), int(y)) 87 | for x in range(first_left, right, grid_size) 88 | for y in range(first_top, bottom, grid_size)] 89 | 90 | def drawBackground(self, painter, rect): 91 | super(NodeScene, self).drawBackground(painter, rect) 92 | 93 | painter.save() 94 | painter.setRenderHint(QtGui.QPainter.Antialiasing, False) 95 | painter.setBrush(self.backgroundBrush()) 96 | 97 | if self._grid_mode is ViewerEnum.GRID_DISPLAY_DOTS.value: 98 | pen = QtGui.QPen(QtGui.QColor(*self.grid_color), 0.65) 99 | self._draw_dots(painter, rect, pen, ViewerEnum.GRID_SIZE.value) 100 | 101 | elif self._grid_mode is ViewerEnum.GRID_DISPLAY_LINES.value: 102 | zoom = self.viewer().get_zoom() 103 | if zoom > -0.5: 104 | pen = QtGui.QPen(QtGui.QColor(*self.grid_color), 0.65) 105 | self._draw_grid( 106 | painter, rect, pen, ViewerEnum.GRID_SIZE.value 107 | ) 108 | 109 | color = QtGui.QColor(*self._bg_color).darker(150) 110 | if zoom < -0.0: 111 | color = color.darker(100 - int(zoom * 110)) 112 | pen = QtGui.QPen(color, 0.65) 113 | self._draw_grid( 114 | painter, rect, pen, ViewerEnum.GRID_SIZE.value * 8 115 | ) 116 | 117 | painter.restore() 118 | 119 | def mousePressEvent(self, event): 120 | selected_nodes = self.viewer().selected_nodes() 121 | if self.viewer(): 122 | self.viewer().sceneMousePressEvent(event) 123 | super(NodeScene, self).mousePressEvent(event) 124 | keep_selection = any([ 125 | event.button() == QtCore.Qt.MiddleButton, 126 | event.button() == QtCore.Qt.RightButton, 127 | event.modifiers() == QtCore.Qt.AltModifier 128 | ]) 129 | if keep_selection: 130 | for node in selected_nodes: 131 | node.setSelected(True) 132 | 133 | def mouseMoveEvent(self, event): 134 | if self.viewer(): 135 | self.viewer().sceneMouseMoveEvent(event) 136 | super(NodeScene, self).mouseMoveEvent(event) 137 | 138 | def mouseReleaseEvent(self, event): 139 | if self.viewer(): 140 | self.viewer().sceneMouseReleaseEvent(event) 141 | super(NodeScene, self).mouseReleaseEvent(event) 142 | 143 | def viewer(self): 144 | return self.views()[0] if self.views() else None 145 | 146 | @property 147 | def grid_mode(self): 148 | return self._grid_mode 149 | 150 | @grid_mode.setter 151 | def grid_mode(self, mode=None): 152 | if mode is None: 153 | mode = ViewerEnum.GRID_DISPLAY_LINES.value 154 | self._grid_mode = mode 155 | 156 | @property 157 | def grid_color(self): 158 | return self._grid_color 159 | 160 | @grid_color.setter 161 | def grid_color(self, color=(0, 0, 0)): 162 | self._grid_color = color 163 | 164 | @property 165 | def background_color(self): 166 | return self._bg_color 167 | 168 | @background_color.setter 169 | def background_color(self, color=(0, 0, 0)): 170 | self._bg_color = color 171 | self.setBackgroundBrush(QtGui.QColor(*self._bg_color)) 172 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/widgets/tab_search.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import re 3 | from collections import OrderedDict 4 | 5 | from Qt import QtCore, QtWidgets, QtGui 6 | 7 | from ..constants import ViewerEnum, ViewerNavEnum 8 | 9 | 10 | class TabSearchCompleter(QtWidgets.QCompleter): 11 | """ 12 | QCompleter adapted from: 13 | https://stackoverflow.com/questions/5129211/qcompleter-custom-completion-rules 14 | """ 15 | 16 | def __init__(self, nodes=None, parent=None): 17 | super(TabSearchCompleter, self).__init__(nodes, parent) 18 | self.setCompletionMode(self.PopupCompletion) 19 | self.setCaseSensitivity(QtCore.Qt.CaseInsensitive) 20 | self._local_completion_prefix = '' 21 | self._using_orig_model = False 22 | self._source_model = None 23 | self._filter_model = None 24 | 25 | def splitPath(self, path): 26 | self._local_completion_prefix = path 27 | self.updateModel() 28 | 29 | if self._filter_model.rowCount() == 0: 30 | self._using_orig_model = False 31 | self._filter_model.setSourceModel(QtCore.QStringListModel([])) 32 | return [] 33 | return [] 34 | 35 | def updateModel(self): 36 | if not self._using_orig_model: 37 | self._filter_model.setSourceModel(self._source_model) 38 | 39 | pattern = QtCore.QRegExp(self._local_completion_prefix, 40 | QtCore.Qt.CaseInsensitive, 41 | QtCore.QRegExp.FixedString) 42 | self._filter_model.setFilterRegExp(pattern) 43 | 44 | def setModel(self, model): 45 | self._source_model = model 46 | self._filter_model = QtCore.QSortFilterProxyModel(self) 47 | self._filter_model.setSourceModel(self._source_model) 48 | super(TabSearchCompleter, self).setModel(self._filter_model) 49 | self._using_orig_model = True 50 | 51 | 52 | class TabSearchLineEditWidget(QtWidgets.QLineEdit): 53 | 54 | tab_pressed = QtCore.Signal() 55 | 56 | def __init__(self, parent=None): 57 | super(TabSearchLineEditWidget, self).__init__(parent) 58 | self.setAttribute(QtCore.Qt.WA_MacShowFocusRect, 0) 59 | self.setMinimumSize(200, 22) 60 | text_color = self.palette().text().color().getRgb() 61 | selected_color = self.palette().highlight().color().getRgb() 62 | style_dict = { 63 | 'QLineEdit': { 64 | 'color': 'rgb({0},{1},{2})'.format(*text_color), 65 | 'border': '1px solid rgb({0},{1},{2})'.format( 66 | *selected_color 67 | ), 68 | 'border-radius': '3px', 69 | 'padding': '2px 4px', 70 | 'margin': '2px 4px 8px 4px', 71 | 'background': 'rgb({0},{1},{2})'.format( 72 | *ViewerNavEnum.BACKGROUND_COLOR.value 73 | ), 74 | 'selection-background-color': 'rgba({0},{1},{2},200)' 75 | .format(*selected_color), 76 | } 77 | } 78 | stylesheet = '' 79 | for css_class, css in style_dict.items(): 80 | style = '{} {{\n'.format(css_class) 81 | for elm_name, elm_val in css.items(): 82 | style += ' {}:{};\n'.format(elm_name, elm_val) 83 | style += '}\n' 84 | stylesheet += style 85 | self.setStyleSheet(stylesheet) 86 | 87 | def keyPressEvent(self, event): 88 | super(TabSearchLineEditWidget, self).keyPressEvent(event) 89 | if event.key() == QtCore.Qt.Key_Tab: 90 | self.tab_pressed.emit() 91 | 92 | 93 | class TabSearchMenuWidget(QtWidgets.QMenu): 94 | 95 | search_submitted = QtCore.Signal(str) 96 | 97 | def __init__(self, node_dict=None): 98 | super(TabSearchMenuWidget, self).__init__() 99 | 100 | self.line_edit = TabSearchLineEditWidget() 101 | self.line_edit.tab_pressed.connect(self._close) 102 | 103 | self._node_dict = node_dict or {} 104 | if self._node_dict: 105 | self._generate_items_from_node_dict() 106 | 107 | search_widget = QtWidgets.QWidgetAction(self) 108 | search_widget.setDefaultWidget(self.line_edit) 109 | self.addAction(search_widget) 110 | 111 | text_color = self.palette().text().color().getRgb() 112 | selected_color = self.palette().highlight().color().getRgb() 113 | style_dict = { 114 | 'QMenu': { 115 | 'color': 'rgb({0},{1},{2})'.format(*text_color), 116 | 'background-color': 'rgb({0},{1},{2})'.format( 117 | *ViewerEnum.BACKGROUND_COLOR.value 118 | ), 119 | 'border': '1px solid rgba({0},{1},{2},30)'.format(*text_color), 120 | 'border-radius': '3px', 121 | }, 122 | 'QMenu::item': { 123 | 'padding': '5px 18px 2px', 124 | 'background-color': 'transparent', 125 | }, 126 | 'QMenu::item:selected': { 127 | 'color': 'rgb({0},{1},{2})'.format(*text_color), 128 | 'background-color': 'rgba({0},{1},{2},200)' 129 | .format(*selected_color), 130 | }, 131 | 'QMenu::separator': { 132 | 'height': '1px', 133 | 'background': 'rgba({0},{1},{2}, 50)'.format(*text_color), 134 | 'margin': '4px 8px', 135 | } 136 | } 137 | self._menu_stylesheet = '' 138 | for css_class, css in style_dict.items(): 139 | style = '{} {{\n'.format(css_class) 140 | for elm_name, elm_val in css.items(): 141 | style += ' {}:{};\n'.format(elm_name, elm_val) 142 | style += '}\n' 143 | self._menu_stylesheet += style 144 | self.setStyleSheet(self._menu_stylesheet) 145 | 146 | self._actions = {} 147 | self._menus = {} 148 | self._searched_actions = [] 149 | 150 | self._block_submit = False 151 | 152 | self.rebuild = False 153 | 154 | self._wire_signals() 155 | 156 | def __repr__(self): 157 | return '<{} at {}>'.format(self.__class__.__name__, hex(id(self))) 158 | 159 | def keyPressEvent(self, event): 160 | super(TabSearchMenuWidget, self).keyPressEvent(event) 161 | self.line_edit.keyPressEvent(event) 162 | 163 | @staticmethod 164 | def _fuzzy_finder(key, collection): 165 | suggestions = [] 166 | pattern = '.*?'.join(key.lower()) 167 | regex = re.compile(pattern) 168 | for item in collection: 169 | match = regex.search(item.lower()) 170 | if match: 171 | suggestions.append((len(match.group()), match.start(), item)) 172 | 173 | return [x for _, _, x in sorted(suggestions)] 174 | 175 | def _wire_signals(self): 176 | self.line_edit.returnPressed.connect(self._on_search_submitted) 177 | self.line_edit.textChanged.connect(self._on_text_changed) 178 | 179 | def _on_text_changed(self, text): 180 | self._clear_actions() 181 | 182 | if not text: 183 | self._set_menu_visible(True) 184 | return 185 | 186 | self._set_menu_visible(False) 187 | 188 | action_names = self._fuzzy_finder(text, self._actions.keys()) 189 | 190 | self._searched_actions = [self._actions[name] for name in action_names] 191 | self.addActions(self._searched_actions) 192 | 193 | if self._searched_actions: 194 | self.setActiveAction(self._searched_actions[0]) 195 | 196 | def _clear_actions(self): 197 | for action in self._searched_actions: 198 | self.removeAction(action) 199 | action.triggered.connect(self._on_search_submitted) 200 | del self._searched_actions[:] 201 | 202 | def _set_menu_visible(self, visible): 203 | for menu in self._menus.values(): 204 | menu.menuAction().setVisible(visible) 205 | 206 | def _close(self): 207 | self._set_menu_visible(False) 208 | self.setVisible(False) 209 | self.menuAction().setVisible(False) 210 | self._block_submit = True 211 | 212 | def _show(self): 213 | self.line_edit.setText("") 214 | self.line_edit.setFocus() 215 | self._set_menu_visible(True) 216 | self._block_submit = False 217 | self.exec_(QtGui.QCursor.pos()) 218 | 219 | def _on_search_submitted(self): 220 | if not self._block_submit: 221 | action = self.sender() 222 | if type(action) is not QtWidgets.QAction: 223 | if len(self._searched_actions) > 0: 224 | action = self._searched_actions[0] 225 | else: 226 | self._close() 227 | return 228 | 229 | text = action.text() 230 | node_type = self._node_dict.get(text) 231 | if node_type: 232 | self.search_submitted.emit(node_type) 233 | 234 | self._close() 235 | 236 | def build_menu_tree(self): 237 | node_types = sorted(self._node_dict.values()) 238 | node_names = sorted(self._node_dict.keys()) 239 | menu_tree = OrderedDict() 240 | 241 | max_depth = 0 242 | for node_type in node_types: 243 | trees = '.'.join(node_type.split('.')[:-1]).split('::') 244 | for depth, menu_name in enumerate(trees): 245 | new_menu = None 246 | menu_path = '::'.join(trees[:depth + 1]) 247 | if depth in menu_tree.keys(): 248 | if menu_name not in menu_tree[depth].keys(): 249 | new_menu = QtWidgets.QMenu(menu_name) 250 | new_menu.keyPressEvent = self.keyPressEvent 251 | new_menu.setStyleSheet(self._menu_stylesheet) 252 | menu_tree[depth][menu_path] = new_menu 253 | else: 254 | new_menu = QtWidgets.QMenu(menu_name) 255 | new_menu.setStyleSheet(self._menu_stylesheet) 256 | menu_tree[depth] = {menu_path: new_menu} 257 | if depth > 0 and new_menu: 258 | new_menu.parentPath = '::'.join(trees[:depth]) 259 | 260 | max_depth = max(max_depth, depth) 261 | 262 | for i in range(max_depth+1): 263 | menus = menu_tree[i] 264 | for menu_path, menu in menus.items(): 265 | self._menus[menu_path] = menu 266 | if i == 0: 267 | self.addMenu(menu) 268 | else: 269 | parent_menu = self._menus[menu.parentPath] 270 | parent_menu.addMenu(menu) 271 | 272 | for name in node_names: 273 | action = QtWidgets.QAction(name, self) 274 | action.setText(name) 275 | action.triggered.connect(self._on_search_submitted) 276 | self._actions[name] = action 277 | 278 | menu_name = self._node_dict[name] 279 | menu_path = '.'.join(menu_name.split('.')[:-1]) 280 | 281 | if menu_path in self._menus.keys(): 282 | self._menus[menu_path].addAction(action) 283 | else: 284 | self.addAction(action) 285 | 286 | def set_nodes(self, node_dict=None): 287 | if not self._node_dict or self.rebuild: 288 | self._node_dict.clear() 289 | self._clear_actions() 290 | self._set_menu_visible(False) 291 | for menu in self._menus.values(): 292 | self.removeAction(menu.menuAction()) 293 | self._actions.clear() 294 | self._menus.clear() 295 | for name, node_types in node_dict.items(): 296 | if len(node_types) == 1: 297 | self._node_dict[name] = node_types[0] 298 | continue 299 | for node_id in node_types: 300 | self._node_dict['{} ({})'.format(name, node_id)] = node_id 301 | self.build_menu_tree() 302 | self.rebuild = False 303 | 304 | self._show() 305 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/NodeGraphQt/widgets/viewer_nav.py: -------------------------------------------------------------------------------- 1 | from Qt import QtWidgets, QtCore, QtGui 2 | 3 | from ..constants import NodeEnum, ViewerNavEnum 4 | 5 | 6 | class NodeNavigationDelagate(QtWidgets.QStyledItemDelegate): 7 | 8 | def paint(self, painter, option, index): 9 | """ 10 | Args: 11 | painter (QtGui.QPainter): 12 | option (QtGui.QStyleOptionViewItem): 13 | index (QtCore.QModelIndex): 14 | """ 15 | if index.column() != 0: 16 | super(NodeNavigationDelagate, self).paint(painter, option, index) 17 | return 18 | 19 | item = index.model().item(index.row(), index.column()) 20 | 21 | margin = 1.0, 1.0 22 | rect = QtCore.QRectF( 23 | option.rect.x() + margin[0], 24 | option.rect.y() + margin[1], 25 | option.rect.width() - (margin[0] * 2), 26 | option.rect.height() - (margin[1] * 2) 27 | ) 28 | 29 | painter.save() 30 | painter.setPen(QtCore.Qt.NoPen) 31 | painter.setBrush(QtCore.Qt.NoBrush) 32 | painter.setRenderHint(QtGui.QPainter.Antialiasing, True) 33 | 34 | # background. 35 | bg_color = QtGui.QColor(*ViewerNavEnum.ITEM_COLOR.value) 36 | itm_color = QtGui.QColor(80, 128, 123) 37 | if option.state & QtWidgets.QStyle.State_Selected: 38 | bg_color = bg_color.lighter(120) 39 | itm_color = QtGui.QColor(*NodeEnum.SELECTED_BORDER_COLOR.value) 40 | 41 | roundness = 2.0 42 | painter.setBrush(bg_color) 43 | painter.drawRoundedRect(rect, roundness, roundness) 44 | 45 | if index.row() != 0: 46 | txt_offset = 8.0 47 | m = 6.0 48 | x = rect.left() + 2.0 + m 49 | y = rect.top() + m + 2 50 | h = rect.height() - (m * 2) - 2 51 | painter.setBrush(itm_color) 52 | for i in range(4): 53 | itm_rect = QtCore.QRectF(x, y, 1.3, h) 54 | painter.drawRoundedRect(itm_rect, 1.0, 1.0) 55 | x += 2.0 56 | y += 2 57 | h -= 4 58 | else: 59 | txt_offset = 5.0 60 | x = rect.left() + 4.0 61 | size = 10.0 62 | for clr in [QtGui.QColor(0, 0, 0, 80), itm_color]: 63 | itm_rect = QtCore.QRectF( 64 | x, rect.center().y() - (size / 2), size, size) 65 | painter.setBrush(clr) 66 | painter.drawRoundedRect(itm_rect, 2.0, 2.0) 67 | size -= 5.0 68 | x += 2.5 69 | 70 | # text 71 | pen_color = option.palette.text().color() 72 | pen = QtGui.QPen(pen_color, 0.5) 73 | pen.setCapStyle(QtCore.Qt.RoundCap) 74 | painter.setPen(pen) 75 | 76 | font = painter.font() 77 | font_metrics = QtGui.QFontMetrics(font) 78 | font_width = font_metrics.horizontalAdvance( 79 | item.text().replace(' ', '_') 80 | ) 81 | font_height = font_metrics.height() 82 | text_rect = QtCore.QRectF( 83 | rect.center().x() - (font_width / 2) + txt_offset, 84 | rect.center().y() - (font_height / 2), 85 | font_width, font_height) 86 | painter.drawText(text_rect, item.text()) 87 | painter.restore() 88 | 89 | 90 | class NodeNavigationWidget(QtWidgets.QListView): 91 | 92 | navigation_changed = QtCore.Signal(str, list) 93 | 94 | def __init__(self, parent=None): 95 | super(NodeNavigationWidget, self).__init__(parent) 96 | self.setSelectionMode(self.SingleSelection) 97 | self.setResizeMode(self.Adjust) 98 | self.setViewMode(self.ListMode) 99 | self.setFlow(self.LeftToRight) 100 | self.setDragEnabled(False) 101 | self.setMinimumHeight(20) 102 | self.setMaximumHeight(36) 103 | self.setSpacing(0) 104 | 105 | # self.viewport().setAutoFillBackground(False) 106 | self.setStyleSheet( 107 | 'QListView {{border: 0px;background-color: rgb({0},{1},{2});}}' 108 | .format(*ViewerNavEnum.BACKGROUND_COLOR.value) 109 | ) 110 | 111 | self.setItemDelegate(NodeNavigationDelagate(self)) 112 | self.setModel(QtGui.QStandardItemModel()) 113 | 114 | def keyPressEvent(self, event): 115 | event.ignore() 116 | 117 | def mouseReleaseEvent(self, event): 118 | super(NodeNavigationWidget, self).mouseReleaseEvent(event) 119 | if not self.selectedIndexes(): 120 | return 121 | index = self.selectedIndexes()[0] 122 | rows = reversed(range(1, self.model().rowCount())) 123 | if index.row() == 0: 124 | rows = [r for r in rows if r > 0] 125 | else: 126 | rows = [r for r in rows if index.row() < r] 127 | if not rows: 128 | return 129 | rm_node_ids = [self.model().item(r, 0).toolTip() for r in rows] 130 | node_id = self.model().item(index.row(), 0).toolTip() 131 | [self.model().removeRow(r) for r in rows] 132 | self.navigation_changed.emit(node_id, rm_node_ids) 133 | 134 | def clear(self): 135 | self.model().sourceMode().clear() 136 | 137 | def add_label_item(self, label, node_id): 138 | item = QtGui.QStandardItem(label) 139 | item.setToolTip(node_id) 140 | metrics = QtGui.QFontMetrics(item.font()) 141 | width = metrics.horizontalAdvance(item.text()) + 30 142 | item.setSizeHint(QtCore.QSize(width, 20)) 143 | self.model().appendRow(item) 144 | self.selectionModel().setCurrentIndex( 145 | self.model().indexFromItem(item), 146 | QtCore.QItemSelectionModel.ClearAndSelect) 147 | 148 | def remove_label_item(self, node_id): 149 | rows = reversed(range(1, self.model().rowCount())) 150 | node_ids = [self.model().item(r, 0).toolTip() for r in rows] 151 | if node_id not in node_ids: 152 | return 153 | index = node_ids.index(node_id) 154 | if index == 0: 155 | rows = [r for r in rows if r > 0] 156 | else: 157 | rows = [r for r in rows if index < r] 158 | [self.model().removeRow(r) for r in rows] 159 | 160 | 161 | if __name__ == '__main__': 162 | import sys 163 | 164 | def on_nav_changed(selected_id, remove_ids): 165 | print(selected_id, remove_ids) 166 | 167 | app = QtWidgets.QApplication(sys.argv) 168 | 169 | widget = NodeNavigationWidget() 170 | widget.navigation_changed.connect(on_nav_changed) 171 | 172 | widget.add_label_item('Close Graph', 'root') 173 | for i in range(1, 5): 174 | widget.add_label_item( 175 | 'group node {}'.format(i), 176 | 'node_id{}'.format(i) 177 | ) 178 | widget.resize(600, 30) 179 | widget.show() 180 | 181 | app.exec_() 182 | -------------------------------------------------------------------------------- /depthai_pipeline_graph/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luxonis/depthai_pipeline_graph/673f7673a37aac5dc3980adbc0f87fd6eb02683c/depthai_pipeline_graph/__init__.py -------------------------------------------------------------------------------- /media/age-gender-demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luxonis/depthai_pipeline_graph/673f7673a37aac5dc3980adbc0f87fd6eb02683c/media/age-gender-demo.jpg -------------------------------------------------------------------------------- /media/pipeline_graph_naming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luxonis/depthai_pipeline_graph/673f7673a37aac5dc3980adbc0f87fd6eb02683c/media/pipeline_graph_naming.png -------------------------------------------------------------------------------- /media/ports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luxonis/depthai_pipeline_graph/673f7673a37aac5dc3980adbc0f87fd6eb02683c/media/ports.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Qt.py>=1.3.0,<1.4.0 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('requirements.txt') as f: 4 | required = f.readlines() 5 | 6 | setup( 7 | name="depthai_pipeline_graph", 8 | version="0.0.5", 9 | packages=[*find_packages()], 10 | entry_points={ 11 | 'console_scripts': [ 12 | 'pipeline_graph = depthai_pipeline_graph.pipeline_graph:main' 13 | ] 14 | }, 15 | author="geaxgx", 16 | include_package_data=True, 17 | long_description=open("README.md", encoding="utf-8").read(), 18 | long_description_content_type="text/markdown", 19 | description="Tool to create graphs oh DepthAI pipelines", 20 | 21 | 22 | python_requires=">=3.6", 23 | install_requires=list(open("requirements.txt")), 24 | ) 25 | --------------------------------------------------------------------------------