├── .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 | 
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 |

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 | 
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 |
--------------------------------------------------------------------------------