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