├── .github
└── workflows
│ └── codeql-analysis.yml
├── .gitignore
├── CHANGES.rst
├── CONTRIBUTING.md
├── LICENSE.txt
├── MANIFEST.in
├── NodeEditorDocs.rst
├── README.md
├── Todo.txt
├── VVS Documentation EN Final .pdf
├── VVS Simple Usecase.webm
├── We Just Went Open Source !
├── docs
├── Makefile
├── build.py
├── make.bat
├── screenshot-calculator.png
├── screenshot-example.png
└── source
│ ├── coding_standards.md
│ ├── conf.py
│ ├── evaluation.rst
│ ├── events.rst
│ ├── index.rst
│ ├── introduction.rst
│ ├── releasenotes
│ ├── 1.0.0.rst
│ └── index.rst
│ ├── rst
│ ├── events_scene.rst
│ ├── events_scene_history.rst
│ ├── nodeeditor.node_content_widget.rst
│ ├── nodeeditor.node_edge.rst
│ ├── nodeeditor.node_edge_dragging.rst
│ ├── nodeeditor.node_edge_intersect.rst
│ ├── nodeeditor.node_edge_rerouting.rst
│ ├── nodeeditor.node_edge_snapping.rst
│ ├── nodeeditor.node_edge_validators.rst
│ ├── nodeeditor.node_editor_widget.rst
│ ├── nodeeditor.node_editor_window.rst
│ ├── nodeeditor.node_graphics_cutline.rst
│ ├── nodeeditor.node_graphics_edge.rst
│ ├── nodeeditor.node_graphics_edge_path.rst
│ ├── nodeeditor.node_graphics_node.rst
│ ├── nodeeditor.node_graphics_scene.rst
│ ├── nodeeditor.node_graphics_socket.rst
│ ├── nodeeditor.node_graphics_view.rst
│ ├── nodeeditor.node_node.rst
│ ├── nodeeditor.node_scene.rst
│ ├── nodeeditor.node_scene_clipboard.rst
│ ├── nodeeditor.node_scene_history.rst
│ ├── nodeeditor.node_serializable.rst
│ ├── nodeeditor.node_socket.rst
│ ├── nodeeditor.rst
│ └── nodeeditor.utils.rst
│ └── serialization.rst
├── flow_charts
├── All Diagrams.drawio
├── Flowchart MenuBar.drawio
├── GUI Digram.drawio
├── Test Diagram.drawio
├── VVS Architecture.drawio
└── Vision Analysis and Design.drawio
├── requirements.txt
├── setup.py
├── tox.ini
└── vvs_app
├── QRoundPB.py
├── Testing.py
├── VVS-Help.chm
├── editor_files_wdg.py
├── editor_node_list.py
├── editor_properties_list.py
├── editor_settings_wnd.py
├── editor_user_nodes_list.py
├── global_switches.py
├── graph_graphics.py
├── icons
├── Dark
│ ├── Loop.png
│ ├── Orientation.png
│ ├── Row Code.png
│ ├── Settings.png
│ ├── VVS_Logo_Thick.png
│ ├── VVS_White1.png
│ ├── VVS_White2.png
│ ├── VVS_White_Splash.png
│ ├── add.png
│ ├── and.png
│ ├── close.png
│ ├── copy.png
│ ├── divide.png
│ ├── edit.png
│ ├── equal.png
│ ├── event.png
│ ├── exit.png
│ ├── if.png
│ ├── less_than.png
│ ├── library.png
│ ├── more_than.png
│ ├── mul.png
│ ├── node design.png
│ ├── out.png
│ ├── print.png
│ ├── return.png
│ ├── run.png
│ ├── search.png
│ ├── sub.png
│ └── user input.png
└── light
│ ├── Edit.png
│ ├── Loop.png
│ ├── Row Code.png
│ ├── VVS_Logo_Thick.png
│ ├── VVS_White1.png
│ ├── VVS_White2.png
│ ├── VVS_White_Splash.png
│ ├── add.png
│ ├── and.png
│ ├── close.png
│ ├── copy.png
│ ├── divide.png
│ ├── equal.png
│ ├── event.png
│ ├── exit.png
│ ├── if.png
│ ├── less_than.png
│ ├── library.png
│ ├── more_than.png
│ ├── mul.png
│ ├── node design.png
│ ├── orientation.png
│ ├── out.png
│ ├── print.png
│ ├── return.png
│ ├── run.png
│ ├── search.png
│ ├── settings.png
│ ├── sub.png
│ └── user input.png
├── main.exe
├── main.py
├── master_designer_wnd.py
├── master_editor_wnd.py
├── master_node.py
├── master_window.py
├── node_edge.py
├── node_edge_dragging.py
├── node_edge_intersect.py
├── node_edge_rerouting.py
├── node_edge_snapping.py
├── node_edge_validators.py
├── node_editor_widget.py
├── node_editor_window.py
├── node_graphics_cutline.py
├── node_graphics_edge.py
├── node_graphics_edge_path.py
├── node_graphics_node.py
├── node_graphics_scene.py
├── node_node.py
├── node_scene.py
├── node_scene_clipboard.py
├── node_scene_history.py
├── node_serializable.py
├── node_socket.py
├── nodes
├── __init__.py
├── default_functions.py
├── nodes_configuration.py
├── user_functions_nodes.py
└── variables_nodes.py
├── qss
├── __init__.py
├── light_theme_colors.png
├── nodeeditor-dark.qss
├── nodeeditor-light.qss
├── nodeeditor-night.qss
├── nodeeditor.qss
├── nodeeditor.styl
├── nodeeditor_dark_resources.py
└── nodestyle.qss
└── utils.py
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '36 20 * * 6'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: Roboto-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'python' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v2
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v1
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 |
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54 | # If this step fails, then you should remove it and run the build manually (see below)
55 | - name: Autobuild
56 | uses: github/codeql-action/autobuild@v1
57 |
58 | # ℹ️ Command-line programs to run using the OS shell.
59 | # 📚 https://git.io/JvXDl
60 |
61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62 | # and modify them (or add more) to build your code if your project
63 | # uses a compiled language
64 |
65 | #- run: |
66 | # make bootstrap
67 | # make release
68 |
69 | - name: Perform CodeQL Analysis
70 | uses: github/codeql-action/analyze@v1
71 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | .idea
7 | .tox
8 |
9 |
10 | # Distribution / packaging
11 | .Python
12 | env/
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | /venv/
29 |
--------------------------------------------------------------------------------
/CHANGES.rst:
--------------------------------------------------------------------------------
1 | Changelog (PyNodeEditor)
2 | ========================
3 |
4 | 0.9.6
5 | -----
6 |
7 | - QDMGraphicsEdgeDirect and QDMGraphicsEdgeBezier no longer derive from QDMGraphicsEdge
8 | - QDMGraphicsEdge is now always used to represent graphics edge, and internaly got stored an instance of GraphicsEdgePathBase
9 | - logic of calculating Direct and Bezier edges has moved to node_graphics_edge_path.py file into respective classes GraphicsEdgePathDirect and GraphicsEdgePathBezier
10 | - possibility for NodeEditorWidget to override QDMGraphicsView class by setting `GraphicsView_class` class variable
11 |
12 | 0.9.5
13 | -----
14 |
15 | - fixed panning issue when drag edge caused by DragEdge being selectable edge
16 |
17 | 0.9.4
18 | -----
19 |
20 | - improvements to selection and edges
21 |
22 | 0.9.3
23 | -----
24 |
25 | - improved deserialization even with selections now
26 |
27 | 0.9.2
28 | -----
29 |
30 | - First polished and tested version of the library
31 | - After 54 tutorials: https://www.blenderfreak.com/tutorials/node-editor-tutorial-series/
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to PyQt-Node-Editor
2 |
3 | Bug fixes, feature additions, tests, documentation and more can be contributed
4 | via [issues](https://gitlab.com/pavel.krupala/pyqt-node-editor/issues) and/or [merge_requests](https://gitlab.com/pavel.krupala/pyqt-node-editor/merge_requests). All contributions are welcome.
5 |
6 | ## Contributors
7 |
8 | - Richard Boltze
9 | - RoniPerson
10 |
11 | ## Bug fixes, feature additions, etc.
12 |
13 | Please send a merge request to the master branch. Please include [documentation](https://pyqt-node-editor.readthedocs.io/en/latest/). Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://gitlab.com/pavel.krupala/pyqt-node-editor/issues/new),
14 |
15 | - Create a branch from master.
16 | - Develop bug fixes, features, tests, etc.
17 | - Test your code on Python 3.x.
18 | - Create a merge request to merge the changes from your branch to the PyQt-Node-Editor master.
19 |
20 | ### Guidelines
21 |
22 | - Separate code commits from reformatting commits.
23 | - Provide tests for any newly added code when possible.
24 |
25 | ## Reporting Issues
26 |
27 | When reporting issues, please include code that reproduces the issue and whenever possible, an image that demonstrates the issue. Please upload images to GitLab, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.
28 |
29 | The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework, try to replicate the issue just using PyQt-Node-Editor.
30 |
31 | ### Provide details
32 |
33 | - What did you do?
34 | - What did you expect to happen?
35 | - What actually happened?
36 | - What versions of PyQt-Node-Editor and Python are you using?
37 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Sherif Hany Moustafa
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 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include CHANGES.rst
2 | include LICENSE
3 | include README.rst
4 | include requirements.txt
5 |
6 | recursive-include tests *
7 | recursive-exclude * __pycache__
8 | recursive-exclude * *.py[co]
9 |
10 | recursive-include docs *.rst *.md conf.py Makefile make.bat *.jpg *.png *.gif
11 |
--------------------------------------------------------------------------------
/NodeEditorDocs.rst:
--------------------------------------------------------------------------------
1 | Welcome to PyQtNodeEditor
2 | ==========================
3 |
4 | .. image:: https://badge.fury.io/py/nodeeditor.svg
5 | :target: https://badge.fury.io/py/nodeeditor
6 |
7 | .. image:: https://readthedocs.org/projects/pyqt-node-editor/badge/?version=latest
8 | :target: https://pyqt-node-editor.readthedocs.io/en/latest/?badge=latest
9 | :alt: Documentation Status
10 |
11 |
12 | This package was created from the Node Editor written in PyQt5. The intention was to create a tutorial series
13 | describing the path to create a reusable nodeeditor which can be used in different projects.
14 | The tutorials are published on youtube for free. The full list of tutorials can be located here:
15 | https://www.blenderfreak.com/tutorials/node-editor-tutorial-series/
16 |
17 | Features
18 | --------
19 |
20 | - provides full framework for creating customizable graph, nodes, sockets and edges
21 | - full support for undo / redo and serialization into files in a VCS friendly way
22 | - support for implementing evaluation logic
23 | - hovering effects, dragging edges, cutting lines and a bunch more...
24 | - provided 2 examples on how node editor can be implemented
25 |
26 | Requirements
27 | ------------
28 |
29 | - Python 3.x
30 | - PyQt5 or PySide2 (using wrapper QtPy)
31 |
32 | Installation
33 | ------------
34 |
35 | ::
36 |
37 | $ pip install nodeeditor
38 |
39 |
40 | Or directly from source code to get the latest version
41 |
42 |
43 | ::
44 |
45 | $ pip install git+https://gitlab.com/pavel.krupala/pyqt-node-editor.git
46 |
47 |
48 | Or download the source code from gitlab::
49 |
50 | git clone https://gitlab.com/pavel.krupala/pyqt-node-editor.git
51 |
52 |
53 | Screenshots
54 | -----------
55 |
56 | .. image:: https://www.blenderfreak.com/media/products/NodeEditor/screenshot-calculator.png
57 | :alt: Screenshot of Calculator Example
58 |
59 | .. image:: https://www.blenderfreak.com/media/products/NodeEditor/screenshot-example.png
60 | :alt: Screenshot of Node Editor
61 |
62 | Other links
63 | -----------
64 |
65 | - `Documentation `_
66 |
67 | - `Contribute `_
68 |
69 | - `Issues `_
70 |
71 | - `Merge requests `_
72 |
73 | - `Changelog `_
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This software started as a university graduation project aimed to create an all in one software that allows anyone to just hop in and start programing using visual modular code blocks (nodes), kind of like (( Unreal Engines Blueprint Graph System )).. It enables the code to be translated from the Visual Scripting graph into any programming language syntax the user selects.
2 |
3 | ---------------------
4 | Simple use case..
5 |
6 | [Vvs Showcase-1.webm](https://github.com/Sheriff99yt/Vision_Visual_Scripting/assets/56188682/57f3fdca-cc0b-4c85-ba58-9ca3d5445f50)
7 |
8 |
9 | ---------------------
10 |
11 | Steps to Run Vision Visual Scripting
12 |
13 | 1- Download and Install PyCharm
14 | https://www.jetbrains.com/pycharm/download/#section=windows
15 |
16 |
17 |
18 | ---------------------
19 |
20 | 2- Make sure GitHub plug-in is installed
21 |
22 | 3- Click on Get From VCS
23 |
24 |
25 |
26 | ---------------------
27 |
28 | 4- Copy the Link from the GitHub Repo then Past it in PyCharm then click Clone
29 |
30 |
31 |
32 |
33 |
34 | ---------------------
35 |
36 | 5- Go to the requirements file and make sure all necessary libraries are installed
37 |
38 |
39 |
40 | ---------------------
41 |
42 | 6- Go to vvs_app/main.py
43 |
44 |
45 |
46 | ---------------------
47 |
48 | 7- Right click on main.py then click run
49 |
50 |
51 |
52 | Finally - This is what should apear
53 |
54 |
55 |
56 |
57 | ---------------------
58 | ---------------------
59 | ---------------------
60 | ---------------------
61 |
62 | **Notes:**
63 |
64 | - If you don't see run button then you need to configure a new Python interpreter environment.
65 |
66 | https://www.youtube.com/watch?v=GTtpypvLoeY&ab_channel=PyCharmbyJetBrains
67 |
68 | - Make sure Python is installed before doing step (3).
69 |
70 | - It's better to create a new empty environment for the project to have least issues.
71 |
--------------------------------------------------------------------------------
/Todo.txt:
--------------------------------------------------------------------------------
1 | # Language Bugs
2 | # C++
3 | # - c++ includes
4 | # - remove main function header declaration
5 | # - move using namespace std; form .cpp to .h file
6 | # Rust
7 | # - print doesn't work with normal variables
8 | #
9 | #
10 | # # inProgress
11 | #
12 | # # urgent
13 | # - B c++ code / C++ includes
14 | #
15 | # # ToDos
16 | # - B enabling input in the terminal / Adding windows cmd and running the python interpreter
17 | #
18 | # # Done
19 | # - A list type combobox
20 | # - A Graph background color
21 | # - Missing Icons
22 | # - Splash Screen
23 | # - B Add light Ui Modes
24 | # - S save all graphs & close all graphs QActions
25 | # - A Deleting Vars and Events.
26 | # - S functions category open and close indicators.
27 | # - S functions categories (Tree Widget).
28 | # - warning if the shortcut is already in use.
29 | # - hold reset btn to reset settings with a progress par.
30 | # - Socket name For each socket on the node itself.
31 | # - fixing input widgets.
32 | # - node auto sizing based on sockets and node content to compensate for the name lbl size and amongst other things.
33 | # - ** Always save before closing option.
34 | # - default node content serialisation.
--------------------------------------------------------------------------------
/VVS Documentation EN Final .pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/VVS Documentation EN Final .pdf
--------------------------------------------------------------------------------
/VVS Simple Usecase.webm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/VVS Simple Usecase.webm
--------------------------------------------------------------------------------
/We Just Went Open Source !:
--------------------------------------------------------------------------------
1 | Yaaaaayyy !!!
2 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SOURCEDIR = source
8 | BUILDDIR = build
9 |
10 | # Put it first so that "make" without argument is like "make help".
11 | help:
12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
13 |
14 | .PHONY: help Makefile
15 |
16 | # Catch-all target: route all unknown targets to Sphinx using the new
17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
18 | %: Makefile
19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/docs/build.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | os.system('make clean')
4 | os.system('make html')
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/screenshot-calculator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/docs/screenshot-calculator.png
--------------------------------------------------------------------------------
/docs/screenshot-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/docs/screenshot-example.png
--------------------------------------------------------------------------------
/docs/source/coding_standards.md:
--------------------------------------------------------------------------------
1 | ```eval_rst
2 | .. _coding-standards:
3 | ```
4 | # Coding Standards
5 |
6 | The following rules and guidelines are used throughout the nodeeditor package:
7 |
8 | ## File naming guidelines
9 |
10 | * files in the nodeeditor package start with ```node_```
11 | * files containing graphical representation (PyQt5 overridden classes) start with ```node_graphics_```
12 | * files for window/widget start with ```node_editor_```
13 |
14 | ## Coding guidelines
15 |
16 | * methods use Camel case naming
17 | * variables/properties use Snake case naming
18 |
19 | * The constructor ```__init__``` always contains all class variables for the entire class. This is helpful for new users, so they can
20 | just look at the constructor and read about all properties that class is using in one place. Nobody wants any
21 | surprises hidden in the code later
22 | * nodeeditor uses custom callbacks and listeners. Methods for adding callback functions
23 | are usually named ```addXYListener```
24 | * custom events are usually named ```onXY```
25 | * methods named ```doXY``` usually *do* certain tasks and also take care of low level operations
26 | * classes always contain methods in this order:
27 | * ```__init__```
28 | * python magic methods (i.e. ```__str__```), setters and getters
29 | * ```initXY``` functions
30 | * listener functions
31 | * nodeeditor event fuctions
32 | * nodeeditor ```doXY``` and ```getXY``` helping functions
33 | * Qt5 event functions
34 | * other functions
35 | * optionally overridden Qt ```paint``` method
36 | * ```serialize``` and ```deserialize``` methods at the end
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Configuration file for the Sphinx documentation builder.
4 | #
5 | # This file does only contain a selection of the most common options. For a
6 | # full list see the documentation:
7 | # http://www.sphinx-doc.org/en/master/config
8 |
9 | # -- Path setup --------------------------------------------------------------
10 |
11 | # If extensions (or modules to document with autodoc) are in another directory,
12 | # add these directories to sys.path here. If the directory is relative to the
13 | # documentation root, use os.path.abspath to make it absolute, like shown here.
14 | #
15 | import os
16 | import sys
17 | sys.path.insert(0, os.path.abspath('../..'))
18 | import nodeeditor
19 |
20 |
21 | # -- Project information -----------------------------------------------------
22 |
23 | project = 'NodeEditor'
24 | copyright = '2019, Pavel Křupala'
25 | author = 'Pavel Křupala'
26 |
27 | # The short X.Y version
28 | version = nodeeditor.__version__
29 | # The full version, including alpha/beta/rc tags
30 | release = nodeeditor.__version__
31 |
32 |
33 | # -- General configuration ---------------------------------------------------
34 |
35 | # If your documentation needs a minimal Sphinx version, state it here.
36 | #
37 | # needs_sphinx = '1.0'
38 |
39 | # Add any Sphinx extension module names here, as strings. They can be
40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
41 | # ones.
42 | import sphinx_rtd_theme
43 |
44 | extensions = [
45 | 'sphinx.ext.autodoc',
46 | 'sphinx.ext.autosectionlabel',
47 | 'sphinx_rtd_theme',
48 | 'sphinx.ext.todo',
49 | 'sphinx.ext.coverage',
50 | 'recommonmark',
51 | ]
52 |
53 | autosectionlabel_prefix_document = True
54 |
55 | autodoc_member_order = 'bysource'
56 | autoclass_content = "both"
57 |
58 | from recommonmark.transform import AutoStructify
59 | github_doc_root = 'https://github.com/rtfd/recommonmark/tree/master/doc/'
60 | def setup(app):
61 | app.add_config_value('recommonmark_config', {
62 | # 'url_resolver': lambda url: github_doc_root + url,
63 | 'auto_toc_tree_section': 'Contents',
64 | }, True)
65 | app.add_transform(AutoStructify)
66 |
67 | # Add any paths that contain templates here, relative to this directory.
68 | templates_path = ['_templates']
69 |
70 | # The suffix(es) of source filenames.
71 | # You can specify multiple suffix as a list of string:
72 | #
73 | source_suffix = ['.rst', '.md']
74 | # source_suffix = '.rst'
75 |
76 | # The master toctree document.
77 | master_doc = 'index'
78 |
79 | # The language for content autogenerated by Sphinx. Refer to documentation
80 | # for a list of supported languages.
81 | #
82 | # This is also used if you do content translation via gettext catalogs.
83 | # Usually you set "language" from the command line for these cases.
84 | language = None
85 |
86 | # List of patterns, relative to source directory, that match files and
87 | # directories to ignore when looking for source files.
88 | # This pattern also affects html_static_path and html_extra_path.
89 | exclude_patterns = []
90 |
91 | # The name of the Pygments (syntax highlighting) style to use.
92 | pygments_style = None
93 |
94 |
95 | # -- Options for HTML output -------------------------------------------------
96 |
97 | # The theme to use for HTML and HTML Help pages. See the documentation for
98 | # a list of builtin themes.
99 | #
100 | html_theme = 'sphinx_rtd_theme'
101 | html_theme_path = ["_themes", ]
102 |
103 | # Theme options are theme-specific and customize the look and feel of a theme
104 | # further. For a list of options available for each theme, see the
105 | # documentation.
106 | #
107 | # html_theme_options = {}
108 |
109 | # Add any paths that contain custom static files (such as style sheets) here,
110 | # relative to this directory. They are copied after the builtin static files,
111 | # so a file named "default.css" will overwrite the builtin "default.css".
112 | html_static_path = ['_static']
113 |
114 | # Custom sidebar templates, must be a dictionary that maps document names
115 | # to template names.
116 | #
117 | # The default sidebars (for documents that don't match any pattern) are
118 | # defined by theme itself. Builtin themes are using these templates by
119 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
120 | # 'searchbox.html']``.
121 | #
122 | # html_sidebars = {}
123 |
124 |
125 | # -- Options for HTMLHelp output ---------------------------------------------
126 |
127 | # Output file base name for HTML help builder.
128 | htmlhelp_basename = 'NodeEditordoc'
129 |
130 |
131 | # -- Options for LaTeX output ------------------------------------------------
132 |
133 | latex_elements = {
134 | # The paper size ('letterpaper' or 'a4paper').
135 | #
136 | # 'papersize': 'letterpaper',
137 |
138 | # The font size ('10pt', '11pt' or '12pt').
139 | #
140 | # 'pointsize': '10pt',
141 |
142 | # Additional stuff for the LaTeX preamble.
143 | #
144 | # 'preamble': '',
145 |
146 | # Latex figure (float) alignment
147 | #
148 | # 'figure_align': 'htbp',
149 | }
150 |
151 | # Grouping the document tree into LaTeX files. List of tuples
152 | # (source start file, target name, title,
153 | # author, documentclass [howto, manual, or own class]).
154 | latex_documents = [
155 | (master_doc, 'NodeEditor.tex', 'NodeEditor Documentation',
156 | 'Pavel Křupala', 'manual'),
157 | ]
158 |
159 |
160 | # -- Options for manual page output ------------------------------------------
161 |
162 | # One entry per manual page. List of tuples
163 | # (source start file, name, description, authors, manual section).
164 | man_pages = [
165 | (master_doc, 'nodeeditor', 'NodeEditor Documentation',
166 | [author], 1)
167 | ]
168 |
169 |
170 | # -- Options for Texinfo output ----------------------------------------------
171 |
172 | # Grouping the document tree into Texinfo files. List of tuples
173 | # (source start file, target name, title, author,
174 | # dir menu entry, description, category)
175 | texinfo_documents = [
176 | (master_doc, 'NodeEditor', 'NodeEditor Documentation',
177 | author, 'NodeEditor', 'One line description of project.',
178 | 'Miscellaneous'),
179 | ]
180 |
181 |
182 | # -- Options for Epub output -------------------------------------------------
183 |
184 | # Bibliographic Dublin Core info.
185 | epub_title = project
186 |
187 | # The unique identifier of the text. This can be a ISBN number
188 | # or the project homepage.
189 | #
190 | # epub_identifier = ''
191 |
192 | # A unique identification for the text.
193 | #
194 | # epub_uid = ''
195 |
196 | # A list of files that should not be packed into the epub file.
197 | epub_exclude_files = ['search.html']
198 |
--------------------------------------------------------------------------------
/docs/source/evaluation.rst:
--------------------------------------------------------------------------------
1 | .. _evaluation:
2 |
3 | Evaluation
4 | ==========
5 |
6 | TL;DR: The evaluation system uses
7 | :func:`~nodeeditor.node_node.Node.eval` and
8 | :func:`~nodeeditor.node_node.Node.evalChildren`. ``eval()`` method is supposed to be overriden by your own
9 | implementation. The evaluation logic uses Flags for marking the `Nodes` to be `Dirty` and/or `Invalid`.
10 |
11 | Evaluation Functions
12 | --------------------
13 |
14 | There are 2 main methods used for evaluation:
15 |
16 | - :func:`~nodeeditor.node_node.Node.eval`
17 | - :func:`~nodeeditor.node_node.Node.evalChildren`
18 |
19 | These functions are mutually exclusive. That means that ``evalChildren`` does **not** eval current `Node`,
20 | but only children of the current `Node`.
21 |
22 | By default the implementation of :func:`~nodeeditor.node_node.Node.eval` is "empty" and return 0. However
23 | it seems logical, that eval (if successfull) resets the `Node` not to be `Dirty` nor `Invalid`.
24 | This method is supposed to be overriden by your own implementation. As an example, you can check out
25 | the repository's ``examples/example_calculator`` to have an inspiration how to setup the
26 | `Node` evaluation on your own.
27 |
28 | The evaluation takes advantage of `Node` flags described below.
29 |
30 | :class:`~nodeeditor.node_node.Node` Flags
31 | -----------------------------------------
32 |
33 | Each :class:`~nodeeditor.node_node.Node` has 2 flags:
34 |
35 | - ``Dirty``
36 | - ``Invalid``
37 |
38 | The `Invalid` flag has always higher priority. That means when the `Node` is `Invalid` it
39 | doesn't matter if it is `Dirty` or not.
40 |
41 | To mark a node `Dirty` or `Invalid` there are respective methods :func:`~nodeeditor.node_node.Node.markDirty`
42 | and :func:`~nodeeditor.node_node.Node.markInvalid`. Both methods take `bool` parameter for the new state.
43 | You can mark `Node` dirty by setting the parameter to ``True``. Also you can un-mark the state by passing
44 | ``False`` value.
45 |
46 | For both flags there are 3 methods available:
47 |
48 | - :func:`~nodeeditor.node_node.Node.markInvalid` - to mark only the `Node`
49 | - :func:`~nodeeditor.node_node.Node.markChildrenInvalid` - to mark only the direct (first level) children of the `Node`
50 | - :func:`~nodeeditor.node_node.Node.markDescendantsInvalid` - to mark it self and all descendant children of the `Node`
51 |
52 | The same goes for the `Dirty` flag of course:
53 |
54 | - :func:`~nodeeditor.node_node.Node.markDirty` - to mark only the `Node`
55 | - :func:`~nodeeditor.node_node.Node.markChildrenDirty` - to mark only the direct (first level) children of the `Node`
56 | - :func:`~nodeeditor.node_node.Node.markDescendantsDirty` - to mark it self and all descendant children of the `Node`
57 |
58 | Descendants or Children are always connected to Output(s) of current `Node`.
59 |
60 | When a node is marked `Dirty` or `Invalid` event methods
61 | :func:`~nodeeditor.node_node.Node.onMarkedInvalid`
62 | :func:`~nodeeditor.node_node.Node.onMarkedDirty` are being called. By default, these methods do nothing.
63 | But still they are implemented in case you would like to override them and use in you own evaluation system.
64 |
65 |
--------------------------------------------------------------------------------
/docs/source/events.rst:
--------------------------------------------------------------------------------
1 | Event system
2 | ============
3 |
4 | Nodeeditor uses its own events (and tries to avoid using ``pyqtSignal``) to handle logic
5 | happening inside the Scene. If a class does handle some events, they are usually described
6 | at the top of the page in this documentation.
7 |
8 | Any of the events is subscribable to and the methods for registering callback are called:
9 |
10 | .. code-block:: python
11 |
12 | addListener(callback)
13 |
14 | You can register to any of these events any time.
15 |
16 | Events used in NodeEditor:
17 | --------------------------
18 |
19 | :class:`~nodeeditor.node_scene.Scene`
20 | +++++++++++++++++++++++++++++++++++++
21 |
22 | .. include:: rst/events_scene.rst
23 |
24 |
25 | :class:`~nodeeditor.node_scene_history.SceneHistory`
26 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
27 |
28 | .. include:: rst/events_scene_history.rst
29 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. NodeEditor documentation master file, created by
2 | sphinx-quickstart on Sat Jan 5 17:17:35 2019.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to NodeEditor's documentation!
7 | ======================================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 | :caption: Contents:
12 |
13 | introduction
14 | events
15 | serialization
16 | evaluation
17 | coding_standards
18 | releasenotes/index
19 | rst/nodeeditor
20 |
21 |
22 | Indices and tables
23 | ==================
24 |
25 | * :ref:`genindex`
26 | * :ref:`modindex`
27 | * :ref:`search`
28 |
--------------------------------------------------------------------------------
/docs/source/introduction.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../../README.rst
--------------------------------------------------------------------------------
/docs/source/releasenotes/1.0.0.rst:
--------------------------------------------------------------------------------
1 | 1.0.0 (unreleased)
2 | ------------------
3 |
4 | - Added first version of library
--------------------------------------------------------------------------------
/docs/source/releasenotes/index.rst:
--------------------------------------------------------------------------------
1 | Release Notes
2 | =============
3 |
4 | .. note:: Contributors please include release notes as needed or appropriate with your bug fixes, feature additions and tests.
5 |
6 | .. toctree::
7 | :maxdepth: 2
8 |
9 | 1.0.0
--------------------------------------------------------------------------------
/docs/source/rst/events_scene.rst:
--------------------------------------------------------------------------------
1 | `Has Been Modified`
2 | when something has changed in the `Scene`
3 | `Item Selected`
4 | when `Node` or `Edge` is selected
5 | `Items Deselected`
6 | when deselect everything appears
7 | `Drag Enter`
8 | when something is Dragged onto the `Scene`. Here we do allow or deny the drag
9 | `Drop`
10 | when we Drop something into the `Scene`
11 |
--------------------------------------------------------------------------------
/docs/source/rst/events_scene_history.rst:
--------------------------------------------------------------------------------
1 | `History Modified`
2 | after `History Stamp` has been stored or restored
3 | `History Stored`
4 | after `History Stamp` has been stored
5 | `History Restored`
6 | after `History Stamp` has been restored
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_content_widget.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_content_widget
2 |
3 | :py:mod:`node\_content\_widget` Module
4 | =======================================
5 |
6 | .. automodule:: nodeeditor.node_content_widget
7 |
8 | .. autoclass:: QDMNodeContentWidget
9 | :members:
10 | :undoc-members:
11 | :show-inheritance:
12 |
13 |
14 | .. autoclass:: QDMTextEdit
15 | :members:
16 | :undoc-members:
17 | :show-inheritance:
18 |
19 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_edge.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_edge
2 |
3 | :py:mod:`node\_edge` Module
4 | ============================
5 |
6 | .. automodule:: nodeeditor.node_edge
7 | :members: EDGE_TYPE_DIRECT, EDGE_TYPE_BEZIER
8 |
9 | .. _edge-type-constants:
10 |
11 | Edge Type Constants
12 | -------------------
13 |
14 | Edge Validators
15 | ---------------
16 |
17 | Edge Validator can be registered to Edge class using its method
18 | :class:`~nodeeditor.node_edge.Edge.registerEdgeValidator()`.
19 |
20 | Each validator callback takes 2 params: `start_socket` and `end_socket`.
21 | Validator also needs to return `True` or `False`. For example of validators
22 | have a look in :mod:`node\_edge\_validators` module.
23 |
24 | Here is an example how you can register the Edge Validator callbacks:
25 |
26 | .. code-block:: python
27 |
28 | from node_edge_validators import *
29 |
30 | Edge.registerEdgeValidator(edge_validator_debug)
31 | Edge.registerEdgeValidator(edge_cannot_connect_two_outputs_or_two_inputs)
32 | Edge.registerEdgeValidator(edge_cannot_connect_input_and_output_of_same_node)
33 |
34 |
35 |
36 | Edge Class
37 | ----------
38 |
39 | .. autoclass:: nodeeditor.node_edge.Edge
40 | :members:
41 | :undoc-members:
42 | :show-inheritance:
43 |
44 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_edge_dragging.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_edge_dragging
2 |
3 | :py:mod:`node\_edge\_dragging` Module
4 | =====================================
5 |
6 | .. automodule:: nodeeditor.node_edge_dragging
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_edge_intersect.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_edge_intersect
2 |
3 | :py:mod:`node\_edge\_intersect` Module
4 | =======================================
5 |
6 | .. automodule:: nodeeditor.node_edge_intersect
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_edge_rerouting.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_edge_rerouting
2 |
3 | :py:mod:`node\_edge\_rerouting` Module
4 | =======================================
5 |
6 | .. automodule:: nodeeditor.node_edge_rerouting
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_edge_snapping.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_edge_snapping
2 |
3 | :py:mod:`node\_edge\_snapping` Module
4 | =======================================
5 |
6 | .. automodule:: nodeeditor.node_edge_snapping
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_edge_validators.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_edge_validators
2 |
3 | :py:mod:`node\_edge\_validators` Module
4 | =======================================
5 |
6 | .. automodule:: nodeeditor.node_edge_validators
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_editor_widget.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_edgitor_widget
2 |
3 | :py:mod:`node\_editor\_widget` Module
4 | ======================================
5 |
6 | .. automodule:: nodeeditor.node_editor_widget
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_editor_window.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_edgitor_window
2 |
3 | :py:mod:`node\_editor\_window` Module
4 | ======================================
5 |
6 | .. automodule:: nodeeditor.node_editor_window
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_graphics_cutline.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_graphics_cutline
2 |
3 | :py:mod:`node\_graphics\_cutline` Module
4 | =========================================
5 |
6 | .. automodule:: nodeeditor.node_graphics_cutline
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_graphics_edge.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_graphics_edge
2 |
3 | :py:mod:`node\_graphics\_edge` Module
4 | ======================================
5 |
6 | .. automodule:: nodeeditor.node_graphics_edge
7 |
8 |
9 | `QDMGraphicsEdge` class
10 | ------------------------------
11 |
12 | .. autoclass:: QDMGraphicsEdge
13 | :members:
14 | :undoc-members:
15 | :show-inheritance:
16 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_graphics_edge_path.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_graphics_edge_path
2 |
3 | :py:mod:`node\_graphics\_edge\_path` Module
4 | ===========================================
5 |
6 | .. automodule:: nodeeditor.node_graphics_edge_path
7 | :members: EDGE_CP_ROUNDNESS
8 |
9 | Constants
10 | ---------
11 |
12 |
13 | `GraphicsEdgePathBase` base class
14 | ----------------------------------
15 |
16 | .. autoclass:: GraphicsEdgePathBase
17 | :members:
18 | :undoc-members:
19 | :show-inheritance:
20 |
21 | `GraphicsEdgePathDirect` class
22 | --------------------------------------
23 |
24 | .. autoclass:: GraphicsEdgePathDirect
25 | :members:
26 | :undoc-members:
27 | :show-inheritance:
28 |
29 | `GraphicsEdgePathBezier` class
30 | -------------------------------------
31 |
32 | .. autoclass:: GraphicsEdgePathBezier
33 | :members:
34 | :undoc-members:
35 | :show-inheritance:
36 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_graphics_node.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_graphics_node
2 |
3 | :py:mod:`node\_graphics\_node` Module
4 | ======================================
5 |
6 | .. automodule:: nodeeditor.node_graphics_node
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_graphics_scene.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_graphics_scene
2 |
3 | :py:mod:`node\_graphics\_scene` Module
4 | =======================================
5 |
6 | .. automodule:: nodeeditor.node_graphics_scene
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_graphics_socket.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_graphics_socket
2 |
3 | :py:mod:`node\_graphics\_socket` Module
4 | ========================================
5 |
6 | .. automodule:: nodeeditor.node_graphics_socket
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_graphics_view.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_graphics_view
2 |
3 | :py:mod:`node\_graphics\_view` Module
4 | ======================================
5 |
6 | .. automodule:: nodeeditor.node_graphics_view
7 | :members: MODE_NOOP, MODE_EDGE_DRAG, MODE_EDGE_CUT, MODE_EDGES_REROUTING, EDGE_DRAG_START_THRESHOLD
8 |
9 | Constants
10 | ---------
11 |
12 |
13 | `QDMGraphicsView` class
14 | -----------------------
15 |
16 | .. autoclass:: QDMGraphicsView
17 | :members:
18 | :undoc-members:
19 | :show-inheritance:
20 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_node.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_node
2 |
3 | :py:mod:`node\_node` Module
4 | ============================
5 |
6 | .. automodule:: nodeeditor.node_node
7 |
8 |
9 | .. autoclass:: Node
10 | :members:
11 | :undoc-members:
12 | :show-inheritance:
13 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_scene.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_scene
2 |
3 | :py:mod:`node\_scene` Module
4 | =============================
5 |
6 | .. automodule:: nodeeditor.node_scene
7 |
8 | Events
9 | ------
10 |
11 | .. include:: events_scene.rst
12 |
13 | Exceptions
14 | ----------
15 |
16 | .. autoclass:: InvalidFile
17 | :members:
18 | :undoc-members:
19 | :show-inheritance:
20 |
21 | Scene Class
22 | -----------
23 |
24 | .. autoclass:: Scene
25 | :members:
26 | :undoc-members:
27 | :show-inheritance:
28 |
29 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_scene_clipboard.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_scene_clipboard
2 |
3 | :py:mod:`node\_scene\_clipboard` Module
4 | ========================================
5 |
6 | .. automodule:: nodeeditor.node_scene_clipboard
7 | :members:
8 | :undoc-members:
9 | :show-inheritance:
10 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_scene_history.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_scene_history
2 |
3 | :py:mod:`node\_scene\_history` Module
4 | ======================================
5 |
6 | .. automodule:: nodeeditor.node_scene_history
7 |
8 | Events
9 | ------
10 |
11 | .. include:: events_scene_history.rst
12 |
13 | SceneHistory Class
14 | ------------------
15 |
16 | .. autoclass:: SceneHistory
17 | :members:
18 | :undoc-members:
19 | :show-inheritance:
20 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_serializable.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_serializable
2 |
3 | :py:mod:`node\_serializable` Module
4 | ====================================
5 |
6 | .. automodule:: nodeeditor.node_serializable
7 |
8 | .. autoclass:: nodeeditor.node_serializable.Serializable
9 | :members:
10 | :undoc-members:
11 | :show-inheritance:
12 |
13 | .. py:attribute:: id
14 |
15 | We set this property in the `constructor` because all of NodeEditor's serializable
16 | objects use this attribute to unique object identification. It is handy for
17 | referencing objects.
18 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.node_socket.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.node_socket
2 |
3 | :py:mod:`node\_socket` Module
4 | ==============================
5 |
6 | .. automodule:: nodeeditor.node_socket
7 | :members: LEFT_TOP, LEFT_CENTER, LEFT_BOTTOM, RIGHT_TOP, RIGHT_CENTER, RIGHT_BOTTOM
8 |
9 | .. _socket-position-constants:
10 |
11 | Socket Position Constants
12 | -------------------------
13 |
14 | .. .. autoattribute:: nodeeditor.node_socket.LEFT_TOP
15 | .. autoattribute:: nodeeditor.node_socket.LEFT_CENTER
16 | .. autoattribute:: nodeeditor.node_socket.LEFT_BOTTOM
17 | .. autoattribute:: nodeeditor.node_socket.RIGHT_TOP
18 | .. autoattribute:: nodeeditor.node_socket.RIGHT_CENTER
19 | .. autoattribute:: nodeeditor.node_socket.RIGHT_BOTTOM
20 |
21 |
22 | Socket Class
23 | ------------
24 |
25 | .. autoclass:: Socket
26 | :members:
27 | :undoc-members:
28 | :show-inheritance:
29 |
30 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.rst:
--------------------------------------------------------------------------------
1 | nodeeditor Package
2 | ==================
3 |
4 |
5 | .. toctree::
6 |
7 | nodeeditor.node_content_widget
8 | nodeeditor.node_edge
9 | nodeeditor.node_edge_dragging
10 | nodeeditor.node_edge_intersect
11 | nodeeditor.node_edge_rerouting
12 | nodeeditor.node_edge_snapping
13 | nodeeditor.node_edge_validators
14 | nodeeditor.node_editor_widget
15 | nodeeditor.node_editor_window
16 | nodeeditor.node_graphics_cutline
17 | nodeeditor.node_graphics_edge
18 | nodeeditor.node_graphics_edge_path
19 | nodeeditor.node_graphics_node
20 | nodeeditor.node_graphics_scene
21 | nodeeditor.node_graphics_socket
22 | nodeeditor.node_graphics_view
23 | nodeeditor.node_node
24 | nodeeditor.node_scene
25 | nodeeditor.node_scene_clipboard
26 | nodeeditor.node_scene_history
27 | nodeeditor.node_serializable
28 | nodeeditor.node_socket
29 | nodeeditor.utils
30 |
31 |
--------------------------------------------------------------------------------
/docs/source/rst/nodeeditor.utils.rst:
--------------------------------------------------------------------------------
1 | .. py:currentmodule:: nodeeditor.utils
2 |
3 | :py:mod:`utils` Module
4 | =======================
5 |
6 |
7 | .. automodule:: nodeeditor.utils
8 | :members:
9 | :undoc-members:
10 | :show-inheritance:
11 |
12 | .. autofunction:: nodeeditor.utils.pp
13 |
14 | Shortcut function for ``PrettyPrinter.pprint()`` method. Already instantiated and
15 | with indentation set to 4 spaces
16 |
--------------------------------------------------------------------------------
/docs/source/serialization.rst:
--------------------------------------------------------------------------------
1 | Serialization
2 | =============
3 |
4 | All of serializable classes derive from :class:`~nodeeditor.node_serializable.Serializable` class.
5 | `Serializable` does create commonly used parameters for our classes. In our case it is just ``id``
6 | attribute.
7 |
8 | `Serializable` defines two methods which should be overriden in child classes:
9 |
10 | - :py:func:`~nodeeditor.node_serializable.Serializable.serialize`
11 | - :py:func:`~nodeeditor.node_serializable.Serializable.deserialize`
12 |
13 | According to :ref:`coding-standards` we keep these two functions on the bottom of the class source code.
14 |
15 | To contain all of the data we use ``OrderedDict`` instead of regular `dict`. Mainly because we want
16 | to retain the order of parameters serialized in files.
17 |
18 | Classes which derive from :class:`~nodeeditr.serializable.Serializable`:
19 |
20 | - :class:`~nodeeditor.node_scene.Scene`
21 | - :class:`~nodeeditor.node_node.Node`
22 | - :class:`~nodeeditor.node_content_widget.QDMNodeContentWidget`
23 | - :class:`~nodeeditor.node_edge.Edge`
24 | - :class:`~nodeeditor.node_socket.Socket`
--------------------------------------------------------------------------------
/flow_charts/GUI Digram.drawio:
--------------------------------------------------------------------------------
1 | 7Vtdc5s4FP01fkxG4ptHx0nT7aTZzMZtp08dYmTMFiMWROzsr18JJPMhGbs22N5pZjIDvgi43HvO4V6JjPTJcn2fesniM/ZRNNKAvx7ptyNN04AO6YZZ3koLBLZdWoI09LmtMjyH/yIxkFvz0EdZYyDBOCJh0jTOcByjGWnYvDTFq+awOY6ad028AEmG55kXydZvoU8W3GoBUB34iMJgwW/tigNLTwzmhmzh+XhVM+l3I32SYkzKveV6giIWPRGX8rwPW45uHEtRTPY5AY2vHh7/CqbG5Mvd/fQHyYOHT1fshMI58iaeGPk0APxnjGO6uUlxHvuIXQfQXzglCxzg2IseME6oEVLj34iQN54+LyeYmhZkGfGjsq/c/Qzn6Qx1OGjynHtpgPip2tgMHr///OqgafL64+ZTnk2exDjmfO0GPBL3CC8RSd/ogBRFHglfm9n1OEiCzbjNqU84pC5rQCDa5tnkeDYss3mJ0lF+VpUNulNzozIVOfqFfPG7vXpRzh/hS4bSkWZFNDg3L2wvYHufvcBHUmopBBO2my+j8YzglA58RSkJKdwfvBcUPeEsJCGO6ZAXTAhe1gaMozBgBwhLeT23OCdRGKPJhoCgK+HscmjdmaK1YFIj0rplX/OnX1VEtPigRY2CEJrb09pISEf0lSDbUHlYtmQURGTMxKu6aGH7EDJ/i8ui2BcjZpGXZeGsNPIhFelajDiMhZbMQuU4qA3COgh1rQmGdooHpp0r0Y7CvYCyT81TtGbOTrCCck0wrBYhQc+JVwR7RV+Ze8rk/qyBbYXSTJk1EChoY/TAGjUogBS9olqgpgdMlYVuv3pp6L1E9EV/9viBVvxMEat6/ExV/MBAqmPvCMrB7+NepAGKqmyXNlhHSsNRMYTdyo2iF7y6qwzHa3hNn7mCV+IM2hpfKXhd5s+VKAhOlKlON2tq8YhW1MA148z64MJWAQgU8qqdUh6s/uVBVYAchkR1rQ72ReKxRfxRgd1Wa4M/4iQnfSBRgp0inluRqGtGC4maokBWY3GwAhlKYfk9CmSo7Qlp90hEbymQAbSaYHBOWyCL55crZFbdhVlelHllhQym+NKKZgj3KJp1BZPMoVRd06WAtsP47L2yDQV0P3XzUWokhVDXFXXzUH2HOoRy4TyEGJ210uvEzuAluVqMXMu9tjQo/sQFRD/ltvJdPo6kTNJlHasFsOEkTh1VR2LkLZp7ecR8fMRslpo2tGF2/spA6mEtQyFnJ+1h9ff+S9V/dQCtf1oK4okSwTyQiNKF2owemIi6JRFRTIAvoJgAF5Ys8eIG7qx/crbicjPHMbnKCviM6QDNSNZ0U5wJKPXIlVdOeLODM5rsYpadnytuQt/IbLa8VuE8z9IwIWEciPvTJyxdaLpVzdTXBsreJ3s/zr4ub1oYenTONOgAP2tetTjNvGiX6in+iSY4YqsNgopzSrCWid58xsKm35rVr2lB9ittm2xiKpPzqKDwIvR9FEvKwbLMNYK2S72oq9OeAJCkdbN00ViVcIbSVkOeX5USE9CwJFsfni+eshnY0g5+OSgtTVCFRfnGgWCoaWdZJVQsBU8pnqGs94nn7kzt7kw2Ir1fGK3BXtzy0odQAsatfaQVOkxatwjRJvwbdSkv27vAcG285cIoL2wuqYIUtYcq1z2oSgcittLKkrNvKLKvDyYtcltfW6oBfyZstfgCm0/TluXHPmW9K3h7yomw7VXsrmK4zypXMbfbAa3+Z8IMpzUTBuG1obu2C0zbdKGlH1b0QlNwQbSfrn7tGA60bFN3NcuGxkmLYEN+vU3yjH2ycenNqH32BVXD6STnezMq4WyACet2Ga0f2I1CqDcJTwl5Uiaacvn9GcU5tdx46dnpZ4rvLS9olciUl3h3xOk4zvW7/tO1Vrj7M8Vj19bVJDCM1lqgdujcDgDdFxqaTG6nNFc46FWfa/U8dM5dVqnjop0KYMd9i6BfXPpOvTy8d/ou8YsHud1jq4tnf4nZ7fZOUUKq2rvB+mIx3XJBOL8MmVKsPF6gTJny4vqdH56/V5Jwbp4b593f8/y+ODf+Hzg3JJx/C2P2X0iXhnRL8RmlCuntcrQ/pMstyDvSGYRO1tkch3T5Y82PKEouD+fWYIpOf1b/TFj2Y9X/ZOp3/wE=1Vhdc5pAFP01zrQPcYBFGh8bY2ymTZtUG/Wps5EboIMsXVfF/PoucAlsUPxIDNYH5R4uK3vuObsXGqQzjXqchu4Ns8FvGJodNchlwzB0YhryJ0ZWKXJutVLA4Z6NSTnQ954AQQ3RuWfDTEkUjPnCC1VwwoIAJkLBKOdsqaY9Ml/915A6UAL6E+qX0aFnCxdn0dJy/At4jpv9s67hmSnNkhGYudRmywJEug3S4YyJ9GgadcCPyct4+aHfhfZfawA/H91BMBt+Gl4HZ+lgV/tc8jwFDoE4eOjH0ZVD6I05vR7/vht81Tq9WXSWTU2sMr7AlvRhyLhwmcMC6ndz9IKzeWBDPKomozznG2OhBHUJ/gEhVqgFOhdMQq6Y+ngWIk+M4subLYzGhTOXEY6cBCsMdqQAqZqxOZ9ARR6KWlDuQNV4JM2LSSnICQnuAZuC4CuZwMGnwluomqMoXec5Dy/9zDldFRJC5gViVhj5NgZkArrQPEfPoQeJ/qLU++XLg/QOsqgwlRxK5LOHSpHSBfXnSMN9877Zl9CH3q/rjyWh5TKKK790PQH9kCZVW8qlSJXMfuVfABcQVRYMzxKiKUzpFsbLfK0wdGTTLawTbW1zjQs8V9K41pFGLY4M5K2PisE492cc5qZMoiO5kuzoSvOtXblrxaruuiD8DgcqQGLfYSm/k521Qv9affo3DVX/xCzrX89yivq3jqV/Uqv+tf30v34n095zJzN39IxRl2fWVtmqo8rHZ37tXN98tTqoh5BeV5yeOX9TD7El/zg9hFlaSicsfno4ybXT0E5t7dRLRP1/rmrtuJ6dSmeuamCbq7bkH8dVrZKrgtRVli8Zvnjg8siJjxaUe/TBP1XDmYb6WGOSug3XqrdZaVptU21Y2m1rW8sSR7fAPckA8Ff3JFU73vv38YftjOdkraw27ozV+a/2cBWlys4YxNZJXp3ZkLxWk19N+TlN977cLo/pXhnm7+XSEuRvN0n3Hw==
--------------------------------------------------------------------------------
/flow_charts/Vision Analysis and Design.drawio:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | PyQt5
2 | PyQt5-Qt5
3 | PyQt5-sip
4 | PyQt5-stubs
5 | QtPy
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """The setup script."""
5 |
6 | from setuptools import setup, find_packages
7 |
8 | with open('NodeEditorDocs.rst') as readme_file:
9 | readme = readme_file.read()
10 |
11 | with open('HISTORY.rst') as history_file:
12 | history = history_file.read()
13 |
14 | with open('requirements.txt') as requirements_file:
15 | requirements = requirements_file.read()
16 |
17 | setup_requirements = [ ]
18 |
19 | test_requirements = [ ]
20 |
21 | import nodeeditor
22 |
23 | setup(
24 | author="Pavel Křupala",
25 | author_email='pavel.krupala@gmail.com',
26 | classifiers=[
27 | 'Development Status :: 3 - Alpha',
28 | 'Intended Audience :: Developers',
29 | 'License :: OSI Approved :: MIT License',
30 | 'Natural Language :: English',
31 | 'Programming Language :: Python :: 3',
32 | 'Programming Language :: Python :: 3.5',
33 | 'Programming Language :: Python :: 3.6',
34 | 'Programming Language :: Python :: 3.7',
35 | 'Programming Language :: Python :: 3.8',
36 | ],
37 | description="Python Node Editor using PyQt5",
38 | install_requires=requirements,
39 | license="MIT license",
40 | long_description=readme + '\n\n' + history,
41 | include_package_data=True,
42 | keywords='nodeeditor',
43 | name='nodeeditor',
44 | #packages=find_packages(include=['_template']),
45 | packages=find_packages(include=['nodeeditor*'], exclude=['vvs_app*', 'tests*']),
46 | package_data={'': ['qss/*']},
47 | setup_requires=setup_requirements,
48 | test_suite='tests',
49 | tests_require=test_requirements,
50 | url='https://gitlab.com/pavel.krupala/pyqt-node-editor.git',
51 | version=nodeeditor.__version__,
52 | zip_safe=False,
53 | )
54 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py36
3 |
4 | [testenv]
5 | setenv =
6 | PYTHONPATH = {toxinidir}
7 |
8 | commands = py.test {posargs:tests}
9 | deps =
10 | pytest
11 | PyQt5
12 |
13 |
--------------------------------------------------------------------------------
/vvs_app/QRoundPB.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | #######################################################
5 | #
6 | # Copyright 2017 Pete Alexandrou
7 | #
8 | # Ported to Python from the original works in C++ by:
9 | #
10 | # Sintegrial Technologies (c) 2015
11 | # https://sourceforge.net/projects/qroundprogressbar
12 | #
13 | # Licensed under the Apache License, Version 2.0 (the "License");
14 | # you may not use this file except in compliance with the License.
15 | # You may obtain a copy of the License at
16 | #
17 | # http://www.apache.org/licenses/LICENSE-2.0
18 | #
19 | # Unless required by applicable law or agreed to in writing, software
20 | # distributed under the License is distributed on an "AS IS" BASIS,
21 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 | # See the License for the specific language governing permissions and
23 | # limitations under the License.
24 | #
25 | #######################################################
26 |
27 | import operator
28 | from enum import Enum
29 |
30 | from PyQt5.QtCore import pyqtSlot, QPointF, Qt, QRectF
31 | from PyQt5.QtGui import (QPalette, QConicalGradient, QGradient, QRadialGradient,
32 | QFontMetricsF, QFont, QPainter, QPen, QPainterPath, QImage,
33 | QPaintEvent)
34 | from PyQt5.QtWidgets import QWidget
35 |
36 |
37 | class QRoundProgressBar(QWidget):
38 |
39 | # CONSTANTS
40 |
41 | PositionLeft = 180
42 | PositionTop = 90
43 | PositionRight = 0
44 | PositionBottom = -90
45 |
46 | # CONSTRUCTOR ---------------------------------------------------
47 |
48 | def __init__(self, parent=None):
49 | super(QRoundProgressBar, self).__init__(parent)
50 | self.m_min = 0
51 | self.m_max = 100
52 | self.m_value = 25
53 | self.m_nullPosition = QRoundProgressBar.PositionTop
54 | self.m_barStyle = self.BarStyle.DONUT
55 | self.m_outlinePenWidth = 1
56 | self.m_dataPenWidth = 1
57 | self.m_rebuildBrush = False
58 | self.m_format = '%p%'
59 | self.m_decimals = 1
60 | self.m_updateFlags = self.UpdateFlags.PERCENT
61 | self.m_gradientData = None
62 | self.text_visability = True
63 |
64 | # ENUMS ---------------------------------------------------------
65 |
66 | class BarStyle(Enum):
67 | DONUT = 0,
68 | PIE = 1,
69 | LINE = 2,
70 | EXPAND = 3
71 |
72 | class UpdateFlags(Enum):
73 | VALUE = 0,
74 | PERCENT = 1,
75 | MAX = 2
76 |
77 | # GETTERS -------------------------------------------------------
78 |
79 | def minimum(self):
80 | return self.m_min
81 |
82 | def maximum(self):
83 | return self.m_max
84 |
85 | def isTextvisabile(self):
86 | return self.text_visability
87 |
88 | # SETTERS -------------------------------------------------------
89 |
90 | def setNullPosition(self, position: float):
91 | if position != self.m_nullPosition:
92 | self.m_nullPosition = position
93 | self.m_rebuildBrush = True
94 | self.update()
95 |
96 | def setBarStyle(self, style: BarStyle):
97 | if style != self.m_barStyle:
98 | self.m_barStyle = style
99 | self.m_rebuildBrush = True
100 | self.update()
101 |
102 | def setOutlinePenWidth(self, width: float):
103 | if width != self.m_outlinePenWidth:
104 | self.m_outlinePenWidth = width
105 | self.update()
106 |
107 | def setDataPenWidth(self, width: float):
108 | if width != self.m_dataPenWidth:
109 | self.m_dataPenWidth = width
110 | self.update()
111 |
112 | def setDataColors(self, stopPoints: list):
113 | if stopPoints != self.m_gradientData:
114 | self.m_gradientData = stopPoints
115 | self.m_rebuildBrush = True
116 | self.update()
117 |
118 | def setFormat(self, val: str):
119 | if val != self.m_format:
120 | self.m_format = val
121 | self.valueFormatChanged()
122 |
123 | def resetFormat(self):
124 | self.m_format = None
125 | self.valueFormatChanged()
126 |
127 | def setDecimals(self, count: int):
128 | if count >= 0 and count != self.m_decimals:
129 | self.m_decimals = count
130 | self.valueFormatChanged()
131 |
132 | def setTextVisabile(self, show: bool):
133 | if show != self.text_visability:
134 | self.text_visability = show
135 | self.update()
136 |
137 | # SLOTS ---------------------------------------------------------
138 |
139 | @pyqtSlot(float, float)
140 | def setRange(self, minval: float, maxval: float):
141 | self.m_min = minval
142 | self.m_max = maxval
143 | if self.m_max < self.m_min:
144 | self.m_min = maxval
145 | self.m_max = minval
146 | if self.m_value < self.m_min:
147 | self.m_value = self.m_min
148 | elif self.m_value > self.m_max:
149 | self.m_value = self.m_max
150 | self.m_rebuildBrush = True
151 | self.update()
152 |
153 | @pyqtSlot(float)
154 | def setMinimum(self, val: float):
155 | self.setRange(val, self.m_max)
156 |
157 | @pyqtSlot(float)
158 | def setMaximum(self, val: float):
159 | self.setRange(self.m_min, val)
160 |
161 | @pyqtSlot(int)
162 | def setValue(self, val: int):
163 | if self.m_value != val:
164 | if val < self.m_min:
165 | self.m_value = self.m_min
166 | elif val > self.m_max:
167 | self.m_value = self.m_max
168 | else:
169 | self.m_value = val
170 | self.update()
171 |
172 | # PAINTING ------------------------------------------------------
173 |
174 | def paintEvent(self, event: QPaintEvent):
175 | outerRadius = min(self.width(), self.height())
176 | baseRect = QRectF(1, 1, outerRadius - 2, outerRadius - 2)
177 | buffer = QImage(outerRadius, outerRadius, QImage.Format_ARGB32_Premultiplied)
178 | p = QPainter(buffer)
179 | p.setRenderHint(QPainter.Antialiasing)
180 | self.rebuildDataBrushIfNeeded()
181 | self.drawBackground(p, buffer.rect())
182 | self.drawBase(p, baseRect)
183 | if self.m_value > 0:
184 | delta = (self.m_max - self.m_min) / (self.m_value - self.m_min)
185 | else:
186 | delta = 0
187 | self.drawValue(p, baseRect, self.m_value, delta)
188 | innerRect, innerRadius = self.calculateInnerRect(outerRadius)
189 | self.drawInnerBackground(p, innerRect)
190 | self.conditionalDrawText(self.text_visability, p, innerRect, innerRadius, self.m_value)
191 | p.end()
192 | painter = QPainter(self)
193 | painter.fillRect(baseRect, self.palette().window())
194 | painter.drawImage(0, 0, buffer)
195 |
196 | def drawBackground(self, p: QPainter, baseRect: QRectF):
197 | p.fillRect(baseRect, self.palette().window())
198 |
199 | def drawBase(self, p: QPainter, baseRect: QRectF):
200 | if self.m_barStyle == self.BarStyle.DONUT:
201 | p.setPen(QPen(self.palette().shadow().color(), self.m_outlinePenWidth))
202 | p.setBrush(self.palette().base())
203 | p.drawEllipse(baseRect)
204 | elif self.m_barStyle == self.BarStyle.LINE:
205 | p.setPen(QPen(self.palette().base().color(), self.m_outlinePenWidth))
206 | p.setBrush(Qt.NoBrush)
207 | p.drawEllipse(baseRect.adjusted(self.m_outlinePenWidth / 2, self.m_outlinePenWidth / 2,
208 | -self.m_outlinePenWidth / 2, -self.m_outlinePenWidth / 2))
209 | elif self.m_barStyle in (self.BarStyle.PIE, self.BarStyle.EXPAND):
210 | p.setPen(QPen(self.palette().base().color(), self.m_outlinePenWidth))
211 | p.setBrush(self.palette().base())
212 | p.drawEllipse(baseRect)
213 |
214 | def drawValue(self, p: QPainter, baseRect: QRectF, value: float, delta: float):
215 | if value == self.m_min:
216 | return
217 | if self.m_barStyle == self.BarStyle.EXPAND:
218 | p.setBrush(self.palette().highlight())
219 | p.setPen(QPen(self.palette().shadow().color(), self.m_dataPenWidth))
220 | radius = (baseRect.height() / 2) / delta
221 | p.drawEllipse(baseRect.center(), radius, radius)
222 | return
223 | if self.m_barStyle == self.BarStyle.LINE:
224 | p.setPen(QPen(self.palette().highlight().color(), self.m_dataPenWidth))
225 | p.setBrush(Qt.NoBrush)
226 | if value == self.m_max:
227 | p.drawEllipse(baseRect.adjusted(self.m_outlinePenWidth / 2, self.m_outlinePenWidth / 2,
228 | -self.m_outlinePenWidth / 2, -self.m_outlinePenWidth / 2))
229 | else:
230 | arcLength = 360 / delta
231 | p.drawArc(baseRect.adjusted(self.m_outlinePenWidth / 2, self.m_outlinePenWidth / 2,
232 | -self.m_outlinePenWidth / 2, -self.m_outlinePenWidth / 2),
233 | int(self.m_nullPosition * 16),
234 | int(-arcLength * 16))
235 | return
236 | dataPath = QPainterPath()
237 | dataPath.setFillRule(Qt.WindingFill)
238 | if value == self.m_max:
239 | dataPath.addEllipse(baseRect)
240 | else:
241 | arcLength = 360 / delta
242 | dataPath.moveTo(baseRect.center())
243 | dataPath.arcTo(baseRect, self.m_nullPosition, -arcLength)
244 | dataPath.lineTo(baseRect.center())
245 | p.setBrush(self.palette().highlight())
246 | p.setPen(QPen(self.palette().shadow().color(), self.m_dataPenWidth))
247 | p.drawPath(dataPath)
248 |
249 | def calculateInnerRect(self, outerRadius: float):
250 | if self.m_barStyle in (self.BarStyle.LINE, self.BarStyle.EXPAND):
251 | innerRadius = outerRadius - self.m_outlinePenWidth
252 | else:
253 | innerRadius = outerRadius * 0.75
254 | delta = (outerRadius - innerRadius) / 2
255 | innerRect = QRectF(delta, delta, innerRadius, innerRadius)
256 | return innerRect, innerRadius
257 |
258 | def drawInnerBackground(self, p: QPainter, innerRect: QRectF):
259 | if self.m_barStyle == self.BarStyle.DONUT:
260 | p.setBrush(self.palette().alternateBase())
261 | p.drawEllipse(innerRect)
262 |
263 | def drawText(self, p: QPainter, innerRect: QRectF, innerRadius: float, value: float):
264 | if not self.m_format:
265 | return
266 | f = QFont(self.font())
267 | f.setPixelSize(10)
268 | fm = QFontMetricsF(f)
269 | maxWidth = fm.width(self.valueToText(self.m_max))
270 | delta = innerRadius / maxWidth
271 | fontSize = f.pixelSize() * delta * 0.75
272 | f.setPixelSize(int(fontSize))
273 | p.setFont(f)
274 | textRect = QRectF(innerRect)
275 | p.setPen(self.palette().text().color())
276 | p.drawText(textRect, Qt.AlignCenter, self.valueToText(value))
277 |
278 | def conditionalDrawText(self, visability, p: QPainter=None, innerRect: QRectF=None, innerRadius: float=None, value: float=None):
279 | if visability:
280 | self.drawText(p, innerRect, innerRadius, value)
281 |
282 | def valueToText(self, value: float):
283 | textToDraw = self.m_format
284 | if self.m_updateFlags == self.UpdateFlags.VALUE:
285 | textToDraw = textToDraw.replace('%v', str(round(value, self.m_decimals)))
286 | if self.m_updateFlags == self.UpdateFlags.PERCENT:
287 | procent = (value - self.m_min) / (self.m_max - self.m_min) * 100
288 | textToDraw = textToDraw.replace('%p', str(round(procent, self.m_decimals)))
289 | if self.m_updateFlags == self.UpdateFlags.MAX:
290 | textToDraw = textToDraw.replace('%m', str(round(self.m_max - self.m_min + 1, self.m_decimals)))
291 | return textToDraw
292 |
293 | def valueFormatChanged(self):
294 | if operator.contains(self.m_format, '%v'):
295 | self.m_updateFlags = self.UpdateFlags.VALUE
296 | if operator.contains(self.m_format, '%p'):
297 | self.m_updateFlags = self.UpdateFlags.PERCENT
298 | if operator.contains(self.m_format, '%m'):
299 | self.m_updateFlags = self.UpdateFlags.MAX
300 | self.update()
301 |
302 | def rebuildDataBrushIfNeeded(self):
303 | if not self.m_rebuildBrush or not self.m_gradientData or self.m_barStyle == self.BarStyle.LINE:
304 | return
305 | self.m_rebuildBrush = False
306 | p = self.palette()
307 | if self.m_barStyle == self.BarStyle.EXPAND:
308 | dataBrush = QRadialGradient(0.5, 0.5, 0.5, 0.5, 0.5)
309 | dataBrush.setCoordinateMode(QGradient.StretchToDeviceMode)
310 | for i in range(0, len(self.m_gradientData)):
311 | dataBrush.setColorAt(self.m_gradientData[i][0], self.m_gradientData[i][1])
312 | p.setBrush(QPalette.Highlight, dataBrush)
313 | else:
314 | dataBrush = QConicalGradient(QPointF(0.5, 0.5), self.m_nullPosition)
315 | dataBrush.setCoordinateMode(QGradient.StretchToDeviceMode)
316 | for i in range(0, len(self.m_gradientData)):
317 | dataBrush.setColorAt(1 - self.m_gradientData[i][0], self.m_gradientData[i][1])
318 | p.setBrush(QPalette.Highlight, dataBrush)
319 | self.setPalette(p)
320 |
--------------------------------------------------------------------------------
/vvs_app/Testing.py:
--------------------------------------------------------------------------------
1 | from array import array
2 |
3 | user_float = array("f", [1.0, 2.0, 3.2])
4 |
5 | for item in user_float:
6 | print(item)
7 |
--------------------------------------------------------------------------------
/vvs_app/VVS-Help.chm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/VVS-Help.chm
--------------------------------------------------------------------------------
/vvs_app/editor_files_wdg.py:
--------------------------------------------------------------------------------
1 | from master_window import *
2 |
3 |
4 | class FilesWDG(QWidget):
5 | def __init__(self, masterRef, parent=None):
6 | super().__init__(parent)
7 | self.default_system_dir = f"C:/Users/{os.getlogin()}/Documents/VVS"
8 |
9 | self.masterRef = masterRef
10 |
11 | layout = QVBoxLayout()
12 | layout.setContentsMargins(0, 0, 0, 0)
13 | self.setLayout(layout)
14 |
15 | self.Model = QFileSystemModel()
16 | self.Model.setRootPath("")
17 |
18 | self.tree_wdg = QTreeView()
19 | self.tree_wdg.setSelectionMode(QAbstractItemView.ExtendedSelection)
20 | self.tree_wdg.setModel(self.Model)
21 | self.tree_wdg.setSortingEnabled(True)
22 | self.tree_wdg.setColumnWidth(0, 190)
23 | self.tree_wdg.sortByColumn(0, Qt.AscendingOrder)
24 | self.tree_wdg.hideColumn(1)
25 | self.tree_wdg.hideColumn(2)
26 | layout.addWidget(self.tree_wdg)
27 |
28 | self.tree_wdg.clicked.connect(self.OpenSelectedFiles)
29 | self.tree_wdg.contextMenuEvent = self.tree_wdg_contextMenuEvent
30 | self.CreateDefaultDir()
31 |
32 | def tree_wdg_contextMenuEvent(self, event):
33 | context_menu = QMenu(self)
34 | delete = context_menu.addAction("Delete")
35 | context_menu.addAction("Cancel")
36 | action = context_menu.exec_(self.mapToGlobal(event.pos()))
37 |
38 | if action == delete:
39 | file_path = QFileSystemModel().filePath(self.tree_wdg.selectedIndexes()[0])
40 | delete_Q = QMessageBox.warning(self, "Warning !", f"You Are About To Delete\n\n{file_path}\n\nThis is Irreversible Are You Sure?", QMessageBox.Yes | QMessageBox.Cancel)
41 | if delete_Q == QMessageBox.Yes:
42 | try:
43 | os.remove(file_path)
44 | except Exception:
45 | warning = QMessageBox(QMessageBox.Warning, "Permissions Error",
46 | "You Don't have Permissions To Delete a Directory", QMessageBox.Ok)
47 | warning.exec_()
48 |
49 |
50 | def OpenSelectedFiles(self):
51 | all_files = []
52 |
53 | selected_files = self.tree_wdg.selectedIndexes()
54 |
55 | for file_name in selected_files:
56 | file_path = QFileSystemModel().filePath(file_name)
57 |
58 | if file_path.endswith(".json"):
59 | if not all_files.__contains__(file_path):
60 | all_files.append(file_path)
61 | # print(all_files)
62 |
63 | self.masterRef.on_file_open(all_files)
64 |
65 | def CreateDefaultDir(self):
66 | self.Project_Directory = self.default_system_dir
67 | if os.path.exists(self.default_system_dir) is False:
68 | os.makedirs(self.Project_Directory)
69 |
70 | self.tree_wdg.setRootIndex(self.Model.index(self.Project_Directory))
71 | self.MakeDir(self.Project_Directory)
72 |
73 | def get_scripts_dir(self, syntax_selector):
74 | if not os.listdir(self.Project_Directory).__contains__(syntax_selector.currentText()):
75 | dir = self.Project_Directory + f"/{syntax_selector.currentText()}"
76 | os.makedirs(dir)
77 |
78 | def set_project_folder(self):
79 | Dir = QFileDialog.getExistingDirectory(self, "Select Project Folder", self.Project_Directory)
80 | if Dir != "":
81 | self.Project_Directory = Dir
82 | self.tree_wdg.setRootIndex(self.Model.index(self.Project_Directory))
83 | self.MakeDir(self.Project_Directory)
84 | return True
85 | else:
86 | return False
87 |
88 | def MakeDir(self, Dir):
89 | if os.listdir(self.Project_Directory).__contains__("VVS Auto Backup") is False:
90 | os.makedirs(Dir + "/VVS Auto Backup")
91 |
92 | def new_graph_name(self, subwnd, all_names):
93 | x = 1
94 | names = []
95 | for item in all_names:
96 | item = item.replace("*", "")
97 | names.append(item)
98 |
99 | newName = f"New Graph {x}"
100 | while names.__contains__(newName):
101 | x += 1
102 | newName = f"New Graph {x}"
103 | else:
104 | subwnd.setWindowTitle(newName)
105 | subwnd.widget().setWindowTitle(newName)
106 |
107 | def size_limit_warning(self):
108 | AutoSaveDir = self.Project_Directory + "/VVS Auto Backup"
109 | dirContentList = os.listdir(AutoSaveDir)
110 |
111 | FolderContentSize = 0
112 | for file in dirContentList:
113 | file = os.path.join(AutoSaveDir, file)
114 | FolderContentSize += os.stat(file).st_size
115 |
116 | FolderContentSizeInGBs = FolderContentSize/(1000 * 1000)
117 |
118 | if FolderContentSizeInGBs >= self.masterRef.global_switches.switches_Dict["System"]["AutoSave Folder MaxSize"]:
119 | self.msg = QMessageBox()
120 | self.msg.setText(f"AutoSave Folder Has Exceeded the Set Limit of {FolderContentSizeInGBs} MB")
121 | self.msg.show()
122 | # self.masterRef.statusBar().showMessage(f"""AutoSave Folder Has Exceeded the Set Limit of {FolderContentSizeInGBs} Gigabytes""")
123 |
--------------------------------------------------------------------------------
/vvs_app/editor_node_list.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtGui import QFont
2 | from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem
3 | from qtpy.QtGui import QPixmap, QIcon, QDrag
4 | from qtpy.QtCore import QSize, Qt, QByteArray, QDataStream, QMimeData, QIODevice, QPoint
5 | from qtpy.QtWidgets import QAbstractItemView
6 |
7 | from nodes.nodes_configuration import FUNCTIONS, get_class_by_type, LISTBOX_MIMETYPE
8 | from utils import dumpException
9 |
10 |
11 | class NodeList(QTreeWidget):
12 | def __init__(self, parent=None):
13 | super().__init__(parent)
14 | self.initUI()
15 |
16 | def initUI(self):
17 | # init
18 | self.setIconSize(QSize(20, 20))
19 | self.setSelectionMode(QAbstractItemView.SingleSelection)
20 | self.setDragEnabled(True)
21 |
22 | self.header().hide()
23 | self.setRootIsDecorated(False)
24 | self.setColumnCount(2)
25 | self.setColumnWidth(0, 70)
26 |
27 | self.addMyFunctions()
28 |
29 | self.expandAll()
30 |
31 | self.itemClicked.connect(lambda: self.currentItem().setExpanded(True) if not self.currentItem().isExpanded() else self.currentItem().setExpanded(False))
32 | self.itemCollapsed.connect(lambda: self.currentItem().setText(0, self.currentItem().text(0).replace("▼", "►")))
33 | self.itemExpanded.connect(lambda: self.currentItem().setText(0, self.currentItem().text(0).replace("►", "▼")))
34 | self.setExpandsOnDoubleClick(False)
35 |
36 | def addMyFunctions(self):
37 | self.clear()
38 |
39 | self.init_primary_content()
40 |
41 | Funs = list(FUNCTIONS.keys())
42 | for Fun in Funs:
43 | node = get_class_by_type(Fun)
44 | self.addMyItem(node.name, node.icon, node)
45 |
46 | def init_primary_content(self):
47 | self.categories = {"▼ Process": None, "▼ Logic": None, "▼ Math": None, "▼ Input": None, "▼ Output": None, "▼ List Operator": None}
48 | for category in list(self.categories.keys()):
49 | item = QTreeWidgetItem(self, [category])
50 | item.setFont(0, QFont("Arial", 9))
51 | item.setSizeHint(1, QSize(18, 18))
52 | self.categories[category.replace("▼ ", "")] = item
53 | del self.categories[category]
54 |
55 | def addMyItem(self, name, icon=None, node=None):
56 | item = QTreeWidgetItem(self.categories[node.sub_category], [name])
57 | pixmap = QPixmap(icon if icon is not None else ".")
58 | item.setIcon(0, QIcon(pixmap))
59 | item.setSizeHint(0, QSize(22, 22))
60 | item.setFirstColumnSpanned(True)
61 |
62 | item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled)
63 |
64 | # setup data
65 | item.setData(0, Qt.UserRole + 1, pixmap)
66 | item.setData(0, Qt.UserRole + 2, node.node_type)
67 |
68 | def startDrag(self, *args, **kwargs):
69 | try:
70 | if self.currentItem().data(0, Qt.UserRole + 2) is not None:
71 | item = self.currentItem()
72 | node_type = item.data(0, Qt.UserRole + 2)
73 |
74 | pixmap = QPixmap(item.data(0, Qt.UserRole + 1))
75 |
76 | itemData = QByteArray()
77 | dataStream = QDataStream(itemData, QIODevice.WriteOnly)
78 | dataStream << pixmap
79 | dataStream.writeInt(node_type)
80 | dataStream.writeQString(item.text(0))
81 | dataStream.writeQStringList(["N"])
82 |
83 | mimeData = QMimeData()
84 | mimeData.setData(LISTBOX_MIMETYPE, itemData)
85 |
86 | drag = QDrag(self)
87 | drag.setMimeData(mimeData)
88 | drag.setHotSpot(QPoint(pixmap.width() // 2, pixmap.height() // 2))
89 | drag.setPixmap(pixmap)
90 |
91 | drag.exec_(Qt.MoveAction)
92 |
93 | if item.parent():
94 | iteme = item.parent()
95 | iteme.setData(0, Qt.UserRole + 2, FUNCTIONS[node_type].node_type)
96 | iteme.setData(0, Qt.UserRole + 1, FUNCTIONS[node_type].icon)
97 | iteme.setText(1, FUNCTIONS[node_type].name)
98 | iteme.setIcon(1, QIcon(QPixmap(FUNCTIONS[node_type].icon)))
99 | except Exception as e:
100 | dumpException(e)
101 |
--------------------------------------------------------------------------------
/vvs_app/editor_properties_list.py:
--------------------------------------------------------------------------------
1 | from time import sleep
2 |
3 | from qtpy.QtCore import *
4 | from qtpy.QtWidgets import *
5 |
6 |
7 | class PropertiesList(QScrollArea):
8 | def __init__(self, parent=None, master_ref=None):
9 | super().__init__(parent)
10 | self.setWidgetResizable(True)
11 | self.master_ref = master_ref
12 | self.myForm = None
13 |
14 | def clear_properties(self):
15 | widget = QFrame()
16 | self.setWidget(widget)
17 | self.myForm = None
18 |
19 | def create_properties_widget(self, name, property_wgd):
20 | if self.myForm:
21 | self.myForm.addRow(QLabel(f"{name}"), property_wgd)
22 | else:
23 | self.myForm = QFormLayout()
24 | widget = QFrame()
25 | self.setWidget(widget)
26 | widget.setLayout(self.myForm)
27 | self.myForm.setSpacing(8)
28 | self.myForm.setAlignment(Qt.AlignTop)
29 | self.myForm.addRow(QLabel(f"{name}"), property_wgd)
30 |
31 | def create_order_wdg(self):
32 | grNodesRef = self.master_ref.currentNodeEditor().scene.getSelectedItems()
33 | if grNodesRef and len(grNodesRef) == 1:
34 | self.order = QSpinBox()
35 | node = grNodesRef[0].node
36 | self.order.setValue(node.getNodeOrder())
37 | self.order.valueChanged.connect(lambda: self.orderChanged(node))
38 | self.myForm.addRow(QLabel(f"Node Order"), self.order)
39 |
40 | def orderChanged(self, node):
41 | i = node.scene.nodes
42 | if self.order.value() > len(i)-1:
43 | self.order.setValue(len(i)-1)
44 | i[node.getNodeOrder()], i[self.order.value()] = i[self.order.value()], i[node.getNodeOrder()]
45 |
46 | node.scene.node_editor.UpdateTextCode()
47 |
--------------------------------------------------------------------------------
/vvs_app/global_switches.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 | from utils import loadStylesheets
5 |
6 |
7 | class GlobalSwitches:
8 | def __init__(self, master):
9 | self.master_ref = master
10 | self.Settings_Directory = f"{self.master_ref.files_widget.default_system_dir}/Preferences"
11 | self.Settings_File = self.Settings_Directory + f"/Settings.json"
12 |
13 | self.themes = {"Dark": "qss/nodeeditor-night.qss", "Light": "qss/nodeeditor-light.qss"}
14 |
15 | self.themes_colors = {"Nodes": ["Text", "Background", "Outline", "", ""],
16 | "Dark": ["#ffffff", "#282828", "#282828", "#828282", "#ffffff"],
17 | "Light": ["#1f1f1f", "#828282", "#565656", "#282828", "#1f1f1f"]}
18 |
19 | self.Default_switches_Dict = {"Appearance":
20 | {
21 | "Theme": ["Dark", "Light"],
22 | "Font Size": 16,
23 | "Grid Size": 30
24 | },
25 | "System":
26 | {
27 | "AutoSave Steps": 30,
28 | "AutoSave Folder MaxSize": 500.0,
29 | "Always Save Before Closing": True,
30 | "Save New Project Folder On Close": False
31 | },
32 | "Key Mapping":
33 | {
34 | "New Graph": "Ctrl+N",
35 | "Open": "Ctrl+O",
36 | "Set Project Location": "Ctrl+Shift+O",
37 | "Save": "Ctrl+S",
38 | "Save As": "Ctrl+Shift+S",
39 | "Exit": "Ctrl+Q",
40 |
41 | "Undo": "Ctrl+Z",
42 | "Redo": "Ctrl+Shift+Z",
43 | "Select All": "Ctrl+A",
44 | "Cut": "Ctrl+X",
45 | "Copy": "Ctrl+C",
46 | "Paste": "Ctrl+V",
47 | "Delete": "Del",
48 |
49 | "Close": "Q",
50 | "Close All": "Shift+Q",
51 | "Tile": "T",
52 | "Next": "Shift+Tab",
53 | "Previous": "Ctrl+Shift+Tab",
54 |
55 | "Settings Window": "S",
56 | "Node Editor Window": "N",
57 | "Node Designer Window": "D",
58 | "Library Window": "L"
59 | }
60 | }
61 |
62 | if os.path.isfile(self.Settings_File):
63 | # Read data from file
64 | self.switches_Dict = json.load(open(self.Settings_File))
65 | else:
66 | if os.path.exists(self.Settings_Directory) is False:
67 | os.makedirs(self.Settings_Directory)
68 |
69 | self.switches_Dict = self.Default_switches_Dict
70 |
71 | self.save_settings_to_file()
72 | self.icons_dict = []
73 | self.fill_icons_dict()
74 |
75 | def save_settings_to_file(self, data=None, file_path=None):
76 | """Serializes/Saves Data Into filePath
77 |
78 | :param data :Is The Data Needed To Be Saved
79 | :param file_path :Full Path Of The File That Data Will Be Saved in
80 | """
81 | if data == None and file_path == None:
82 | data = self.switches_Dict
83 | file_path = self.Settings_File
84 |
85 | json.dump(data, open(file_path, 'w'))
86 |
87 | def update_font_size(self, size:str = ""):
88 | if size == "":
89 | size = self.switches_Dict["Appearance"]["Font Size"]
90 |
91 | s, z = "{font:", "}"
92 | self.master_ref.setStyleSheet(f"QWidget {s}{size}px{z}")
93 | if self.master_ref.settingsWidget:
94 | self.master_ref.settingsWidget.setStyleSheet(f"QWidget {s}{size}px{z}")
95 |
96 | def change_theme(self, theme:str = ""):
97 | if theme == "":
98 | theme = self.switches_Dict["Appearance"]["Theme"][0]
99 |
100 | self.master_ref.qss_theme = self.themes[theme]
101 | self.master_ref.stylesheet_filename = os.path.join(os.path.dirname(__file__), self.master_ref.qss_theme)
102 |
103 | loadStylesheets(
104 | os.path.join(os.path.dirname(__file__), self.master_ref.qss_theme), self.master_ref.stylesheet_filename)
105 |
106 | def fill_icons_dict(self):
107 | for icon in os.listdir(f"""vvs_app/icons/{self.switches_Dict["Appearance"]["Theme"][0]}"""):
108 | self.icons_dict.append(icon)
109 | pass
110 |
111 | def get_icon(self, icon):
112 | return f"""vvs_app/icons/{self.switches_Dict["Appearance"]["Theme"][0]}/{icon}"""
113 |
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/Loop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/Loop.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/Orientation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/Orientation.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/Row Code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/Row Code.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/Settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/Settings.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/VVS_Logo_Thick.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/VVS_Logo_Thick.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/VVS_White1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/VVS_White1.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/VVS_White2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/VVS_White2.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/VVS_White_Splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/VVS_White_Splash.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/add.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/and.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/and.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/close.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/copy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/copy.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/divide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/divide.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/edit.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/equal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/equal.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/event.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/event.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/exit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/exit.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/if.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/if.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/less_than.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/less_than.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/library.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/library.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/more_than.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/more_than.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/mul.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/mul.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/node design.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/node design.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/out.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/out.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/print.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/print.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/return.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/return.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/run.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/run.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/search.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/sub.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/sub.png
--------------------------------------------------------------------------------
/vvs_app/icons/Dark/user input.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/Dark/user input.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/Edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/Edit.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/Loop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/Loop.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/Row Code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/Row Code.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/VVS_Logo_Thick.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/VVS_Logo_Thick.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/VVS_White1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/VVS_White1.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/VVS_White2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/VVS_White2.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/VVS_White_Splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/VVS_White_Splash.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/add.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/and.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/and.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/close.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/copy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/copy.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/divide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/divide.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/equal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/equal.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/event.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/event.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/exit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/exit.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/if.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/if.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/less_than.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/less_than.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/library.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/library.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/more_than.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/more_than.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/mul.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/mul.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/node design.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/node design.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/orientation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/orientation.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/out.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/out.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/print.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/print.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/return.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/return.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/run.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/run.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/search.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/settings.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/sub.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/sub.png
--------------------------------------------------------------------------------
/vvs_app/icons/light/user input.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/icons/light/user input.png
--------------------------------------------------------------------------------
/vvs_app/main.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/main.exe
--------------------------------------------------------------------------------
/vvs_app/main.py:
--------------------------------------------------------------------------------
1 | import ctypes
2 | import os
3 | import sys
4 | from PyQt5.QtCore import *
5 | from PyQt5.QtGui import *
6 | from PyQt5.QtWidgets import *
7 | from qtpy.QtWidgets import QApplication
8 | from master_window import MasterWindow, Splash
9 |
10 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), "", ".."))
11 |
12 | if __name__ == '__main__':
13 | app = QApplication(sys.argv)
14 |
15 | app.setStyle('Fusion')
16 | app.setWindowIcon(QIcon("vvs_app/icons/Dark/VVS_Logo_Thick.png"))
17 |
18 | # Show app Icon In Task Manager
19 | myappid = 'mycompany.myproduct.subproduct.version'
20 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
21 |
22 | splash = Splash()
23 |
24 | wnd = MasterWindow()
25 |
26 | splash.run(wnd)
27 |
28 | sys.exit(app.exec_())
29 |
--------------------------------------------------------------------------------
/vvs_app/master_designer_wnd.py:
--------------------------------------------------------------------------------
1 | from node_editor_widget import NodeEditorWidget
2 |
3 | DEBUG = False
4 | DEBUG_CONTEXT = False
5 |
6 |
7 | class MasterDesignerWnd(NodeEditorWidget):
8 | def __init__(self, masterRef=None):
9 | super().__init__(masterRef)
10 | pass
11 |
12 |
--------------------------------------------------------------------------------
/vvs_app/master_editor_wnd.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtGui import QIcon, QPixmap
2 | from qtpy.QtCore import QDataStream, QIODevice, Qt
3 | from qtpy.QtWidgets import QAction, QGraphicsProxyWidget, QMenu
4 |
5 | from node_editor_widget import NodeEditorWidget
6 | from node_node import Node
7 | from nodes.nodes_configuration import *
8 | from node_edge import EDGE_TYPE_DIRECT, EDGE_TYPE_BEZIER, EDGE_TYPE_SQUARE
9 | from graph_graphics import MODE_EDGE_DRAG
10 | from utils import dumpException
11 |
12 | DEBUG = False
13 | DEBUG_CONTEXT = False
14 |
15 |
16 | class NodeEditorTab(NodeEditorWidget):
17 | def __init__(self, masterRef):
18 | super().__init__(masterRef=masterRef)
19 | # self.setAttribute(Qt.WA_DeleteOnClose)
20 |
21 | self.setTitle()
22 |
23 | self.initNewNodeActions()
24 |
25 | self.scene.addHasBeenModifiedListener(self.setTitle)
26 | self.scene.history.addHistoryRestoredListener(self.onHistoryRestored)
27 | self.scene.addDragEnterListener(self.onDragEnter)
28 | self.scene.addDropListener(self.onDrop)
29 | self.scene.setNodeClassSelector(self.getNodeClassFromType)
30 |
31 | self._close_event_listeners = []
32 |
33 | self.scene.masterRef = masterRef
34 |
35 | self.setup_new_graph()
36 |
37 | def select_all_nodes(self):
38 | for node in self.scene.nodes:
39 | node.doSelect(True)
40 |
41 | def getNodeClassFromType(self, data):
42 | if 'node_type' not in data:
43 | return Node
44 | else:
45 | return get_class_by_type(data['node_type'])
46 |
47 | def onHistoryRestored(self):
48 | pass
49 |
50 | def initNewNodeActions(self):
51 | self.node_actions = {}
52 | Funs = list(FUNCTIONS.keys())
53 |
54 | for key in Funs:
55 | node = FUNCTIONS[key]
56 | self.node_actions[node.node_type] = QAction(QIcon(node.icon), node.name)
57 | self.node_actions[node.node_type].setData(node.node_type)
58 |
59 | def initNodesContextMenu(self):
60 | context_menu = QMenu(self)
61 | Funs = list(FUNCTIONS.keys())
62 | Funs.sort()
63 |
64 | for key in Funs:
65 | context_menu.addAction(self.node_actions[key])
66 |
67 | return context_menu
68 |
69 | def setTitle(self):
70 | self.setWindowTitle(self.getUserFriendlyFilename())
71 |
72 | def addCloseEventListener(self, callback):
73 | self._close_event_listeners.append(callback)
74 |
75 | def closeEvent(self, event):
76 | for callback in self._close_event_listeners: callback(self, event)
77 |
78 | def onDragEnter(self, event):
79 | if event.mimeData().hasFormat(LISTBOX_MIMETYPE):
80 | event.acceptProposedAction()
81 | else:
82 | if DEBUG: print(" ... denied drag enter event")
83 | event.setAccepted(False)
84 |
85 | def onDrop(self, event):
86 | if event.mimeData().hasFormat(LISTBOX_MIMETYPE):
87 | eventData = event.mimeData().data(LISTBOX_MIMETYPE)
88 | dataStream = QDataStream(eventData, QIODevice.ReadOnly)
89 | pixmap = QPixmap()
90 | dataStream >> pixmap
91 | self.node_type = dataStream.readInt()
92 | text = dataStream.readQString()
93 | newNodeData = dataStream.readQStringList()
94 |
95 |
96 | nodeType = newNodeData[0]
97 |
98 | isEvent = False
99 | is_var = False
100 | isNode = False
101 |
102 | if nodeType == "E":
103 | isEvent = True
104 | elif nodeType == "N":
105 | isNode = True
106 | elif nodeType == "V":
107 | is_var = True
108 |
109 |
110 | mouse_position = event.pos()
111 | self.scene_position = self.scene.grScene.views()[0].mapToScene(mouse_position)
112 |
113 | if DEBUG: print("GOT DROP: [%d] '%s'" % (self.node_type, text), "mouse:", mouse_position, "scene:", self.scene_position)
114 |
115 | try:
116 | if isEvent or is_var:
117 | self.varSelectMenu(event, is_var)
118 | else:
119 | node = get_class_by_type(self.node_type)(self.scene)
120 | node.setPos(self.scene_position.x(), self.scene_position.y())
121 | self.scene.history.storeHistory("Created Node %s" % node.__class__.__name__)
122 |
123 | except Exception as e:
124 | dumpException(e)
125 |
126 | event.setDropAction(Qt.MoveAction)
127 | event.accept()
128 | else:
129 | event.ignore()
130 | self.scene.node_editor.UpdateTextCode()
131 |
132 | def ActiveScene(self):
133 | return self.scene.masterRef.currentNodeEditor().scene
134 |
135 | def varSelectMenu(self, event, is_var):
136 | context_menu = QMenu(self)
137 | if is_var:
138 | set_text = 'Set'
139 | get_text = 'Get'
140 | else:
141 | set_text = 'Write'
142 | get_text = 'Call'
143 |
144 | getter = context_menu.addAction(get_text)
145 | setter = context_menu.addAction(set_text)
146 |
147 | cancel = context_menu.addAction("Cancel")
148 |
149 | action = context_menu.exec_(self.mapToGlobal(event.pos()))
150 |
151 | if action is cancel or action is None:
152 | return
153 | else:
154 | scene = self.ActiveScene()
155 | user_node = None
156 |
157 | if action == setter:
158 | user_node = scene.user_nodes_wdg.get_user_node_by_id(self.node_type)(scene, isSetter=True)
159 |
160 | elif action == getter:
161 | user_node = scene.user_nodes_wdg.get_user_node_by_id(self.node_type)(scene, isSetter=False)
162 |
163 | if user_node:
164 | user_node.setPos(self.scene_position.x(), self.scene_position.y())
165 | self.scene.history.storeHistory("Created user Variable %s" % user_node.__class__.__name__)
166 |
167 | def contextMenuEvent(self, event):
168 | try:
169 | item = self.scene.getItemAt(event.pos())
170 | if DEBUG_CONTEXT: print(item)
171 |
172 | if type(item) == QGraphicsProxyWidget:
173 | item = item.widget()
174 |
175 | if hasattr(item, 'node') or hasattr(item, 'socket'):
176 | self.handleNodeContextMenu(event)
177 | elif hasattr(item, 'edge'):
178 | self.handleEdgeContextMenu(event)
179 | # elif item is None:
180 | else:
181 | self.handleNewNodeContextMenu(event)
182 |
183 | return super().contextMenuEvent(event)
184 | except Exception as e:
185 | dumpException(e)
186 |
187 | def handleNodeContextMenu(self, event):
188 | if DEBUG_CONTEXT: print("CONTEXT: NODE")
189 | context_menu = QMenu(self)
190 | copy = context_menu.addAction("Copy")
191 | cut = context_menu.addAction("Cut")
192 | delete = context_menu.addAction("Delete")
193 | action = context_menu.exec_(self.mapToGlobal(event.pos()))
194 |
195 | selected = None
196 | item = self.scene.getItemAt(event.pos())
197 | if type(item) == QGraphicsProxyWidget:
198 | item = item.widget()
199 |
200 | if hasattr(item, 'node'):
201 | selected = item.node
202 | if hasattr(item, 'socket'):
203 | selected = item.socket.node
204 |
205 | if action == delete:
206 | self.scene.getView().deleteSelected()
207 | if action == copy:
208 | self.scene.masterRef.onEditCopy()
209 | if action == cut:
210 | self.scene.masterRef.onEditCut()
211 |
212 | def handleEdgeContextMenu(self, event):
213 | if DEBUG_CONTEXT: print("CONTEXT: EDGE")
214 | context_menu = QMenu(self)
215 | bezierAct = context_menu.addAction("Bezier Edge")
216 | directAct = context_menu.addAction("Direct Edge")
217 | squareAct = context_menu.addAction("Square Edge")
218 | action = context_menu.exec_(self.mapToGlobal(event.pos()))
219 |
220 | selected = None
221 | item = self.scene.getItemAt(event.pos())
222 | if hasattr(item, 'edge'):
223 | selected = item.edge
224 |
225 | if selected and action == bezierAct: selected.edge_type = EDGE_TYPE_BEZIER
226 | if selected and action == directAct: selected.edge_type = EDGE_TYPE_DIRECT
227 | if selected and action == squareAct: selected.edge_type = EDGE_TYPE_SQUARE
228 |
229 | # helper functions
230 | def determine_target_socket_of_node(self, was_dragged_flag, new_calc_node):
231 | target_socket = None
232 | if was_dragged_flag:
233 | if len(new_calc_node.inputs) > 0: target_socket = new_calc_node.inputs[0]
234 | else:
235 | if len(new_calc_node.outputs) > 0: target_socket = new_calc_node.outputs[0]
236 | return target_socket
237 |
238 | def finish_new_node_state(self, new_node):
239 | self.scene.doDeselectItems()
240 | new_node.grNode.doSelect(True)
241 | new_node.grNode.onSelected()
242 |
243 | def handleNewNodeContextMenu(self, event):
244 |
245 | if DEBUG_CONTEXT: print("CONTEXT: EMPTY SPACE")
246 | context_menu = self.initNodesContextMenu()
247 | action = context_menu.exec_(self.mapToGlobal(event.pos()))
248 |
249 | if action is not None:
250 | new_node = get_class_by_type(action.data())(self.scene)
251 | scene_pos = self.scene.getView().mapToScene(event.pos())
252 | new_node.setPos(scene_pos.x(), scene_pos.y())
253 | self.scene.node_editor.UpdateTextCode()
254 |
255 | if DEBUG_CONTEXT: print("Selected node:", new_node)
256 |
257 | if self.scene.getView().mode == MODE_EDGE_DRAG:
258 | # if we were dragging an edge...
259 | target_socket = self.determine_target_socket_of_node(
260 | self.scene.getView().dragging.drag_start_socket.is_output, new_node)
261 | if target_socket is not None:
262 | self.scene.getView().dragging.edgeDragEnd(target_socket.grSocket)
263 | self.finish_new_node_state(new_node)
264 |
265 | else:
266 | self.scene.history.storeHistory("Created %s" % new_node.__class__.__name__)
267 |
--------------------------------------------------------------------------------
/vvs_app/master_node.py:
--------------------------------------------------------------------------------
1 | from qtpy.QtGui import *
2 | from qtpy.QtCore import *
3 | from qtpy.QtWidgets import *
4 |
5 | from node_node import Node
6 | from node_graphics_node import QDMGraphicsNode
7 | from utils import dumpException
8 |
9 |
10 | class MasterNode(Node):
11 | name = "MasterNode"
12 | icon = ''
13 |
14 | def __init__(self, scene, inputs, outputs):
15 | super().__init__(scene, self.name, inputs, outputs, node_icon=self.icon)
16 | pass
17 |
18 | def serialize(self):
19 | res = super().serialize()
20 | res['node_type'] = self.__class__.node_type
21 | return res
22 |
23 | def deserialize(self, data, hashmap={}, restore_id=True):
24 | res = super().deserialize(data, hashmap, restore_id)
25 | # print("Deserialized Node '%s'" % self.__class__.__name__, "res:", res)
26 | return res
--------------------------------------------------------------------------------
/vvs_app/node_edge_dragging.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | A module containing the Edge Dragging functionality
4 | """
5 | from PyQt5.QtWidgets import QGraphicsItem, QGraphicsView
6 |
7 | from node_socket import QDMGraphicsSocket
8 | from node_edge import EDGE_TYPE_DEFAULT
9 | from utils import dumpException
10 |
11 | DEBUG = False
12 |
13 |
14 | class EdgeDragging:
15 | def __init__(self, grView: 'QGraphicsView'):
16 | self.grView = grView
17 | # initializing these variable to know we're using them in this class...
18 | self.drag_edge = None
19 | self.drag_start_socket = None
20 |
21 | def getEdgeClass(self):
22 | """Helper function to get the Edge class. Using what Scene class provides"""
23 | return self.grView.grScene.scene.getEdgeClass()
24 |
25 | def updateDestination(self, x: float, y: float):
26 | """
27 | Update the end point of our dragging edge
28 |
29 | :param x: new X scene position
30 | :param y: new Y scene position
31 | """
32 | # according to sentry: 'NoneType' object has no attribute 'grEdge'
33 | if self.drag_edge is not None and self.drag_edge.grEdge is not None:
34 | self.drag_edge.grEdge.setDestination(x, y)
35 | self.drag_edge.grEdge.update()
36 | else:
37 | print(">>> Want to update self.drag_edge grEdge, but it's None!!!")
38 |
39 | def edgeDragStart(self, item: 'QGraphicsItem'):
40 | """Code handling the start of a dragging an `Edge` operation"""
41 | try:
42 | if DEBUG: print('View::edgeDragStart ~ Start dragging edge')
43 | if DEBUG: print('View::edgeDragStart ~ assign Start Socket to:', item.socket)
44 | self.drag_start_socket = item.socket
45 | self.drag_edge = self.getEdgeClass()(item.socket.node.scene, item.socket, None, EDGE_TYPE_DEFAULT)
46 | self.drag_edge.grEdge.makeUnselectable()
47 | if DEBUG: print('View::edgeDragStart ~ dragEdge:', self.drag_edge)
48 | except Exception as e:
49 | dumpException(e)
50 |
51 | def edgeDragEnd(self, item: 'QGraphicsItem'):
52 | """Code handling the end of the dragging an `Edge` operation. If this code returns True then skip the
53 | rest of the mouse event processing. Can be called with ``None`` to cancel the edge dragging mode
54 |
55 | :param item: Item in the `Graphics Scene` where we ended dragging an `Edge`
56 | :type item: ``QGraphicsItem``
57 | """
58 |
59 | # early out - clicked on something else than Socket
60 | if not isinstance(item, QDMGraphicsSocket):
61 | self.grView.resetMode()
62 | if DEBUG: print('View::edgeDragEnd ~ End dragging edge early')
63 | self.drag_edge.remove(silent=True) # don't notify sockets about removing drag_edge
64 | self.drag_edge = None
65 |
66 | # clicked on socket
67 | if isinstance(item, QDMGraphicsSocket):
68 |
69 | # check if edge would be valid
70 | if not self.drag_edge.validateEdge(self.drag_start_socket, item.socket):
71 | print("NOT VALID EDGE")
72 | return False
73 |
74 | # regular processing of drag edge
75 | self.grView.resetMode()
76 |
77 | if DEBUG: print('View::edgeDragEnd ~ End dragging edge')
78 | self.drag_edge.remove(silent=True) # don't notify sockets about removing drag_edge
79 | self.drag_edge = None
80 |
81 | try:
82 | if item.socket != self.drag_start_socket:
83 | # if we released dragging on a socket (other then the beginning socket)
84 |
85 | ## First remove old edges / send notifications
86 | for socket in (item.socket, self.drag_start_socket):
87 | if not socket.is_multi_edges:
88 | if socket.is_input:
89 | # print("removing SILENTLY edges from input socket (is_input and !is_multi_edges) [DragStart]:", item.socket.edges)
90 | socket.removeAllEdges(silent=True)
91 | else:
92 | socket.removeAllEdges(silent=False)
93 |
94 | ## Create new Edge
95 | new_edge = self.getEdgeClass()(item.socket.node.scene, self.drag_start_socket, item.socket,
96 | edge_type=EDGE_TYPE_DEFAULT)
97 | if DEBUG: print("View::edgeDragEnd ~ created new edge:", new_edge, "connecting",
98 | new_edge.start_socket, "<-->", new_edge.end_socket)
99 |
100 | ## Send notifications for the new edge
101 | for socket in [self.drag_start_socket, item.socket]:
102 | # @TODO: Add possibility (ie when an input edge was replaced) to be silent and don't trigger change
103 | socket.node.onEdgeConnectionChanged(new_edge)
104 | if socket.is_input: socket.node.onInputChanged(socket)
105 |
106 | self.grView.grScene.scene.history.storeHistory("Created new edge by dragging", setModified=True)
107 | return True
108 | except Exception as e:
109 | dumpException(e)
110 |
111 | if DEBUG: print('View::edgeDragEnd ~ everything done.')
112 | return False
113 |
--------------------------------------------------------------------------------
/vvs_app/node_edge_intersect.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | A module containing the intersecting nodes functionality. If a node gets dragged and dropped on an existing edge
4 | it will intersect that edge.
5 | """
6 | from qtpy.QtWidgets import QGraphicsView
7 | from qtpy.QtCore import QRectF
8 | from node_edge import Edge
9 |
10 |
11 | class EdgeIntersect:
12 |
13 | def __init__(self, grView: "QGraphicsView"):
14 | self.grScene = grView.grScene
15 | self.grView = grView
16 | self.draggedNode = None
17 | self.hoveredList = []
18 |
19 | def enterState(self, node: "Node"):
20 | """
21 | Initialize when we enter the state
22 |
23 | :param node: :class:`~nodeeditor.node_node.Node` which we started to drag
24 | :type node: :class:`~nodeeditor.node_node.Node`
25 | """
26 | self.hoveredList = []
27 | self.draggedNode = node
28 |
29 | def leaveState(self, scene_pos_x: float, scene_pos_y: float):
30 | """
31 | Deinit when we leave this state
32 |
33 | :param scene_pos_x: scene position x
34 | :type scene_pos_x: `float`
35 | :param scene_pos_y: scene position y
36 | :type scene_pos_y: `float`
37 | """
38 | self.dropNode(self.draggedNode, scene_pos_x, scene_pos_y)
39 | self.draggedNode = None
40 | self.hoveredList = []
41 |
42 | def dropNode(self, node: "Node", scene_pos_x: float, scene_pos_y: float):
43 | """
44 | Code handling the dropping of a node on an existing edge.
45 |
46 | :param scene_pos_x: scene position x
47 | :type scene_pos_x: `float`
48 | :param scene_pos_y: scene position y
49 | :type scene_pos_y: `float`
50 | """
51 |
52 | node_box = self.hotZoneRect(node)
53 |
54 | # check if the node is dropped on an existing edge
55 | edge = self.intersect(node_box)
56 | if edge is None: return
57 |
58 | if self.isConnected(node): return
59 |
60 |
61 | # determine the order of start and end
62 | if edge.start_socket.is_output:
63 | socket_start = edge.start_socket
64 | socket_end = edge.end_socket
65 | else:
66 | socket_start = edge.end_socket
67 | socket_end = edge.start_socket
68 |
69 | #
70 |
71 | # The new edges will have the same edge_type as the intersected edge
72 | edge_type = edge.edge_type
73 | edge.remove()
74 | self.grView.grScene.scene.history.storeHistory('Delete existing edge', setModified=True)
75 |
76 | new_node_socket_in = node.inputs[0]
77 | Edge(self.grScene.scene, socket_start, new_node_socket_in, edge_type=edge_type)
78 | new_node_socket_out = node.outputs[0]
79 | Edge(self.grScene.scene, new_node_socket_out, socket_end, edge_type=edge_type)
80 |
81 | self.grView.grScene.scene.history.storeHistory('Created new edges by dropping node', setModified=True)
82 |
83 | def hotZoneRect(self, node: 'Node') -> 'QRectF':
84 | """
85 | Returns A QRectF of creating a box around a node
86 |
87 | :param node: :class:`~nodeeditor.node_node.Node` for which we want to get `QRectF` describing its position and area
88 | :type node: :class:`~nodeeditor.node_node.Node`
89 | :return: `QRectF` describing node's position and area
90 | :rtype: `QRectF`
91 | """
92 | nodePos = node.grNode.scenePos()
93 | x = nodePos.x()
94 | y = nodePos.y()
95 | w = node.grNode.width
96 | h = node.grNode.height
97 | return QRectF(x, y, w, h)
98 |
99 |
100 | def update(self, scene_pos_x: float, scene_pos_y: float):
101 | """
102 | Updating during mouse move when grView is in this state
103 |
104 | :param scene_pos_x: scene position x
105 | :type scene_pos_x: `float`
106 | :param scene_pos_y: scene position y
107 | :type scene_pos_y: `float`
108 | """
109 | rect = self.hotZoneRect(self.draggedNode)
110 | grItems = self.grScene.items(rect)
111 | for grEdge in self.hoveredList:
112 | grEdge.hovered = False
113 | self.hoveredList = []
114 | for grItem in grItems:
115 | if hasattr(grItem, 'edge') and not self.draggedNode.hasConnectedEdge(grItem.edge):
116 | self.hoveredList.append(grItem)
117 | grItem.hovered = True
118 |
119 | def intersect(self, node_box: 'QRectF') -> 'Edge':
120 | """
121 | Checking for intersection of a rectangle (usually a `Node`) with edges in the scene
122 |
123 | :param node_box: `QRectF` for which we want find intersecting `Edges`
124 | :type node_box: `QRectF`
125 | :return: :class:`~nodeeditor.node_edge.Edge` or `None` if the node is being cut by an `Edge`
126 | :rtype: :class:`~nodeeditor.node_edge.Edge`
127 | """
128 | # returns the first edge that intersects with the dropped node, ignores the rest
129 | grItems = self.grScene.items(node_box)
130 | for grItem in grItems:
131 | if hasattr(grItem, 'edge') and not self.draggedNode.hasConnectedEdge(grItem.edge):
132 | return grItem.edge
133 | return None
134 |
135 | def isConnected(self, node: 'Node'):
136 | """
137 | Return ``True`` if node got any connections
138 |
139 | :param node: :class:`~nodeeditor.node_node.Node` which connections to check
140 | :type node: :class:`~nodeeditor.node_node.Node`
141 | :return:
142 | """
143 | # Nodes with only inputs or outputs are excluded
144 | if node.inputs == [] or node.outputs == []:
145 | return True
146 |
147 | # Check if the node has edges connected
148 | return node.getInput() or node.getOutputs()
149 |
--------------------------------------------------------------------------------
/vvs_app/node_edge_rerouting.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | A module containing the Edge Rerouting functionality
4 | """
5 |
6 |
7 | DEBUG_REROUTING = True
8 |
9 |
10 | class EdgeRerouting:
11 | def __init__(self, grView: 'QGraphicsView'):
12 | self.grView = grView
13 | self.start_socket = None # store where we started re-routing the edges
14 | self.rerouting_edges = [] # edges representing the re-routing (dashed edges)
15 | self.is_rerouting = False # are we currently re-routing?
16 | self.first_mb_release = False # flag for detecting if we already clicked with the rerouting LMB release
17 |
18 | def print(self, *args):
19 | """Helper function to better control debug printing to console for this feature"""
20 | if DEBUG_REROUTING: print("REROUTING:", *args)
21 |
22 | def getEdgeClass(self):
23 | """Helper function to get the Edge class. Using what the Scene class provides"""
24 | return self.grView.grScene.scene.getEdgeClass()
25 |
26 | def getAffectedEdges(self) -> list:
27 | """
28 | Get a list of all edges connected to the `self.start_socket` where we started the re-routing
29 |
30 | :return: List of all edges affected by the rerouting started from this `self.start_socket` :class:`~nodeeditor.node_socket.Socket`
31 | :rtype: ``list``
32 | """
33 | if self.start_socket is None:
34 | return [] # no starting socket assigned, so no edges for us
35 | # return edges connected to the socket
36 | return self.start_socket.socketEdges.copy()
37 |
38 | def setAffectedEdgesVisible(self, visibility: bool=True):
39 | """
40 | Show/Hide all edges connected to the `self.start_socket` where we started the re-routing
41 |
42 | :param visibility: ``True`` if all the affected :class:`~nodeeditor.node_edge.Edge` (s) should be shown or hidden
43 | :type visibility: ``bool``
44 | """
45 | for edge in self.getAffectedEdges():
46 | if visibility: edge.grEdge.show()
47 | else: edge.grEdge.hide()
48 |
49 | def resetRerouting(self):
50 | """Reset to default state. Init this feature internal variables"""
51 | self.is_rerouting = False
52 | self.start_socket = None
53 | self.first_mb_release = False
54 | # holding all rerouting edges should be empty at this point...
55 | # self.rerouting_edges = []
56 |
57 | def clearReroutingEdges(self):
58 | """Remove the helping dashed edges from the :class:`~nodeeditor.node_scene.Scene`"""
59 | self.print("clean called")
60 | while self.rerouting_edges != []:
61 | edge = self.rerouting_edges.pop()
62 | self.print("\twant to clean:", edge)
63 | edge.remove()
64 |
65 | def updateScenePos(self, x: float, y: float):
66 | """
67 | Update position of all the rerouting edges (dashed ones). Called from mouseMove event to update to new mouse position
68 |
69 | :param x: new X position
70 | :type x: ``float``
71 | :param y: new Y position
72 | :type y: ``float``
73 | """
74 | if self.is_rerouting:
75 | for edge in self.rerouting_edges:
76 | if edge and edge.grEdge:
77 | edge.grEdge.setDestination(x, y)
78 | edge.grEdge.update()
79 |
80 | def startRerouting(self, socket: 'Socket'):
81 | """
82 | Method to start the re-routing. Called from the grView's state machine.
83 |
84 | :param socket: :class:`~nodeeditor.node_socket.Socket` where we started the re-routing
85 | :type socket: :class:`~nodeeditor.node_socket.Socket`
86 | """
87 | self.print("startRerouting", socket)
88 | self.is_rerouting = True
89 | self.start_socket = socket
90 |
91 | self.print("numEdges:", len(self.getAffectedEdges()))
92 | self.setAffectedEdgesVisible(visibility=False)
93 |
94 | start_position = self.start_socket.node.getSocketScenePosition(self.start_socket)
95 |
96 | for edge in self.getAffectedEdges():
97 | other_socket = edge.getOtherSocket(self.start_socket)
98 |
99 | new_edge = self.getEdgeClass()(self.start_socket.node.scene, edge_type=edge.edge_type)
100 | new_edge.start_socket = other_socket
101 | new_edge.grEdge.setSource(*other_socket.node.getSocketScenePosition(other_socket))
102 | new_edge.grEdge.setDestination(*start_position)
103 | new_edge.grEdge.update()
104 | self.rerouting_edges.append(new_edge)
105 |
106 |
107 | def stopRerouting(self, target: 'Socket'=None):
108 | """
109 | Method for stopping the re-routing
110 |
111 | :param target: Target where we ended the rerouting (usually released mouse button). Provide ``Socket`` or ``None`` to cancel
112 | :type target: :class:`~nodeeditor.node_socket.Socket` or ``None``
113 | """
114 | self.print("stopRerouting on:", target, "no change" if target==self.start_socket else "")
115 |
116 | if self.start_socket is not None:
117 | # reset start socket highlight
118 | self.start_socket.grSocket.isHighlighted = False
119 |
120 | # collect all affected (node, edge) tuples in the meantime.. if necessary
121 | affected_nodes = []
122 |
123 | if target is None or target == self.start_socket:
124 | # canceling -> no change
125 | self.setAffectedEdgesVisible(visibility=True)
126 |
127 | else:
128 | # validate edges before doing anything else
129 | valid_edges, invalid_edges = self.getAffectedEdges(), []
130 | for edge in self.getAffectedEdges():
131 | start_sock = edge.getOtherSocket(self.start_socket)
132 | if not edge.validateEdge(start_sock, target):
133 | # not valid edge
134 | self.print("This edge rerouting is not valid!", edge)
135 | invalid_edges.append(edge)
136 |
137 | # remove the invalidated edges from the list
138 | for invalid_edge in invalid_edges:
139 | valid_edges.remove(invalid_edge)
140 |
141 | # reconnect to new socket
142 | self.print("should reconnect from:", self.start_socket, "-->", target)
143 |
144 | self.setAffectedEdgesVisible(visibility=True)
145 |
146 | for edge in valid_edges:
147 | for node in [edge.start_socket.node, edge.end_socket.node]:
148 | if node not in affected_nodes:
149 | affected_nodes.append((node, edge))
150 |
151 | if target.is_input:
152 | target.removeAllEdges(silent=True)
153 |
154 | if edge.end_socket == self.start_socket:
155 | edge.end_socket = target
156 | else:
157 | edge.start_socket = target
158 |
159 | edge.updatePositions()
160 |
161 |
162 | # hide rerouting edges
163 | self.clearReroutingEdges()
164 |
165 | # Send notifications for all affected nodes
166 | for affected_node, edge in affected_nodes:
167 | affected_node.onEdgeConnectionChanged(edge)
168 | if edge.start_socket in affected_node.inputs:
169 | affected_node.onInputChanged(edge.start_socket)
170 | if edge.end_socket in affected_node.inputs:
171 | affected_node.onInputChanged(edge.end_socket)
172 |
173 | # store history stamp
174 | self.start_socket.node.scene.history.storeHistory("Rerouted edges", setModified=True)
175 |
176 | # reset variables of this rerouting state
177 | self.resetRerouting()
178 |
--------------------------------------------------------------------------------
/vvs_app/node_edge_snapping.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | A module containing the Edge Snapping functions which are used in :class:`~nodeeditor.node_graphics_view.QDMGraphicsView` class.
4 | """
5 |
6 |
7 | from qtpy.QtCore import QPointF, QRectF
8 | from node_socket import QDMGraphicsSocket
9 |
10 |
11 | class EdgeSnapping():
12 | def __init__(self, grView: 'QGraphicsView', snapping_radius: float = 24):
13 | self.grView = grView
14 | self.grScene = self.grView.grScene
15 | self.edge_snapping_radius = snapping_radius
16 |
17 | def getSnappedSocketItem(self, event: 'QMouseEvent') -> 'QDMGraphicsSocket':
18 | """Returns :class:`~nodeeditor.node_graphics_socket.QDMGraphicsSocket` which we should snap to"""
19 | scenepos = self.grView.mapToScene(event.pos())
20 | grSocket, pos = self.getSnappedToSocketPosition(scenepos)
21 | return grSocket
22 |
23 | def getSnappedToSocketPosition(self, scenepos: QPointF) -> ('QDMGraphicsSocket', QPointF):
24 | """
25 | Returns grSocket and Scene position to nearest Socket or original position if no nearby Socket found
26 |
27 | :param scenepos: From which point should I snap?
28 | :type scenepos: ``QPointF``
29 | :return: grSocket and Scene postion to nearest socket
30 | """
31 | scanrect = QRectF(
32 | scenepos.x() - self.edge_snapping_radius, scenepos.y() - self.edge_snapping_radius,
33 | self.edge_snapping_radius * 2, self.edge_snapping_radius * 2
34 | )
35 | items = self.grScene.items(scanrect)
36 | items = list(filter(lambda x: isinstance(x, QDMGraphicsSocket), items))
37 |
38 | if len(items) == 0:
39 | return None, scenepos
40 |
41 | selected_item = items[0]
42 | if len(items) > 1:
43 | # calculate the nearest socket
44 | nearest = 10000000000
45 | for grsock in items:
46 | grsock_scenepos = grsock.socket.node.getSocketScenePosition(grsock.socket)
47 | qpdist = QPointF(*grsock_scenepos) - scenepos
48 | dist = qpdist.x() * qpdist.x() + qpdist.y() * qpdist.y()
49 | if dist < nearest:
50 | nearest, selected_item = dist, grsock
51 |
52 | selected_item.isHighlighted = True
53 |
54 | calcpos = selected_item.socket.node.getSocketScenePosition(selected_item.socket)
55 |
56 | return selected_item, QPointF(*calcpos)
--------------------------------------------------------------------------------
/vvs_app/node_edge_validators.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | A module containing the Edge Validator functions which can be registered as callbacks to
4 | :class:`~nodeeditor.node_edge.Edge` class.
5 |
6 | Example of registering Edge Validator callbacks:
7 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 |
9 | You can register validation callbacks once for example on the bottom of node_edge.py file or on the
10 | application start with calling this:
11 |
12 | .. code-block:: python
13 |
14 | from node_edge_validators import *
15 |
16 | Edge.registerEdgeValidator(edge_validator_debug)
17 | Edge.registerEdgeValidator(edge_cannot_connect_two_outputs_or_two_inputs)
18 | Edge.registerEdgeValidator(edge_cannot_connect_input_and_output_of_same_node)
19 | Edge.registerEdgeValidator(edge_cannot_connect_input_and_output_of_different_type)
20 |
21 |
22 | """
23 |
24 | from node_socket import *
25 |
26 | DEBUG = False
27 |
28 |
29 | def print_error(*args):
30 | """Helper method which prints to console if `DEBUG` is set to `True`"""
31 | if DEBUG: print("Edge Validation Error:", *args)
32 |
33 | def edge_validator_debug(input: 'Socket', output: 'Socket') -> bool:
34 | """This will consider edge always valid, however writes bunch of debug stuff into console"""
35 | print("VALIDATING:")
36 | print(input, "input" if input.is_input else "output", "of node", input.node)
37 | for s in input.node.inputs+input.node.outputs: print("\t", s, "input" if s.is_input else "output")
38 | print(output, "input" if input.is_input else "output", "of node", output.node)
39 | for s in output.node.inputs+output.node.outputs: print("\t", s, "input" if s.is_input else "output")
40 |
41 | return True
42 |
43 | def edge_cannot_connect_two_outputs_or_two_inputs(input: 'Socket', output: 'Socket') -> bool:
44 | """Edge is invalid if it connects 2 output sockets or 2 input sockets"""
45 | if input.is_output and output.is_output:
46 | print_error("Connecting 2 outputs")
47 | return False
48 |
49 | if input.is_input and output.is_input:
50 | print_error("Connecting 2 inputs")
51 | return False
52 |
53 | return True
54 |
55 | def edge_cannot_connect_input_and_output_of_same_node(input: 'Socket', output:'Socket') -> bool:
56 | """Edge is invalid if it connects the same node"""
57 | if input.node == output.node:
58 | print_error("Connecting the same node")
59 | return False
60 |
61 | return True
62 |
63 | def edge_cannot_connect_input_and_output_of_different_type(input: 'Socket', output: 'Socket') -> bool:
64 | """Edge is invalid if it connects sockets with different colors"""
65 |
66 | if (input.socket_type != 0 and output.socket_type == 0) or (input.socket_type == 0 and output.socket_type != 0):
67 | print("Trying to connect an action to a value")
68 | return False
69 |
70 | elif input.socket_type != output.socket_type:
71 | if not(input.socket_type == 6 or output.socket_type == 6):
72 | if not(input.grSocket.shape == 1 and output.socket_type == 5) or not(output.grSocket.shape == 1 and input.socket_type == 5):
73 | print("Trying to connect an different values")
74 | return False
75 |
76 | return True
77 |
--------------------------------------------------------------------------------
/vvs_app/node_graphics_cutline.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | A module containing the class for Cutting Line
4 | """
5 | from qtpy.QtGui import QPen, QPainterPath, QPolygonF, QPainter
6 | from qtpy.QtWidgets import QGraphicsItem, QWidget
7 | from qtpy.QtCore import Qt, QRectF, QPointF
8 |
9 |
10 | class QDMCutLine(QGraphicsItem):
11 | """Class representing Cutting Line used for cutting multiple `Edges` with one stroke"""
12 | def __init__(self, parent:QWidget=None):
13 | """
14 | :param parent: parent widget
15 | :type parent: ``QWidget``
16 | """
17 | super().__init__(parent)
18 |
19 | self.line_points = []
20 |
21 | self._pen = QPen(Qt.white)
22 | self._pen.setWidthF(2)
23 | self._pen.setDashPattern([3, 3])
24 |
25 | self.setZValue(2)
26 |
27 | def boundingRect(self) -> QRectF:
28 | """Defining Qt' bounding rectangle"""
29 | return self.shape().boundingRect()
30 |
31 | def shape(self) -> QPainterPath:
32 | """Calculate the QPainterPath object from list of line points
33 |
34 | :return: shape function returning ``QPainterPath`` representation of Cutting Line
35 | :rtype: ``QPainterPath``
36 | """
37 | poly = QPolygonF(self.line_points)
38 |
39 | if len(self.line_points) > 1:
40 | path = QPainterPath(self.line_points[0])
41 | for pt in self.line_points[1:]:
42 | path.lineTo(pt)
43 | else:
44 | path = QPainterPath(QPointF(0,0))
45 | path.lineTo(QPointF(1,1))
46 |
47 | return path
48 |
49 | def paint(self, painter, QStyleOptionGraphicsItem, widget=None):
50 | """Paint the Cutting Line"""
51 | painter.setRenderHint(QPainter.Antialiasing)
52 | painter.setBrush(Qt.NoBrush)
53 | painter.setPen(self._pen)
54 |
55 | poly = QPolygonF(self.line_points)
56 | painter.drawPolyline(poly)
--------------------------------------------------------------------------------
/vvs_app/node_graphics_edge.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | A module containing the Graphics representation of an Edge
4 | """
5 | from qtpy.QtWidgets import QGraphicsPathItem, QWidget, QGraphicsItem
6 | from qtpy.QtGui import QColor, QPen, QPainterPath
7 | from qtpy.QtCore import Qt, QRectF, QPointF
8 |
9 | from node_graphics_edge_path import GraphicsEdgePathBezier, GraphicsEdgePathDirect, GraphicsEdgePathSquare
10 |
11 |
12 | class QDMGraphicsEdge(QGraphicsPathItem):
13 | """Base class for Graphics Edge"""
14 | def __init__(self, edge: 'Edge', parent: QWidget = None):
15 | """
16 | :param edge: reference to :class:`~nodeeditor.node_edge.Edge`
17 | :type edge: :class:`~nodeeditor.node_edge.Edge`
18 | :param parent: parent widget
19 | :type parent: ``QWidget``
20 |
21 | :Instance attributes:
22 |
23 | - **edge** - reference to :class:`~nodeeditor.node_edge.Edge`
24 | - **posSource** - ``[x, y]`` source position in the `Scene`
25 | - **posDestination** - ``[x, y]`` destination position in the `Scene`
26 | """
27 | super().__init__(parent)
28 |
29 | self.edge = edge
30 |
31 | # create instance of our path class
32 | self.pathCalculator = self.determineEdgePathClass()(self)
33 |
34 | # init our flags
35 | self._last_selected_state = False
36 | self.hovered = False
37 |
38 | # init our variables
39 | self.posSource = [0, 0]
40 | self.posDestination = [200, 100]
41 |
42 | self.initAssets()
43 | self.initUI()
44 |
45 | def initUI(self):
46 | """Set up this ``QGraphicsPathItem``"""
47 | self.setFlag(QGraphicsItem.ItemIsSelectable)
48 | self.setAcceptHoverEvents(True)
49 | self.setZValue(-1)
50 |
51 | def initAssets(self):
52 | """Initialize ``QObjects`` like ``QColor``, ``QPen`` and ``QBrush``"""
53 | self._color = self._default_color = QColor("#101010")
54 | self._color_selected = QColor("#FFFFA637")
55 | self._color_hovered = QColor("#FFFFFF")
56 | self._pen = QPen(self._color)
57 | self._pen_selected = QPen(self._color_selected)
58 | self._pen_dragging = QPen(QColor("#90000000"))
59 | self._pen_hovered = QPen(self._color_hovered)
60 | # self._pen_dragging.setStyle(Qt.DashLine)
61 | self._pen.setWidthF(4.0)
62 | self._pen_selected.setWidthF(4.0)
63 | self._pen_dragging.setWidthF(3.0)
64 | self._pen_hovered.setWidthF(5.0)
65 |
66 | def createEdgePathCalculator(self):
67 | """Create instance of :class:`~nodeeditor.node_graphics_edge_path.GraphicsEdgePathBase`"""
68 | self.pathCalculator = self.determineEdgePathClass()(self)
69 | return self.pathCalculator
70 |
71 | def determineEdgePathClass(self):
72 | """Decide which GraphicsEdgePath class should be used to calculate path according to edge.edge_type value"""
73 | from node_edge import EDGE_TYPE_BEZIER, EDGE_TYPE_DIRECT, EDGE_TYPE_SQUARE
74 | if self.edge.edge_type == EDGE_TYPE_BEZIER:
75 | return GraphicsEdgePathBezier
76 | if self.edge.edge_type == EDGE_TYPE_DIRECT:
77 | return GraphicsEdgePathDirect
78 | if self.edge.edge_type == EDGE_TYPE_SQUARE:
79 | return GraphicsEdgePathSquare
80 | else:
81 | return GraphicsEdgePathBezier
82 |
83 | def makeUnselectable(self):
84 | """Used for drag edge to disable click detection over this graphics item"""
85 | self.setFlag(QGraphicsItem.ItemIsSelectable, False)
86 | self.setAcceptHoverEvents(False)
87 |
88 | def changeColor(self, color):
89 | """Change color of the edge from string hex value '#00ff00'"""
90 | # print("^Called change color to:", color.red(), color.green(), color.blue(), "on edge:", self.edge)
91 | self._color = QColor(color) if type(color) == str else color
92 | self._pen = QPen(self._color)
93 | self._pen.setWidthF(3)
94 |
95 | def setColorFromSockets(self) -> bool:
96 | """Change color according to connected sockets. Returns ``True`` if color can be determined"""
97 | socket_type_start = self.edge.start_socket.SOCKET_COLORS
98 | socket_type_end = self.edge.end_socket.SOCKET_COLORS
99 | if socket_type_start != socket_type_end: return False
100 | self.changeColor(self.edge.start_socket.grSocket.getSocketColor(socket_type_start))
101 |
102 | def onSelected(self):
103 | """Our event handling when the edge was selected"""
104 | self.edge.scene.grScene.itemSelected.emit()
105 |
106 | def doSelect(self, new_state:bool=True):
107 | """Safe version of selecting the `Graphics Node`. Takes care about the selection state flag used internally
108 |
109 | :param new_state: ``True`` to select, ``False`` to deselect
110 | :type new_state: ``bool``
111 | """
112 | self.setSelected(new_state)
113 | self._last_selected_state = new_state
114 | if new_state: self.onSelected()
115 |
116 | def mouseReleaseEvent(self, event):
117 | """Overridden Qt's method to handle selecting and deselecting this `Graphics Edge`"""
118 | super().mouseReleaseEvent(event)
119 | if self._last_selected_state != self.isSelected():
120 | self.edge.scene.resetLastSelectedStates()
121 | self._last_selected_state = self.isSelected()
122 | self.onSelected()
123 |
124 | def hoverEnterEvent(self, event: 'QGraphicsSceneHoverEvent') -> None:
125 | """Handle hover effect"""
126 | self.hovered = True
127 | self.update()
128 |
129 | def hoverLeaveEvent(self, event: 'QGraphicsSceneHoverEvent') -> None:
130 | """Handle hover effect"""
131 | self.hovered = False
132 | self.update()
133 |
134 | def setSource(self, x:float, y:float):
135 | """ Set source point
136 |
137 | :param x: x position
138 | :type x: ``float``
139 | :param y: y position
140 | :type y: ``float``
141 | """
142 | self.posSource = [x, y]
143 |
144 | def setDestination(self, x:float, y:float):
145 | """ Set destination point
146 |
147 | :param x: x position
148 | :type x: ``float``
149 | :param y: y position
150 | :type y: ``float``
151 | """
152 | self.posDestination = [x, y]
153 |
154 | def boundingRect(self) -> QRectF:
155 | """Defining Qt' bounding rectangle"""
156 | return self.shape().boundingRect()
157 |
158 | def shape(self) -> QPainterPath:
159 | """Returns ``QPainterPath`` representation of this `Edge`
160 |
161 | :return: path representation
162 | :rtype: ``QPainterPath``
163 | """
164 | return self.calcPath()
165 |
166 | def paint(self, painter, QStyleOptionGraphicsItem, widget=None):
167 | """Qt's overridden method to paint this Graphics Edge. Path calculated
168 | in :func:`~nodeeditor.node_graphics_edge.QDMGraphicsEdge.calcPath` method"""
169 | self.setPath(self.calcPath())
170 |
171 | painter.setBrush(Qt.NoBrush)
172 |
173 | if self.hovered and self.edge.end_socket is not None:
174 | painter.setPen(self._pen_hovered)
175 | painter.drawPath(self.path())
176 |
177 | if self.edge.end_socket is None:
178 | painter.setPen(self._pen_dragging)
179 | else:
180 | painter.setPen(self._pen if not self.isSelected() else self._pen_selected)
181 |
182 | painter.drawPath(self.path())
183 |
184 | def intersectsWith(self, p1:QPointF, p2:QPointF) -> bool:
185 | """Does this Graphics Edge intersect with the line between point A and point B ?
186 |
187 | :param p1: point A
188 | :type p1: ``QPointF``
189 | :param p2: point B
190 | :type p2: ``QPointF``
191 | :return: ``True`` if this `Graphics Edge` intersects
192 | :rtype: ``bool``
193 | """
194 | cutpath = QPainterPath(p1)
195 | cutpath.lineTo(p2)
196 | path = self.calcPath()
197 | return cutpath.intersects(path)
198 |
199 | def calcPath(self) -> QPainterPath:
200 | """Will handle drawing QPainterPath from Point A to B. Internally there exist self.pathCalculator which
201 | is an instance of derived :class:`~nodeeditor.node_graphics_edge_path.GraphicsEdgePathBase` class
202 | containing the actual `calcPath()` function - computing how the edge should look like.
203 |
204 | :returns: ``QPainterPath`` of the edge connecting `source` and `destination`
205 | :rtype: ``QPainterPath``
206 | """
207 | return self.pathCalculator.calcPath()
208 |
209 |
--------------------------------------------------------------------------------
/vvs_app/node_graphics_edge_path.py:
--------------------------------------------------------------------------------
1 | import math
2 | from qtpy.QtCore import QPointF
3 | from qtpy.QtGui import QPainterPath
4 |
5 |
6 | EDGE_CP_ROUNDNESS = 100 #: Bezier control point distance on the line
7 | WEIGHT_SOURCE = 0.2 #: factor for square edge to change the midpoint between start and end socket
8 |
9 |
10 | class GraphicsEdgePathBase:
11 | """Base Class for calculating the graphics path to draw for an graphics Edge"""
12 |
13 | def __init__(self, owner: 'QDMGraphicsEdge'):
14 | # keep the reference to owner GraphicsEdge class
15 | self.owner = owner
16 |
17 | def calcPath(self):
18 | """Calculate the Direct line connection
19 |
20 | :returns: ``QPainterPath`` of the graphics path to draw
21 | :rtype: ``QPainterPath`` or ``None``
22 | """
23 | return None
24 |
25 |
26 | class GraphicsEdgePathDirect(GraphicsEdgePathBase):
27 | """Direct line connection Graphics Edge"""
28 | def calcPath(self) -> QPainterPath:
29 | """Calculate the Direct line connection
30 |
31 | :returns: ``QPainterPath`` of the direct line
32 | :rtype: ``QPainterPath``
33 | """
34 | path = QPainterPath(QPointF(self.owner.posSource[0], self.owner.posSource[1]))
35 | path.lineTo(self.owner.posDestination[0], self.owner.posDestination[1])
36 | return path
37 |
38 |
39 | class GraphicsEdgePathBezier(GraphicsEdgePathBase):
40 | """Cubic line connection Graphics Edge"""
41 | def calcPath(self) -> QPainterPath:
42 | """Calculate the cubic Bezier line connection with 2 control points
43 |
44 | :returns: ``QPainterPath`` of the cubic Bezier line
45 | :rtype: ``QPainterPath``
46 | """
47 | s = self.owner.posSource
48 | d = self.owner.posDestination
49 | dist = (d[0] - s[0]) * 0.5
50 |
51 | cpx_s = +dist
52 | cpx_d = -dist
53 | cpy_s = 0
54 | cpy_d = 0
55 |
56 | if self.owner.edge.start_socket is not None:
57 | ssin = self.owner.edge.start_socket.is_input
58 | ssout = self.owner.edge.start_socket.is_output
59 |
60 | if (s[0] > d[0] and ssout) or (s[0] < d[0] and ssin):
61 | cpx_d *= -1
62 | cpx_s *= -1
63 |
64 | cpy_d = (
65 | (s[1] - d[1]) / math.fabs(
66 | (s[1] - d[1]) if (s[1] - d[1]) != 0 else 0.00001
67 | )
68 | ) * EDGE_CP_ROUNDNESS
69 | cpy_s = (
70 | (d[1] - s[1]) / math.fabs(
71 | (d[1] - s[1]) if (d[1] - s[1]) != 0 else 0.00001
72 | )
73 | ) * EDGE_CP_ROUNDNESS
74 |
75 | path = QPainterPath(QPointF(self.owner.posSource[0], self.owner.posSource[1]))
76 | path.cubicTo( s[0] + cpx_s, s[1] + cpy_s, d[0] + cpx_d, d[1] + cpy_d, self.owner.posDestination[0], self.owner.posDestination[1])
77 |
78 | return path
79 |
80 |
81 | class GraphicsEdgePathSquare(GraphicsEdgePathBase):
82 | """Square line connection Graphics Edge"""
83 | def __init__(self, *args, handle_weight=0.5, **kwargs):
84 | super().__init__(*args, **kwargs)
85 | self.rand = None
86 | self.handle_weight = handle_weight
87 |
88 | def calcPath(self):
89 | """Calculate the square edge line connection
90 |
91 | :returns: ``QPainterPath`` of the edge square line
92 | :rtype: ``QPainterPath``
93 | """
94 |
95 | s = self.owner.posSource
96 | d = self.owner.posDestination
97 |
98 | mid_x = s[0] + ((d[0] - s[0]) * self.handle_weight)
99 |
100 | path = QPainterPath(QPointF(s[0], s[1]))
101 | path.lineTo(mid_x, s[1])
102 | path.lineTo(mid_x, d[1])
103 | path.lineTo(d[0], d[1])
104 |
105 | return path
106 |
--------------------------------------------------------------------------------
/vvs_app/node_graphics_node.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | A module containing Graphics representation of :class:`~nodeeditor.node_node.Node`
4 | """
5 | import os
6 |
7 | from PyQt5.QtGui import QIcon, QImage
8 | from qtpy.QtWidgets import QGraphicsItem, QWidget, QGraphicsTextItem, QGraphicsDropShadowEffect
9 | from qtpy.QtGui import QFont, QColor, QPen, QBrush, QPainterPath
10 | from qtpy.QtCore import Qt, QRectF
11 |
12 |
13 | class QDMGraphicsNode(QGraphicsItem):
14 | """Class describing Graphics representation of :class:`~nodeeditor.node_node.Node`"""
15 |
16 | def __init__(self, node: 'Node', parent: QWidget = None, node_icon=''):
17 | """
18 | :param node: reference to :class:`~nodeeditor.node_node.Node`
19 | :type node: :class:`~nodeeditor.node_node.Node`
20 | :param parent: parent widget
21 | :type parent: QWidget
22 |
23 | :Instance Attributes:
24 |
25 | - **node** - reference to :class:`~nodeeditor.node_node.Node`
26 | """
27 | super().__init__(parent)
28 | self.node = node
29 | self.node_icon = QImage(node_icon)
30 | # init our flags
31 | self.hovered = False
32 | self._was_moved = False
33 | self._last_selected_state = False
34 |
35 | self.updateSizes()
36 | self.initAssets()
37 | self.initUI()
38 |
39 | # init title
40 | self.init_name()
41 |
42 | # creating a QGraphicsDropShadowEffect object
43 | # shadow = QGraphicsDropShadowEffect()
44 | # shadow.setColor(QColor(14, 14, 14))
45 | #
46 | # shadow.setXOffset(-6)
47 | # shadow.setYOffset(6)
48 | # # setting blur radius (optional step)
49 | # shadow.setBlurRadius(12)
50 | # # adding shadow to the grNode
51 | # self.setGraphicsEffect(shadow)
52 |
53 | @property
54 | def name(self):
55 | """title of this `Node`
56 |
57 | :getter: current Graphics Node title
58 | :setter: stores and make visible the new title
59 | :type: str
60 | """
61 | return self._name
62 |
63 | @name.setter
64 | def name(self, value):
65 | self._name = value
66 | self.grName = self._name
67 |
68 | self.name_item.setPlainText(self.grName)
69 | self.name_item.adjustSize()
70 |
71 | if self.name_item.textWidth()+self.title_horizontal_padding > self.width:
72 |
73 | while self.name_item.textWidth() + self.title_horizontal_padding > self.width:
74 | self.grName = self.grName[0: -1]
75 | self.name_item.setPlainText(self.grName)
76 | self.name_item.setPlainText(self.grName+"...")
77 | self.name_item.adjustSize()
78 |
79 | self.name_item.adjustSize()
80 |
81 | def initUI(self):
82 | """Set up this ``QGraphicsItem``"""
83 | self.setFlag(QGraphicsItem.ItemIsSelectable)
84 | self.setFlag(QGraphicsItem.ItemIsMovable)
85 | self.setAcceptHoverEvents(True)
86 |
87 | def update_node_theme(self, all: bool=False, text_color: str = ""):
88 | current_theme = self.node.scene.masterRef.global_switches.switches_Dict["Appearance"]["Theme"][0]
89 |
90 | if all:
91 | icon = os.path.split(self.node.icon)[-1]
92 | self.node_icon = QImage(self.node.scene.masterRef.global_switches.get_icon(icon))
93 |
94 | if text_color != "":
95 | self.name_item.setDefaultTextColor(QColor(text_color))
96 |
97 | background_color_index = self.node.scene.masterRef.global_switches.themes_colors["Nodes"].index("Background")
98 | self.node_background_color = QColor(self.node.scene.masterRef.global_switches.themes_colors[current_theme][background_color_index])
99 | self._brush_background = QBrush(self.node_background_color)
100 |
101 | Outline_color_index = self.node.scene.masterRef.global_switches.themes_colors["Nodes"].index("Outline")
102 | self._color = QColor(
103 | self.node.scene.masterRef.global_switches.themes_colors[current_theme][Outline_color_index])
104 | self._pen_default = QPen(self._color)
105 | self._pen_default.setWidthF(1.5)
106 |
107 | def updateSizes(self):
108 | """Set up internal attributes like `width`, `height`, etc."""
109 | self.width = 140
110 | self.height = 80
111 | self.edge_roundnes = 1
112 | self.title_height = 20
113 | self.title_horizontal_padding = self.title_height
114 | self.title_vertical_padding = 8
115 |
116 | def AutoResizeGrNode(self):
117 | socketsHeight = 0
118 | if len(self.node.inputs) > len(self.node.outputs):
119 | maxSockets = self.node.inputs
120 | else:
121 | maxSockets = self.node.outputs
122 |
123 | for socket in maxSockets:
124 | socketsHeight += socket.grSocket.radius*2 + socket.grSocket.radius / 2
125 |
126 | self.height = socketsHeight + self.title_height + 2
127 |
128 | def initAssets(self):
129 | """Initialize ``QObjects`` like ``QColor``, ``QPen`` and ``QBrush``"""
130 | self._title_color = Qt.white
131 | self._title_font = QFont("Roboto", 13)
132 |
133 | current_theme = self.node.scene.masterRef.global_switches.switches_Dict["Appearance"]["Theme"][0]
134 |
135 | Outline_color_index = self.node.scene.masterRef.global_switches.themes_colors["Nodes"].index("Outline")
136 | self._color = QColor(
137 | self.node.scene.masterRef.global_switches.themes_colors[current_theme][Outline_color_index])
138 | self._color_selected = QColor("#FFFFA637")
139 | self._color_hovered = QColor("#FFFFFF")
140 |
141 | self._pen_default = QPen(self._color)
142 | self._pen_default.setWidthF(1.5)
143 | self._pen_selected = QPen(self._color_selected)
144 | self._pen_selected.setWidthF(2.0)
145 | self._pen_hovered = QPen(self._color_hovered)
146 | self._pen_hovered.setWidthF(1)
147 |
148 | self.title_color = QColor("#FF313131")
149 | self._brush_title = QBrush(self.title_color)
150 |
151 | background_color_index = self.node.scene.masterRef.global_switches.themes_colors["Nodes"].index("Background")
152 | self.node_background_color = QColor(
153 | self.node.scene.masterRef.global_switches.themes_colors[current_theme][background_color_index])
154 | self._brush_background = QBrush(self.node_background_color)
155 |
156 | def onSelected(self):
157 | """Our event handling when the node was selected"""
158 | self.node.scene.grScene.itemSelected.emit()
159 |
160 | def doSelect(self, new_state=True):
161 | """Safe version of selecting the `Graphics Node`. Takes care about the selection state flag used internally
162 |
163 | :param new_state: ``True`` to select, ``False`` to deselect
164 | :type new_state: ``bool``
165 | """
166 | self.setSelected(new_state)
167 | self._last_selected_state = new_state
168 | if new_state: self.onSelected()
169 |
170 | def mouseMoveEvent(self, event):
171 | """Overridden event to detect that we moved with this `Node`"""
172 | super().mouseMoveEvent(event)
173 |
174 | # optimize me! just update the selected nodes
175 | for node in self.scene().scene.nodes:
176 | if node.grNode.isSelected():
177 | node.updateConnectedEdges()
178 | self._was_moved = True
179 |
180 | def mouseReleaseEvent(self, event):
181 | """Overriden event to handle when we moved, selected or deselected this `Node`"""
182 | super().mouseReleaseEvent(event)
183 |
184 | # handle when grNode moved
185 | if self._was_moved:
186 | self._was_moved = False
187 | self.node.scene.history.storeHistory("Node moved", setModified=True)
188 |
189 | self.node.scene.resetLastSelectedStates()
190 | self.doSelect() # also trigger itemSelected when node was moved
191 |
192 | # we need to store the last selected state, because moving does also select the nodes
193 | self.node.scene._last_selected_items = self.node.scene.getSelectedItems()
194 |
195 | # now we want to skip storing selection
196 | return
197 |
198 | # handle when grNode was clicked on
199 | if self._last_selected_state != self.isSelected() or self.node.scene._last_selected_items != self.node.scene.getSelectedItems():
200 | self.node.scene.resetLastSelectedStates()
201 | self._last_selected_state = self.isSelected()
202 | self.onSelected()
203 |
204 | def mouseDoubleClickEvent(self, event):
205 | """Overriden event for doubleclick. Resend to `Node::onDoubleClicked`"""
206 | self.node.onDoubleClicked(event)
207 |
208 | def hoverEnterEvent(self, event: 'QGraphicsSceneHoverEvent') -> None:
209 | """Handle hover effect"""
210 | self.hovered = True
211 | self.update()
212 |
213 | def hoverLeaveEvent(self, event: 'QGraphicsSceneHoverEvent') -> None:
214 | """Handle hover effect"""
215 | self.hovered = False
216 | self.update()
217 |
218 | def boundingRect(self) -> QRectF:
219 | """Defining Qt' bounding rectangle"""
220 | return QRectF(
221 | 0,
222 | 0,
223 | self.width,
224 | self.height
225 | ).normalized()
226 |
227 | def set_input_label_text(self, index, text):
228 | if self.node.inputs[index]:
229 | socket = self.node.inputs[index]
230 | socket.socket_label.setPlainText(text)
231 | socket.update_label()
232 | else:
233 | print("Trying to access an input socket_label that doesn't exist")
234 |
235 | def set_output_label_text(self, index, text):
236 | if self.node.outputs[index]:
237 | socket = self.node.outputs[index]
238 | socket.socket_label.setPlainText(text)
239 | socket.update_label()
240 | else:
241 | print("Trying to access an output socket_label that doesn't exist")
242 |
243 | def init_name(self):
244 | """Set up the title Graphics representation: font, color, position, etc."""
245 |
246 | self.name_item = QGraphicsTextItem(self)
247 | self.name_item.setDefaultTextColor(Qt.white)
248 | self.name_item.setFont(self._title_font)
249 | self.name_item.setPos(self.title_horizontal_padding, -3)
250 |
251 | self.name = self.node.name
252 |
253 | def highlight_code(self, raw_code):
254 |
255 | if self.isSelected():
256 | code = f""" {raw_code}
"""
257 | else:
258 | code = f""" {raw_code}
"""
259 | return code
260 |
261 | def paint(self, painter, QStyleOptionGraphicsItem, widget=None):
262 | """Painting the rounded rectanglar `Node`"""
263 |
264 | # content
265 | path_content = QPainterPath()
266 | path_content.setFillRule(Qt.WindingFill)
267 | path_content.addRoundedRect(0, 0, self.width, self.height, self.edge_roundnes, self.edge_roundnes)
268 | path_content.addRect(0, self.title_height, self.edge_roundnes, self.edge_roundnes)
269 | path_content.addRect(self.width - self.edge_roundnes, self.title_height, self.edge_roundnes,
270 | self.edge_roundnes)
271 |
272 | painter.setPen(Qt.NoPen)
273 | painter.setBrush(self._brush_background)
274 | painter.drawPath(path_content.simplified())
275 |
276 | # title
277 | path_title = QPainterPath()
278 | path_title.setFillRule(Qt.WindingFill)
279 | path_title.addRoundedRect(0, 0, self.width, self.title_height, self.edge_roundnes, self.edge_roundnes)
280 | path_title.addRect(0, self.title_height - self.edge_roundnes, self.edge_roundnes, self.edge_roundnes)
281 | path_title.addRect(self.width - self.edge_roundnes, self.title_height - self.edge_roundnes,
282 | self.edge_roundnes, self.edge_roundnes)
283 |
284 | painter.setPen(Qt.NoPen)
285 | painter.setBrush(self._brush_title)
286 | painter.drawPath(path_title.simplified())
287 |
288 | # outline
289 | path_outline = QPainterPath()
290 | path_outline.addRoundedRect(-1, -1, self.width + 2, self.height + 2, self.edge_roundnes, self.edge_roundnes)
291 | painter.setBrush(Qt.NoBrush)
292 |
293 | if self.hovered:
294 | painter.setBrush(QColor("#10FFFFFF"))
295 | painter.setPen(self._pen_hovered)
296 | painter.drawPath(path_outline.simplified())
297 | # painter.setPen(self._pen_default)
298 | painter.drawPath(path_outline.simplified())
299 | else:
300 | painter.setPen(self._pen_default if not self.isSelected() else self._pen_selected)
301 | painter.drawPath(path_outline.simplified())
302 |
303 | painter.drawImage(QRectF(0, 0, self.title_height, self.title_height), self.node_icon)
304 |
305 |
--------------------------------------------------------------------------------
/vvs_app/node_graphics_scene.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | A module containing Graphic representation of :class:`~nodeeditor.node_scene.Scene`
4 | """
5 | import math
6 | from qtpy.QtWidgets import *
7 | from qtpy.QtCore import *
8 | from qtpy.QtGui import *
9 | from utils import dumpException
10 | from graph_graphics import STATE_STRING, DEBUG_STATE
11 |
12 |
13 | class NodeGraphicsScene(QGraphicsScene):
14 | """Class representing Graphic of :class:`~nodeeditor.node_scene.Scene`"""
15 | #: pyqtSignal emitted when some item is selected in the `Scene`
16 | itemSelected = Signal()
17 | #: pyqtSignal emitted when items are deselected in the `Scene`
18 | itemsDeselected = Signal()
19 |
20 | def __init__(self, scene: 'Scene', parent: QWidget = None):
21 | """
22 | :param scene: reference to the :class:`~nodeeditor.node_scene.Scene`
23 | :type scene: :class:`~nodeeditor.node_scene.NodeScene`
24 | :param parent: parent widget
25 | :type parent: QWidget
26 | """
27 | super().__init__(parent)
28 | self.scene = scene
29 |
30 | # There is an issue when reconnecting edges -> mouseMove and trying to delete/remove them
31 | # the edges stayed in the scene in Qt, however python side was deleted
32 | # this caused a lot of troubles...
33 | #
34 | # I've spend months to find this sh*t!!
35 | #
36 | # https://bugreports.qt.io/browse/QTBUG-18021
37 | # https://bugreports.qt.io/browse/QTBUG-50691
38 | # Affected versions: 4.7.1, 4.7.2, 4.8.0, 5.5.1, 5.7.0 - LOL!
39 |
40 | self.setItemIndexMethod(QGraphicsScene.NoIndex)
41 |
42 | # settings
43 | self.gridSize = self.scene.masterRef.global_switches.switches_Dict["Appearance"]["Grid Size"]
44 | self.gridSquares = 5
45 |
46 | self.initAssets()
47 |
48 | def initAssets(self):
49 | """Initialize ``QObjects`` like ``QColor``, ``QPen`` and ``QBrush``"""
50 | self.update_background_color()
51 | self._color_state = QColor("#ccc")
52 |
53 | self._pen_state = QPen(self._color_state)
54 | self._font_state = QFont("Roboto", 16)
55 |
56 | def update_background_color(self, background_color:str="555555", grid_lines_color:str="555555"):
57 | if self.scene.masterRef.global_switches.switches_Dict["Appearance"]["Theme"][0] == "Dark":
58 | background_color = "404040"
59 | grid_lines_color = "292929"
60 | elif self.scene.masterRef.global_switches.switches_Dict["Appearance"]["Theme"][0] == "Light":
61 | background_color = "e0e0e0"
62 | grid_lines_color = "eeeeee"
63 |
64 | self._color_background = QColor(f"#{background_color}")
65 | self._color_light = QColor(f"#{grid_lines_color}")
66 | self._color_dark = QColor(f"#{grid_lines_color}")
67 |
68 | self._pen_light = QPen(self._color_light)
69 | self._pen_dark = QPen(self._color_dark)
70 | self._pen_light.setWidth(1)
71 | self._pen_dark.setWidth(2)
72 |
73 | self.setBackgroundBrush(self._color_background)
74 |
75 | # the drag events won't be allowed until dragMoveEvent is overriden
76 | def dragMoveEvent(self, event):
77 | """Overriden Qt's dragMoveEvent to enable Qt's Drag Events"""
78 | pass
79 |
80 | def setGrScene(self, width: int, height: int):
81 | """Set `width` and `height` of the `Graphics Scene`"""
82 | self.setSceneRect(-width // 2, -height // 2, width, height)
83 |
84 | def drawBackground(self, painter: QPainter, rect: QRect):
85 | """Draw background scene grid"""
86 | super().drawBackground(painter, rect)
87 |
88 | # here we create our grid
89 | left = int(math.floor(rect.left()))
90 | right = int(math.ceil(rect.right()))
91 | top = int(math.floor(rect.top()))
92 | bottom = int(math.ceil(rect.bottom()))
93 |
94 | first_left = left - (left % self.gridSize)
95 | first_top = top - (top % self.gridSize)
96 |
97 | # compute all lines to be drawn
98 | lines_light, lines_dark = [], []
99 | for x in range(first_left, right, self.gridSize):
100 | if (x % (self.gridSize * self.gridSquares) != 0):
101 | lines_light.append(QLine(x, top, x, bottom))
102 | else:
103 | lines_dark.append(QLine(x, top, x, bottom))
104 |
105 | for y in range(first_top, bottom, self.gridSize):
106 | if (y % (self.gridSize * self.gridSquares) != 0):
107 | lines_light.append(QLine(left, y, right, y))
108 | else:
109 | lines_dark.append(QLine(left, y, right, y))
110 |
111 | # draw the lines
112 | painter.setPen(self._pen_light)
113 | try:
114 | painter.drawLines(*lines_light) # supporting PyQt5
115 | except TypeError:
116 | painter.drawLines(lines_light) # supporting PySide2
117 |
118 | painter.setPen(self._pen_dark)
119 | try:
120 | painter.drawLines(*lines_dark) # supporting PyQt5
121 | except TypeError:
122 | painter.drawLines(lines_dark) # supporting PySide2
123 |
124 | if DEBUG_STATE:
125 | try:
126 | painter.setFont(self._font_state)
127 | painter.setPen(self._pen_state)
128 | painter.setRenderHint(QPainter.TextAntialiasing)
129 | offset = 14
130 | rect_state = QRect(rect.x() + offset, rect.y() + offset, rect.width() - 2 * offset,
131 | rect.height() - 2 * offset)
132 | painter.drawText(rect_state, Qt.AlignRight | Qt.AlignTop, STATE_STRING[self.views()[0].mode].upper())
133 | except:
134 | dumpException()
135 |
--------------------------------------------------------------------------------
/vvs_app/node_scene_clipboard.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | A module containing all code for working with Clipboard
4 | """
5 | from collections import OrderedDict
6 | from node_graphics_edge import QDMGraphicsEdge
7 | from node_edge import Edge
8 | from node_graphics_node import QDMGraphicsNode
9 |
10 | DEBUG = False
11 | DEBUG_PASTING = False
12 |
13 |
14 | class SceneClipboard():
15 | """
16 | Class contains all the code for serialization/deserialization from Clipboard
17 | """
18 | def __init__(self, scene: 'Scene'):
19 | """
20 | :param scene: Reference to the :class:`~nodeeditor.node_scene.Scene`
21 | :type scene: :class:`~nodeeditor.node_scene.NodeScene`
22 |
23 | :Instance Attributes:
24 |
25 | - **scene** - reference to the :class:`~nodeeditor.node_scene.Scene`
26 | """
27 | self.scene = scene
28 |
29 | def serializeSelected(self, delete: bool=False) -> OrderedDict:
30 | """
31 | Serializes selected items in the Scene into ``OrderedDict``
32 |
33 | :param delete: True if you want to delete selected items after serialization. Useful for Cut operation
34 | :type delete: ``bool``
35 | :return: Serialized data of current selection in NodeEditor :class:`~nodeeditor.node_scene.Scene`
36 | """
37 | if DEBUG: print("-- COPY TO CLIPBOARD ---")
38 |
39 | sel_nodes, sel_edges, sel_sockets = [], [], {}
40 |
41 | # sort edges and nodes
42 | for item in self.scene.grScene.selectedItems():
43 | if isinstance(item, QDMGraphicsNode):
44 | sel_nodes.append(item.node.serialize())
45 | for socket in (item.node.inputs + item.node.outputs):
46 | sel_sockets[socket.id] = socket
47 | elif isinstance(item, QDMGraphicsEdge):
48 | sel_edges.append(item.edge)
49 |
50 |
51 | # debug
52 | if DEBUG:
53 | print(" NODES\n ", sel_nodes)
54 | print(" EDGES\n ", sel_edges)
55 | print(" SOCKETS\n ", sel_sockets)
56 |
57 |
58 | # remove all edges which are not connected to a nodeeditor in our list
59 | edges_to_remove = []
60 | for edge in sel_edges:
61 | if edge.start_socket.id in sel_sockets and edge.end_socket.id in sel_sockets:
62 | # if DEBUG: print(" edge is ok, connected with both sides")
63 | pass
64 | else:
65 | if DEBUG: print("edge", edge, "is not connected with both sides")
66 | edges_to_remove.append(edge)
67 | for edge in edges_to_remove:
68 | sel_edges.remove(edge)
69 |
70 | # make final list of edges
71 | edges_final = []
72 | for edge in sel_edges:
73 | edges_final.append(edge.serialize())
74 |
75 | if DEBUG: print("our final edge list:", edges_final)
76 |
77 |
78 | data = OrderedDict([
79 | ('nodes', sel_nodes),
80 | ('edges', edges_final),
81 | ])
82 |
83 |
84 | # if CUT (aka delete) remove selected items
85 | if delete:
86 | self.scene.getView().deleteSelected()
87 | # store our history
88 | self.scene.history.storeHistory("Cut out elements from scene", setModified=True)
89 |
90 | return data
91 |
92 | def deserializeFromClipboard(self, data: dict, *args, **kwargs):
93 | """
94 | Deserializes data from Clipboard.
95 |
96 | :param data: ``dict`` data for deserialization to the :class:`nodeeditor.node_scene.Scene`.
97 | :type data: ``dict``
98 | """
99 |
100 | hashmap = {}
101 |
102 | # calculate mouse pointer - scene position
103 | view = self.scene.getView()
104 | mouse_scene_pos = view.last_scene_mouse_position
105 |
106 | # calculate selected objects bbox and center
107 | minx, maxx, miny, maxy = 10000000, -10000000, 10000000, -10000000
108 | for node_data in data['nodes']:
109 | x, y = node_data['pos_x'], node_data['pos_y']
110 | if x < minx: minx = x
111 | if x > maxx: maxx = x
112 | if y < miny: miny = y
113 | if y > maxy: maxy = y
114 |
115 | # add width and height of a node
116 | maxx -= 180
117 | maxy += 100
118 |
119 | relbboxcenterx = (minx + maxx) / 2 - minx
120 | relbboxcentery = (miny + maxy) / 2 - miny
121 |
122 | if DEBUG_PASTING:
123 | print (" *** PASTA:")
124 | print("Copied boudaries:\n\tX:", minx, maxx, " Y:", miny, maxy)
125 | print("\tbbox_center:", relbboxcenterx, relbboxcentery)
126 |
127 | # calculate the offset of the newly creating nodes
128 | mousex, mousey = mouse_scene_pos.x(), mouse_scene_pos.y()
129 |
130 | # create each node
131 | created_nodes = []
132 |
133 | self.scene.setSilentSelectionEvents()
134 |
135 | self.scene.doDeselectItems()
136 |
137 | for node_data in data['nodes']:
138 |
139 | if node_data['node_usage']:
140 | new_node = self.scene.getNodeClassFromData(node_data)(self.scene, node_data['is_setter'], node_data['node_usage'])
141 | else:
142 | new_node = self.scene.getNodeClassFromData(node_data)(self.scene)
143 |
144 | new_node.deserialize(data=node_data, hashmap=hashmap, restore_id=False, *args, **kwargs)
145 | created_nodes.append(new_node)
146 |
147 | # readjust the new nodeeditor's position
148 |
149 | # new node's current position
150 | posx, posy = new_node.pos.x(), new_node.pos.y()
151 | newx, newy = mousex + posx - minx, mousey + posy - miny
152 |
153 | new_node.setPos(newx, newy)
154 |
155 | new_node.doSelect()
156 |
157 | if DEBUG_PASTING:
158 | print("** PASTA SUM:")
159 | print("\tMouse pos:", mousex, mousey)
160 | print("\tnew node pos:", posx, posy)
161 | print("\tFINAL:", newx, newy)
162 |
163 | # create each edge
164 | if 'edges' in data:
165 | for edge_data in data['edges']:
166 | new_edge = Edge(self.scene)
167 | new_edge.deserialize(edge_data, hashmap, restore_id=False, *args, **kwargs)
168 |
169 |
170 | self.scene.setSilentSelectionEvents(False)
171 |
172 | # store history
173 | self.scene.history.storeHistory("Pasted elements in scene", setModified=True)
174 |
175 | self.scene.node_editor.UpdateTextCode()
176 | return created_nodes
--------------------------------------------------------------------------------
/vvs_app/node_scene_history.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | A module containing all code for working with History (Undo/Redo)
4 | """
5 | from utils import dumpException
6 |
7 | DEBUG = False
8 | DEBUG_SELECTION = False
9 |
10 |
11 | class SceneHistory():
12 | """Class contains all the code for undo/redo operations"""
13 | def __init__(self, masterRef, scene: 'Scene'):
14 | """
15 | :param scene: Reference to the :class:`~nodeeditor.node_scene.Scene`
16 | :type scene: :class:`~nodeeditor.node_scene.NodeScene`
17 |
18 | :Instance Attributes:
19 |
20 | - **scene** - reference to the :class:`~nodeeditor.node_scene.Scene`
21 | - **history_limit** - number of history steps that can be stored
22 | """
23 |
24 | self.scene = scene
25 |
26 | self.masterRef = masterRef
27 |
28 | self.clear()
29 | self.history_limit = 32
30 |
31 | self.undo_selection_has_changed = False
32 |
33 | # listeners
34 | self._history_modified_listeners = []
35 | self._history_stored_listeners = []
36 | self._history_restored_listeners = []
37 |
38 | self.Edits_Counter = -1
39 |
40 | def clear(self):
41 | """Reset the history stack"""
42 | self.history_stack = []
43 | self.history_current_step = -1
44 |
45 | def storeInitialHistoryStamp(self):
46 | """Helper function usually used when new or open file requested"""
47 | self.storeHistory("Initial History Stamp")
48 |
49 | def addHistoryModifiedListener(self, callback: 'function'):
50 | """
51 | Register callback for `HistoryModified` event
52 |
53 | :param callback: callback function
54 | """
55 | self._history_modified_listeners.append(callback)
56 |
57 | def addHistoryStoredListener(self, callback: 'function'):
58 | """
59 | Register callback for `HistoryStored` event
60 |
61 | :param callback: callback function
62 | """
63 | self._history_stored_listeners.append(callback)
64 |
65 | def addHistoryRestoredListener(self, callback: 'function'):
66 | """
67 | Register callback for `HistoryRestored` event
68 |
69 | :param callback: callback function
70 | """
71 | self._history_restored_listeners.append(callback)
72 |
73 | def canUndo(self) -> bool:
74 | """Return ``True`` if Undo is available for current `History Stack`
75 |
76 | :rtype: ``bool``
77 | """
78 | return self.history_current_step > 0
79 |
80 | def canRedo(self) -> bool:
81 | """
82 | Return ``True`` if Redo is available for current `History Stack`
83 |
84 | :rtype: ``bool``
85 | """
86 | return self.history_current_step + 1 < len(self.history_stack)
87 |
88 | def undo(self):
89 | """Undo operation"""
90 | if DEBUG: print("UNDO")
91 |
92 | if self.canUndo():
93 | self.history_current_step -= 1
94 | self.restoreHistory()
95 | self.scene.has_been_modified = True
96 |
97 | def redo(self):
98 | """Redo operation"""
99 | if DEBUG: print("REDO")
100 |
101 | if self.canRedo():
102 | self.history_current_step += 1
103 | self.restoreHistory()
104 | self.scene.has_been_modified = True
105 |
106 |
107 | def restoreHistory(self):
108 | """
109 | Restore `History Stamp` from `History stack`.
110 |
111 | Triggers:
112 |
113 | - `History Modified` event
114 | - `History Restored` event
115 | """
116 | if DEBUG: print("Restoring history",
117 | ".... current_step: @%d" % self.history_current_step,
118 | "(%d)" % len(self.history_stack))
119 | self.restoreHistoryStamp(self.history_stack[self.history_current_step])
120 | for callback in self._history_modified_listeners: callback()
121 | for callback in self._history_restored_listeners: callback()
122 |
123 | self.scene.node_editor.UpdateTextCode()
124 |
125 | def storeHistory(self, desc: str, setModified: bool=False):
126 | """
127 | Store History Stamp into History Stack
128 |
129 | :param desc: Description of current History Stamp
130 | :type desc: ``str``
131 | :param setModified: if ``True`` marks :class:`~nodeeditor.node_scene.Scene` with `has_been_modified`
132 | :type setModified: ``bool``
133 |
134 | Triggers:
135 |
136 | - `History Modified`
137 | - `History Stored`
138 | """
139 | if setModified:
140 | self.scene.has_been_modified = True
141 |
142 | if DEBUG: print("Storing history", '"%s"' % desc,
143 | ".... current_step: @%d" % self.history_current_step,
144 | "(%d)" % len(self.history_stack))
145 |
146 | # if the pointer (history_current_step) is not at the end of history_stack
147 | if self.history_current_step+1 < len(self.history_stack):
148 | self.history_stack = self.history_stack[0:self.history_current_step+1]
149 |
150 | # history is outside of the limits
151 | if self.history_current_step+1 >= self.history_limit:
152 | self.history_stack = self.history_stack[1:]
153 | self.history_current_step -= 1
154 |
155 | hs = self.createHistoryStamp(desc)
156 | self.history_stack.append(hs)
157 | self.history_current_step += 1
158 | if DEBUG: print(" -- setting step to:", self.history_current_step)
159 |
160 | # always trigger history modified (for i.e. updateEditMenu)
161 | for callback in self._history_modified_listeners: callback()
162 | for callback in self._history_stored_listeners: callback()
163 |
164 | self.onAutoSave()
165 |
166 | def onAutoSave(self):
167 | if self.Edits_Counter == -1:
168 | self.Edits_Counter = 0
169 | else:
170 | self.Edits_Counter += 1
171 | if self.Edits_Counter >= self.scene.masterRef.global_switches.switches_Dict["System"]["AutoSave Steps"]:
172 | self.masterRef.onFileAutoSave()
173 | self.Edits_Counter = 0
174 | self.scene.masterRef.global_switches.save_settings_to_file()
175 |
176 | def captureCurrentSelection(self) -> dict:
177 | """
178 | Create dictionary with a list of selected nodes and a list of selected edges
179 | :return: ``dict`` 'nodes' - list of selected nodes, 'edges' - list of selected edges
180 | :rtype: ``dict``
181 | """
182 | sel_obj = {
183 | 'nodes': [],
184 | 'edges': [],
185 | }
186 | for item in self.scene.grScene.selectedItems():
187 | if hasattr(item, 'node'): sel_obj['nodes'].append(item.node.id)
188 | elif hasattr(item, 'edge'): sel_obj['edges'].append(item.edge.id)
189 | return sel_obj
190 |
191 | def createHistoryStamp(self, desc: str) -> dict:
192 | """
193 | Create History Stamp. Internally serialize whole scene and the current selection
194 |
195 | :param desc: Descriptive label for the History Stamp
196 | :return: History stamp serializing state of `Scene` and current selection
197 | :rtype: ``dict``
198 | """
199 | history_stamp = {
200 | 'desc': desc,
201 | 'snapshot': self.scene.serialize(),
202 | 'selection': self.captureCurrentSelection(),
203 | }
204 |
205 | return history_stamp
206 |
207 | def restoreHistoryStamp(self, history_stamp: dict):
208 | """
209 | Restore History Stamp to current `Scene` with selection of items included
210 |
211 | :param history_stamp: History Stamp to restore
212 | :type history_stamp: ``dict``
213 | """
214 | if DEBUG: print("RHS: ", history_stamp['desc'])
215 |
216 | try:
217 | self.undo_selection_has_changed = False
218 | previous_selection = self.captureCurrentSelection()
219 | if DEBUG_SELECTION: print("selected nodes before restore:", previous_selection['nodes'])
220 |
221 | self.scene.deserialize(data=history_stamp['snapshot'])
222 |
223 | # restore selection
224 |
225 | # first clear all selection on edges
226 | for edge in self.scene.edges: edge.grEdge.setSelected(False)
227 | # now restore selected edges from history_stamp
228 | for edge_id in history_stamp['selection']['edges']:
229 | for edge in self.scene.edges:
230 | if edge.id == edge_id:
231 | edge.grEdge.setSelected(True)
232 | break
233 |
234 | # first clear all selection on nodes
235 | for node in self.scene.nodes: node.grNode.setSelected(False)
236 | # now restore selected nodes from history_stamp
237 | for node_type in history_stamp['selection']['nodes']:
238 | for node in self.scene.nodes:
239 | if node.id == node_type:
240 | node.grNode.setSelected(True)
241 | break
242 |
243 | current_selection = self.captureCurrentSelection()
244 | if DEBUG_SELECTION: print("selected nodes after restore:", current_selection['nodes'])
245 |
246 | # reset the last_selected_items - since we're comparing change to the last_selected state
247 | self.scene._last_selected_items = self.scene.getSelectedItems()
248 |
249 | # if the selection of nodes differ before and after restoration, set flag
250 | if current_selection['nodes'] != previous_selection['nodes'] or current_selection['edges'] != previous_selection['edges']:
251 | if DEBUG_SELECTION: print("\nSCENE: Selection has changed")
252 | self.undo_selection_has_changed = True
253 |
254 | except Exception as e: dumpException(e)
--------------------------------------------------------------------------------
/vvs_app/node_serializable.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | A module containing Serializable "Interface". We pretend its an abstract class
4 | """
5 | from collections import OrderedDict
6 |
7 |
8 | class Serializable():
9 | def __init__(self):
10 | """
11 | Default constructor automatically creates data which are common to any serializable object.
12 | In our case we create ``self.id`` which we use in every object in NodeEditor.
13 | """
14 | self.id = id(self)
15 |
16 | def serialize(self) -> OrderedDict:
17 | """
18 | Serialization method to serialize this class data into ``OrderedDict`` which can be easily stored
19 | in memory or file.
20 |
21 | :return: data serialized in ``OrderedDict``
22 | :rtype: ``OrderedDict``
23 | """
24 | raise NotImplemented()
25 |
26 | def deserialize(self, data: dict, hashmap: dict={}, restore_id: bool=True) -> bool:
27 | """
28 | Deserialization method which take data in python ``dict`` format with helping `hashmap` containing
29 | references to existing entities.
30 |
31 | :param data: Dictionary containing serialized data
32 | :type data: ``dict``
33 | :param hashmap: Helper dictionary containing references (by id == key) to existing objects
34 | :type hashmap: ``dict``
35 | :param restore_id: True if we are creating new Sockets. False is useful when loading existing
36 | Sockets of which we want to keep the existing object's `id`.
37 | :type restore_id: bool
38 | :return: ``True`` if deserialization was successful, otherwise ``False``
39 | :rtype: ``bool``
40 | """
41 | raise NotImplemented()
42 |
--------------------------------------------------------------------------------
/vvs_app/nodes/__init__.py:
--------------------------------------------------------------------------------
1 | # __all__ = [ "operations", "input", "output" ]
2 |
3 | from os.path import dirname, basename, isfile, join
4 | import glob
5 |
6 | modules = glob.glob(join(dirname(__file__), "*.py"))
7 | __all__ = [basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]
8 |
--------------------------------------------------------------------------------
/vvs_app/nodes/nodes_configuration.py:
--------------------------------------------------------------------------------
1 | LISTBOX_MIMETYPE = "application/x-item"
2 |
3 |
4 | # Logic = {}
5 | # Math = {}
6 | # Process = {}
7 |
8 | FUNCTIONS = {}
9 | MATH_OPERATORS = {}
10 | LOGIC_OPERATORS = {}
11 | NUMPY = {}
12 |
13 | ######################
14 |
15 | VARIABLES = {}
16 | EVENTS = {}
17 |
18 | class ConfException(Exception):
19 | pass
20 |
21 |
22 | class InvalidNodeRegistration(ConfException):
23 | pass
24 |
25 |
26 | class NodeTypeNotRegistered(ConfException):
27 | pass
28 |
29 |
30 | ######################
31 |
32 |
33 | def register_Node(Node_Class):
34 | Node_Type = len({**FUNCTIONS, **VARIABLES, **EVENTS})
35 | Node_Class.node_type = Node_Type
36 |
37 | if Node_Class.category == "FUNCTION":
38 | FUNCTIONS[Node_Type] = Node_Class
39 | elif Node_Class.category == "User_Function":
40 | EVENTS[Node_Type] = Node_Class
41 | elif Node_Class.category == "VARIABLE":
42 | VARIABLES[Node_Type] = Node_Class
43 |
44 |
45 | ######################
46 |
47 | def get_class_by_type(node_type):
48 | NODES = {**FUNCTIONS, **VARIABLES, **EVENTS}
49 | if node_type not in NODES:
50 | raise NodeTypeNotRegistered("node_type '%d' is not registered" % node_type)
51 | else:
52 | return NODES[node_type]
53 |
54 | ######################
55 |
56 |
57 | ##############################################################################################################
58 | ##############################################################################################################
59 |
60 | # User Variables setup
61 |
62 |
63 | ######################
64 |
65 | # User Events setup
66 |
67 |
68 | ##############################################################################################################
69 | ##############################################################################################################
70 |
71 |
72 | # This comment was originally here before it was removed for better init performance and moved
73 | # import all nodes and register them
74 | # from nodes import *
75 |
--------------------------------------------------------------------------------
/vvs_app/nodes/user_functions_nodes.py:
--------------------------------------------------------------------------------
1 | from nodes.default_functions import Indent, FontFamily, FontSize
2 | from nodes.nodes_configuration import *
3 | from master_node import MasterNode
4 | from node_editor_widget import *
5 |
6 |
7 | class UserFunction(MasterNode):
8 | icon = "event.png"
9 | name = "user_function"
10 | category = "User_Function"
11 | sub_category = "User_Function"
12 | node_usage = 'function'
13 |
14 | def __init__(self, scene, isSetter, node_usage='function'):
15 | if not self.node_usage: self.node_usage = node_usage
16 | if isSetter:
17 | super().__init__(scene, inputs=[], outputs=[0])
18 | self.getNodeCode = self.write_function
19 | else:
20 | super().__init__(scene, inputs=[0], outputs=[0])
21 | self.getNodeCode = self.call_function
22 |
23 | self.is_setter = isSetter
24 |
25 | def write_function(self):
26 | childCode = self.get_other_socket_code(0)
27 | raw_code = "Empty"
28 | L_P = "{"
29 | R_P = "}"
30 |
31 | if self.syntax == "Python":
32 | python_code = f"""
33 | def {self.name}(){self.get_return()}:
34 | {Indent(childCode)}"""
35 | raw_code = python_code
36 |
37 | elif self.syntax == "C++":
38 | CPP_code = f"""
39 | {self.get_return()} {self.name}()
40 | {L_P}
41 | {Indent(childCode)}
42 | {R_P}"""
43 | raw_code = CPP_code
44 |
45 | elif self.syntax == "Rust":
46 | rust_code = f"""
47 | fn {self.name}(){self.get_return()} {L_P}
48 | {Indent(childCode)}
49 | {R_P}"""
50 | raw_code = rust_code
51 | return self.grNode.highlight_code(raw_code)
52 |
53 | def call_function(self):
54 | self.showCode = not self.isInputConnected(0)
55 | brotherCode = self.get_other_socket_code(0)
56 | raw_code = "Empty"
57 |
58 | if self.syntax == "Python":
59 | python_code = f"""
60 | {self.name}()
61 | {brotherCode}"""
62 | raw_code = python_code
63 |
64 | elif self.syntax == "C++":
65 | cpp_code = f"""
66 | {self.name}();
67 | {brotherCode}"""
68 | raw_code = cpp_code
69 |
70 | elif self.syntax == "Rust":
71 | rust_code = f"""
72 | {self.name}();
73 | {brotherCode}"""
74 | raw_code = rust_code
75 |
76 | return self.grNode.highlight_code(raw_code)
77 |
--------------------------------------------------------------------------------
/vvs_app/nodes/variables_nodes.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtGui import QBrush, QColor
2 |
3 | from nodes.default_functions import FontSize, FontFamily, MakeList
4 | from nodes.nodes_configuration import *
5 | from master_node import MasterNode
6 |
7 |
8 | Numpy_Vars = {'float': "'f'",
9 | 'integer': "'i'",
10 | 'boolean': "'?'",
11 | 'string': "'S'"}
12 |
13 | Rust_Vars = {'float': "f32",
14 | 'integer': "i32",
15 | 'boolean': "bool",
16 | 'string': "&str"}
17 |
18 | class UserVar(MasterNode):
19 | icon = ""
20 | name = "user_variable"
21 | category = "VARIABLE"
22 | sub_category = "VARIABLE"
23 |
24 | def __init__(self, scene, isSetter, node_usage=None):
25 | if not self.node_usage: self.node_usage = node_usage
26 | if isSetter:
27 | super().__init__(scene, inputs=[0, self.node_usage], outputs=[0, self.node_usage])
28 | self.getNodeCode = self.get_setter_code
29 | else:
30 | super().__init__(scene, inputs=[], outputs=[self.node_usage])
31 | self.getNodeCode = self.get_getter_code
32 |
33 | self.is_setter = isSetter
34 |
35 | def get_setter_code(self):
36 | self.outputs[1].socket_code = self.name
37 | self.showCode = not self.isInputConnected(0)
38 | brother_code = self.get_other_socket_code(0)
39 | input_1_code = self.get_my_input_code(1)
40 | other_node = self.getConnectedInputNode(1)
41 |
42 | raw_code = "Empty"
43 | L_P = "{"
44 | R_P = "}"
45 |
46 | if self.node_usage == 'string':
47 | input_1_code = f'"{input_1_code}"'
48 |
49 |
50 | if self.syntax == "Python":
51 | if self.node_structure == 'single value':
52 | python_code = f"""
53 | {self.name} = {input_1_code}
54 | {brother_code}"""
55 | raw_code = python_code
56 |
57 | # Python Array Code
58 | elif self.node_structure == 'array':
59 | if other_node == None or isinstance(other_node, MakeList):
60 | python_code = f"""
61 | {self.name} = numpy.array([{input_1_code}], {Numpy_Vars[self.node_usage]})
62 | {brother_code}"""
63 |
64 | elif isinstance(other_node, UserVar):
65 | python_code = f"""
66 | {self.name} = {other_node.name}
67 | {brother_code}"""
68 | raw_code = python_code
69 |
70 | elif self.syntax == "C++":
71 | if self.node_structure == 'single value':
72 | CPP_code = f"""
73 | {self.scene.node_editor.return_types[self.node_usage][self.scene.node_editor.return_types["Languages"].index(self.syntax)]} {self.name} = {input_1_code};
74 | {brother_code}"""
75 | raw_code = CPP_code
76 |
77 | # C++ Array Code
78 | elif self.node_structure == 'array':
79 | if other_node == None or isinstance(other_node, MakeList):
80 | CPP_code = f"""
81 | list <{self.node_usage}> {self.name}({L_P}{input_1_code}{R_P});
82 | {brother_code}"""
83 | elif isinstance(other_node, UserVar):
84 | CPP_code = f"""
85 | list <{self.node_usage}> {self.name} = {other_node.name};
86 | {brother_code}"""
87 | raw_code = CPP_code
88 |
89 | # Rust Array Code
90 | elif self.syntax == "Rust":
91 | if self.node_structure == 'single value':
92 | rust_code = f"""
93 | let {self.name} = {input_1_code};
94 | {brother_code}"""
95 | raw_code = rust_code
96 |
97 | elif self.node_structure == 'array':
98 | if other_node == None or isinstance(other_node, MakeList):
99 | rust_code = f"""
100 | let {self.name}: Vec<{Rust_Vars[self.node_usage]}> = vec![{input_1_code}];
101 | {brother_code}"""
102 |
103 | elif isinstance(other_node, UserVar):
104 | rust_code = f"""
105 | let {self.name}: Vec<{Rust_Vars[self.node_usage]}> = {other_node.name}
106 | {brother_code}"""
107 | raw_code = rust_code
108 |
109 | return self.grNode.highlight_code(raw_code)
110 |
111 | def get_getter_code(self):
112 | self.showCode = False
113 | getCode = self.outputs[0].socket_code = self.name
114 | return getCode
115 |
--------------------------------------------------------------------------------
/vvs_app/qss/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/qss/__init__.py
--------------------------------------------------------------------------------
/vvs_app/qss/light_theme_colors.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sheriff99yt/Vision_Visual_Scripting/4ca42db765208c09755b73d24a9c07553e9e9bbd/vvs_app/qss/light_theme_colors.png
--------------------------------------------------------------------------------
/vvs_app/qss/nodeeditor-dark.qss:
--------------------------------------------------------------------------------
1 | QFrame,QDialog,QMainWindow{background:#474747}QSplitter,QMainWindow::separator{background:#474747}QStatusBar{background:#474747;color:#ccc}QTabWidget{border:0}QTabBar{background:#474747;color:#ccc}QMdiArea QTabBar,QMdiArea QTabWidget,QMdiArea QTabWidget::pane,QMdiArea QTabWidget::tab-bar,QMdiArea QTabBar::tab{height:17px}QMdiArea QTabBar::tab:top:!selected,QMdiArea QTabBar::tab:top:selected,QMdiArea QTabBar::tab:top:!selected:hover{border-top-left-radius:4px;border-top-right-radius:4px;padding:2px 8px;padding-top:0;padding-bottom:3px;min-width:8ex;border:1px solid #333;border-bottom:0}QMdiArea QTabBar::tab:top:!selected,QMdiArea QTabBar::tab:top:!selected:hover{background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #6d6d6d,stop : .1 #474747,stop : .89 #3f3f3f,stop : 1 #3f3f3f)}QMdiArea QTabBar::tab:top:selected{background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #878787,stop : .1 #545454,stop : .89 #474747,stop : 1 #474747)}QMdiArea QTabBar::tab:top:!selected:hover{background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #727272,stop : .1 #4c4c4c,stop : .89 #444,stop : 1 #444)}QMdiArea QTabBar QToolButton{background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #878787,stop : .1 #616161,stop : .89 #4f4f4f,stop : 1 #4f4f4f);border:1px solid #333;border-radius:0}QMdiArea QTabBar QToolButton::left-arrow{image:url(":vvs_app/icons/small_arrow_left-light.png")}QMdiArea QTabBar QToolButton::right-arrow{image:url(":vvs_app/icons/small_arrow_right-light.png")}QMdiArea QTabBar::close-button:selected{image:url(":vvs_app/icons/tab_close_btn.png");origin:border;subcontrol-origin:border;subcontrol-position:right bottom}QMdiArea QTabBar::close-button:!selected{image:url(":vvs_app/icons/tab_close_nonselected_btn.png")}QMdiSubWindow{border-size:1px;border-style:solid;background:#616161}QTabBar::tab:selected,QTabBar::tab:hover{color:#eee}QDockWidget{color:#ddd;font-weight:bold;titlebar-close-icon:url(":vvs_app/icons/docktitle-close-btn-light.png");titlebar-normal-icon:url(":vvs_app/icons/docktitle-normal-btn-light.png")}QDockWidget::title{background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #3b3b3b,stop : 1 #2e2e2e);padding-top:4px;padding-right:22px;font-weight:bold}QDockWidget::close-button,QDockWidget::float-button{subcontrol-position:top right;subcontrol-origin:margin;text-align:center;icon-size:16px;width:14px;position:absolute;top:0;bottom:0;left:0;right:4px}QDockWidget::close-button{right:4px}QDockWidget::float-button{right:18px}QMenuBar{background:#474747}QMenuBar::item{spacing:3px;padding:3px 5px;color:#eee;background:transparent}QMenuBar::item:selected,QMenuBar::item:pressed{background:#4f9eee}QMenu{background:#474747;border:1px solid #2e2e2e}QMenu::item{background:#474747;color:#eee}QMenu::item:selected{background:#616161}QMenu::active{background:#616161;color:#eee}QMenu::separator{height:1px;background:#2e2e2e}QMenu::disabled,QMenu::item:disabled{color:#6e6e6e}QListView{background-color:#555;alternate-background-color:#434343}QListView::item{height:22px;color:#e6e6e6}QListView::item:hover{background:#6e6e6e}QListView::item::active:hover{color:#fff}QListView::item:selected,QListView::item::active:selected{color:#fff;background:qlineargradient(x1 : 0,y1 : 0,x2 : 0,y2 : 1,stop : 0 #4f9eee,stop : 1 #2084ea);border:0}QPushButton{color:#e6e6e6;background:#555;border-color:#141414}QLabel{color:#e6e6e6}QLineEdit,QTextEdit{color:#e6e6e6;background:#5a5a5a}QLineEdit{border:1px solid #3a3a3a;border-radius:2px;padding:1px 2px}QDMNodeContentWidget{background:transparent;}QDMNodeContentWidget QFrame{background:transparent}QDMNodeContentWidget QTextEdit{background:#666}QDMNodeContentWidget QLabel{color:#e0e0e0}QGraphicsView{selection-background-color:#fff}
--------------------------------------------------------------------------------
/vvs_app/qss/nodeeditor-light.qss:
--------------------------------------------------------------------------------
1 |
2 | QMdiArea QTabBar QToolButton,QPushButton:pressed, QToolButton:pressed, QComboBox:hover, QComboBox:focus, QToolBar, QHeaderView::section, QHeaderView, QDialog, QMainWindow, QSplitter, QStatusBar, QMainWindow::separator,QMenuBar ,QListView::item:selected,QListView::item::active:selected{
3 | background-color:#FFFFFF}
4 | QMdiArea QTabBar::tab:top:selected,QPushButton:hover, QToolButton:hover,QPushButton:disabled, QToolButton:disabled,QFrame,QTabBar,QStatusBar,QMenu::active,QMenu::item:selected,QMenuBar::item:selected,QMenuBar::item:pressed,QLineEdit,QTextEdit,QListView {
5 | background-color:#E0E0E0;}
6 | QDockWidget::title,QPushButton, QToolButton:checked,QComboBox::drop-down,QComboBox QAbstractItemView,QComboBox,QPushButton,QToolButton,QMenu,QMenu::item,QMenuBar::item{
7 | background-color: #FFFCAF;}
8 | QMdiArea QTabBar::tab:top:!selected:hover,QListView::item:hover{
9 | background-color: #828282;}
10 | QMdiSubWindow{
11 | background-color: #FFFFFF;}
12 | QLabel{
13 | background-color: transparent}
14 |
15 |
16 |
17 |
18 | QMdiArea QTabBar::tab:top:!selected,QMdiArea QTabBar::tab:top:!selected:hover,QMdiArea QTabBar::tab:top:selected,QMdiArea QTabBar::tab:top:!selected:hover,QMdiArea QTabBar QToolButton,QDockWidget,QDockWidget::title,QListView::item:selected,QListView::item::active:selected,QListView::item,QListView::item::active:hover,QMenuBar::item,QPushButton:disabled, QToolButton:disabled,QPushButton, QToolButton,QListView::item:selected,QListView::item::active:selected,QLineEdit,QTextEdit,QLabel,QMdiArea QTabBar::tab:top:!selected,QMdiArea QTabBar::tab:top:!selected:hover,QHeaderView,QTreeView,QTabBar,QStatusBar,QComboBox,QMenu::active,QMenu::item {
19 | color: #282828;}
20 | QMenu::separator,QMenu::disabled,QMenu::item:disabled{
21 | color:#282828;}
22 |
23 |
24 |
25 |
26 | QGraphicsView{
27 | selection-background-color:#FFFFFF}
28 | QComboBox {
29 | selection-background-color: #E0E0E0;}
30 |
31 |
32 |
33 |
34 | QHeaderView::section,QComboBox{
35 | background-style: solid;}
36 |
37 |
38 |
39 |
40 | QMdiArea QTabBar QToolButton,QPushButton, QToolButton:checked,QPushButton, QToolButton,QPushButton:disabled,QLineEdit{
41 | border-radius:1px;}
42 |
43 |
44 |
45 |
46 | QMdiArea QTabBar QToolButton,QPushButton, QToolButton:checked,QPushButton:hover, QToolButton:hover,QPushButton:pressed, QToolButton:pressed,QPushButton, QToolButton,QMdiSubWindow,QListView::item:selected,QListView::item::active:selected,QLineEdit,QHeaderView::section,QTabWidget,QComboBox{
47 | border: 0px;}
48 |
49 | QMenuBar::item,QLineEdit,QMenuBar::item,QToolBar,QPushButton, QToolButton,QPushButton:disabled, QToolButton:disabled {
50 | padding: 6px;
51 | spacing: 6px;}
52 |
53 |
54 |
55 |
56 | QPushButton:disabled, QToolButton:disabled,QMenu,QComboBox:hover, QComboBox:focus {
57 | border: 4px solid #E0E0E0;}
58 |
59 |
60 |
61 | QComboBox::drop-down{
62 | subcontrol-origin: padding;
63 | subcontrol-position: top right;
64 | width: 0px;
65 | border-left-width: 0px;
66 | border-left-style: solid;}
67 |
68 |
69 | QPushButton, QToolButton,QPushButton:disabled, QToolButton:disabled {
70 | text-align: center;
71 | }
72 |
73 |
74 |
75 | QMdiArea QTabBar::tab:top:!selected,QMdiArea QTabBar::tab:top:selected,QMdiArea QTabBar::tab:top:!selected:hover{
76 | border-top-left-radius:0px;
77 | border-top-right-radius:0px;
78 | padding:0px 0px;padding-top:0;
79 | padding-bottom:0px;min-width:8ex;
80 | border:1px solid #E0E0E0;
81 | border-bottom:0}
82 |
83 | QMdiArea QTabBar QToolButton::left-arrow{image:url(":vvs_app/icons/light/arrow_left.png")}
84 | QMdiArea QTabBar QToolButton::right-arrow{image:url(":vvs_app/icons/light/arrow_right.png")}
85 | QMdiArea QTabBar::close-button:selected{image:url("vvs_app/icons/light/sub.png");origin:border;subcontrol-origin:border;subcontrol-position:right bottom}
86 | QMdiArea QTabBar::close-button:!selected{image:url("vvs_app/icons/light/sub.png")}
87 | QMdiArea QTabBar::close-button{image:url("vvs_app/icons/light/sub.png")}
88 | QMdiArea QTabBar::close-button:hover{image:url("vvs_app/icons/light/close.png")}
89 |
90 |
91 | QDockWidget{font-weight:normal;titlebar-close-icon:url(":vvs_app/icons/light/close.png");titlebar-normal-icon:url(":vvs_app/icons/light/sub.png")}
92 | QDockWidget::title{padding-top:6px;padding-left:6px;}
93 |
94 |
95 |
96 |
97 | QTextEdit{background-color:#FFFFFF}
98 |
99 |
100 |
101 |
102 | QWidget {font-family: "Calibri"; font-size: 16px}
103 |
--------------------------------------------------------------------------------
/vvs_app/qss/nodeeditor-night.qss:
--------------------------------------------------------------------------------
1 |
2 | QTextEdit,QMdiArea QTabBar QToolButton,QPushButton:pressed, QToolButton:pressed, QComboBox:hover, QComboBox:focus, QToolBar, QHeaderView::section, QHeaderView, QDialog, QMainWindow, QSplitter, QStatusBar, QMainWindow::separator,QMenuBar ,QListView::item:selected,QListView::item::active:selected{
3 | background-color:#282828}
4 | QMdiArea QTabBar::tab:top:selected,QPushButton:hover, QToolButton:hover,QPushButton:disabled, QToolButton:disabled,QFrame,QTabBar,QStatusBar,QMenu::active,QMenu::item:selected,QMenuBar::item:selected,QMenuBar::item:pressed,QLineEdit,QListView {
5 | background-color:#404040}
6 | QDockWidget::title,QPushButton, QToolButton:checked,QComboBox::drop-down,QComboBox QAbstractItemView,QComboBox,QPushButton,QToolButton,QMenu,QMenu::item,QMenuBar::item{
7 | background-color: #1F1F1F;}
8 | QMdiArea QTabBar::tab:top:!selected:hover,QListView::item:hover{
9 | background-color: #828282;}
10 | QMdiSubWindow{
11 | background-color: #FFFFFF;}
12 | QLabel{
13 | background-color: transparent}
14 |
15 |
16 |
17 |
18 | QMdiArea QTabBar::tab:top:!selected,QMdiArea QTabBar::tab:top:!selected:hover,QMdiArea QTabBar::tab:top:selected,QMdiArea QTabBar::tab:top:!selected:hover,QMdiArea QTabBar QToolButton,QDockWidget,QDockWidget::title,QListView::item:selected,QListView::item::active:selected,QListView::item,QListView::item::active:hover,QMenuBar::item,QPushButton:disabled, QToolButton:disabled,QPushButton, QToolButton,QListView::item:selected,QListView::item::active:selected,QLineEdit,QTextEdit,QTextEdit,QLabel,QMdiArea QTabBar::tab:top:!selected,QMdiArea QTabBar::tab:top:!selected:hover,QHeaderView,QTreeView,QTabBar,QStatusBar,QComboBox,QMenu::active,QMenu::item {
19 | color: #FFFFFF;}
20 | QMenu::separator,QMenu::disabled,QMenu::item:disabled{
21 | color:#828282;}
22 |
23 |
24 |
25 |
26 | QGraphicsView{
27 | selection-background-color:#FFFFFF}
28 | QComboBox {
29 | selection-background-color: #404040;}
30 |
31 |
32 |
33 |
34 | QHeaderView::section,QComboBox{
35 | background-style: solid;}
36 |
37 |
38 |
39 |
40 | QMdiArea QTabBar QToolButton,QPushButton, QToolButton:checked,QPushButton, QToolButton,QPushButton:disabled,QLineEdit{
41 | border-radius:1px;}
42 |
43 |
44 |
45 |
46 | QMdiArea QTabBar QToolButton,QPushButton, QToolButton:checked,QPushButton:hover, QToolButton:hover,QPushButton:pressed, QToolButton:pressed,QPushButton, QToolButton,QMdiSubWindow,QListView::item:selected,QListView::item::active:selected,QLineEdit,QHeaderView::section,QTabWidget,QComboBox{
47 | border: 0px;}
48 |
49 | QMenuBar::item,QLineEdit,QMenuBar::item,QToolBar,QPushButton, QToolButton,QPushButton:disabled, QToolButton:disabled {
50 | padding: 6px;
51 | spacing: 6px;}
52 |
53 |
54 |
55 |
56 | QPushButton:disabled, QToolButton:disabled,QMenu,QComboBox:hover, QComboBox:focus {
57 | border: 4px solid #1F1F1F;}
58 |
59 |
60 |
61 | QComboBox::drop-down{
62 | subcontrol-origin: padding;
63 | subcontrol-position: top right;
64 | width: 0px;
65 | border-left-width: 0px;
66 | border-left-style: solid;}
67 |
68 |
69 | QPushButton, QToolButton,QPushButton:disabled, QToolButton:disabled {
70 | text-align: center;
71 | }
72 |
73 |
74 |
75 | QMdiArea QTabBar::tab:top:!selected,QMdiArea QTabBar::tab:top:selected,QMdiArea QTabBar::tab:top:!selected:hover{
76 | border-top-left-radius:0px;
77 | border-top-right-radius:0px;
78 | padding:0px 0px;padding-top:0;
79 | padding-bottom:0px;min-width:8ex;
80 | border:1px solid #282828;
81 | border-bottom:0}
82 |
83 | QMdiArea QTabBar QToolButton::left-arrow{image:url(":vvs_app/icons/Dark/arrow_left.png")}
84 | QMdiArea QTabBar QToolButton::right-arrow{image:url(":vvs_app/icons/Dark/arrow_right.png")}
85 | QMdiArea QTabBar::close-button:selected{image:url("vvs_app/icons/Dark/sub.png");origin:border;subcontrol-origin:border;subcontrol-position:right bottom}
86 | QMdiArea QTabBar::close-button:!selected{image:url("vvs_app/icons/Dark/sub.png")}
87 | QMdiArea QTabBar::close-button{image:url("vvs_app/icons/Dark/sub.png")}
88 | QMdiArea QTabBar::close-button:hover{image:url("vvs_app/icons/Dark/close.png")}
89 |
90 |
91 | QDockWidget{font-weight:normal;titlebar-close-icon:url(":vvs_app/icons/Dark/close.png");titlebar-normal-icon:url(":vvs_app/icons/Dark/sub.png")}
92 | QDockWidget::title{padding-top:6px;padding-left:6px;}
93 |
94 |
95 |
96 |
97 | QTextEdit{background-color:#282828}
98 |
99 |
100 |
101 |
102 | QWidget {font-family: "Calibri"; font-size: 16px}
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/vvs_app/qss/nodeeditor.qss:
--------------------------------------------------------------------------------
1 | QDMNodeContentWidget {
2 | background: transparent;
3 | }
4 | QDMNodeContentWidget QFrame {
5 | background: transparent;
6 | }
7 | QDMNodeContentWidget QTextEdit,
8 | QDMNodeContentWidget QLineEdit {
9 | background: #666;
10 | color: #fff;
11 | }
12 | QDMNodeContentWidget QLabel {
13 | color: #e0e0e0;
14 | }
15 | QDMNodeContentWidget QLabel#calc_node_bg,
16 | QDMNodeContentWidget QLabel#calc_node_mul,
17 | QDMNodeContentWidget QLabel#calc_node_div {
18 | background: transparent;
19 | height: 0px;
20 | color: #373737;
21 | font-size: 72px;
22 | max-height: 49px;
23 | min-height: 49px;
24 | padding-left: 94px;
25 | }
26 | QDMNodeContentWidget QLabel#calc_node_mul,
27 | QDMNodeContentWidget QLabel#calc_node_div {
28 | padding-top: 12px;
29 | }
30 | QDMNodeContentWidget QLabel#calc_node_output {
31 | min-width: 150px;
32 | max-width: 150px;
33 | min-height: 45px;
34 | max-height: 45px;
35 | margin-left: 10px;
36 | margin-top: 5px;
37 | font-size: 28px;
38 | }
39 | QDMNodeContentWidget QLineEdit#calc_node_input {
40 | width: 140px;
41 | height: 36px;
42 | margin-top: 5px;
43 | margin-left: 5px;
44 | font-size: 28px;
45 | }
46 | QGraphicsView {
47 | selection-background-color: #fff;
48 | }
49 |
--------------------------------------------------------------------------------
/vvs_app/qss/nodeeditor.styl:
--------------------------------------------------------------------------------
1 | QDMNodeContentWidget
2 | background: transparent
3 | QFrame
4 | background: transparent
5 |
6 | QTextEdit, QLineEdit
7 | background: #666
8 | color: #fff
9 |
10 | QLabel
11 | color: #e0e0e0
12 |
13 | QLabel#calc_node_bg, QLabel#calc_node_mul, QLabel#calc_node_div
14 | background: transparent
15 | height: 0px
16 | color: #373737
17 | font-size: 72px
18 | max-height: 49px
19 | min-height: 49px
20 | padding-left: 94px
21 |
22 | QLabel#calc_node_mul, QLabel#calc_node_div
23 | padding-top: 12px
24 |
25 | QLabel#calc_node_output
26 | min-width: 150px
27 | max-width: 150px
28 | min-height: 45px
29 | max-height: 45px
30 | margin-left: 10px
31 | margin-top: 5px
32 | font-size: 28px
33 |
34 | QLineEdit#calc_node_input
35 | width: 140px
36 | height: 36px
37 | margin-top: 5px
38 | margin-left: 5px
39 | font-size: 28px
40 |
41 |
42 | QGraphicsView
43 | selection-background-color: rgb(255, 255, 255)
--------------------------------------------------------------------------------
/vvs_app/qss/nodestyle.qss:
--------------------------------------------------------------------------------
1 | QDMNodeContentWidget { background: transparent; }
2 | QDMNodeContentWidget QLabel { color: #e0e0e0; }
3 | QGraphicsView { selection-background-color: rgb(255, 255, 255); }
--------------------------------------------------------------------------------
/vvs_app/utils.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | """
3 | Module with some helper functions
4 | """
5 | import traceback
6 | from qtpy.QtCore import QFile
7 | from qtpy.QtWidgets import QApplication
8 | from pprint import PrettyPrinter
9 |
10 | pp = PrettyPrinter(indent=4).pprint
11 |
12 |
13 | def dumpException(e=None):
14 | """
15 | Prints out an Exception message with a traceback to the console
16 |
17 | :param e: Exception to print out
18 | :type e: Exception
19 | """
20 | # print("%s EXCEPTION:" % e.__class__.__name__, e)
21 | # traceback.print_tb(e.__traceback__)
22 | traceback.print_exc()
23 |
24 |
25 | def loadStylesheet(filename: str):
26 | """
27 | Loads an qss stylesheet to the current QApplication instance
28 |
29 | :param filename: Filename of qss stylesheet
30 | :type filename: str
31 | """
32 | print('STYLE loading:', filename)
33 | file = QFile(filename)
34 | file.open(QFile.ReadOnly | QFile.Text)
35 | stylesheet = file.readAll()
36 | QApplication.instance().setStyleSheet(str(stylesheet, encoding='utf-8'))
37 |
38 | def loadStylesheets(*args):
39 | """
40 | Loads multiple qss stylesheets. Concatenates them together and applies the final stylesheet to the current QApplication instance
41 |
42 | :param args: variable number of filenames of qss stylesheets
43 | :type args: str, str,...
44 | """
45 | res = ''
46 | for arg in args:
47 | file = QFile(arg)
48 | file.open(QFile.ReadOnly | QFile.Text)
49 | stylesheet = file.readAll()
50 | res += "\n" + str(stylesheet, encoding='utf-8')
51 | QApplication.instance().setStyleSheet(res)
52 |
--------------------------------------------------------------------------------