├── .gitignore ├── LICENSE ├── README.rst ├── docs ├── Makefile ├── api.rst ├── conf.py ├── guide.rst ├── index.rst └── make.bat ├── mvp ├── __init__.py ├── config.py ├── extensions.py ├── hooks.py ├── integration.py ├── presets.py ├── renderglobals.py ├── renderlayers.py ├── resources │ ├── __init__.py │ ├── dots-vertical.png │ ├── half.png │ ├── icon.png │ ├── resolution.png │ └── style.css ├── ui │ ├── __init__.py │ ├── forms.py │ └── ui.py ├── utils.py ├── vendor │ ├── Qt.py │ ├── __init__.py │ └── psforms │ │ ├── Qt.py │ │ ├── __init__.py │ │ ├── controls.py │ │ ├── exc.py │ │ ├── fields.py │ │ ├── form.py │ │ ├── resource.py │ │ ├── style.css │ │ ├── utils.py │ │ ├── validators.py │ │ └── widgets.py └── viewport.py └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | 57 | venv/ 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dan Bradham 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 | 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/mvp.svg 2 | :target: https://testpypi.python.org/pypi/mvp/ 3 | :alt: Latest Version 4 | 5 | ======================= 6 | MVP - Maya Viewport API 7 | ======================= 8 | 9 | I really needed this... 10 | 11 | This module exists to unify and pythonify the various commands and apis necessary to manipulate Maya's 3D Viewports. These include, hardwareRenderGlobal attributes, modelPanel and modelEditor commands, as well as some key features of OpenMayaUI's M3dView class. 12 | 13 | :: 14 | 15 | from mvp import Viewport 16 | 17 | view = Viewport.active() 18 | view.camera = 'top' 19 | view.background = 0.5, 0.5, 0.5 20 | view.nurbsCurves = False 21 | 22 | 23 | Features 24 | ======== 25 | 26 | * Unified api for manipulating Maya Viewports 27 | 28 | * Get or set viewport state (all attributes). Making it easy to restore a Viewport to a previous configuration. 29 | 30 | * Easily set focus and playblast Viewports. Much more consistent than using active view. 31 | 32 | * Draw text in Viewports using QLabels. 33 | 34 | * Show identifiers in viewports, making it easy to grab the correct viewport at a glance. 35 | 36 | 37 | Get MVP 38 | ======= 39 | 40 | PyPi 41 | ---- 42 | MVP is available through the python package index as **mvp**. 43 | 44 | :: 45 | 46 | pip install mvp 47 | 48 | Distutils/Setuptools 49 | -------------------- 50 | 51 | :: 52 | 53 | git clone git@github.com/danbradham/mvp.git 54 | cd mvp 55 | python setup.py install 56 | 57 | 58 | Documentation 59 | ============= 60 | 61 | For more information visit the `docs `_. 62 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Shout.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Shout.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Shout" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Shout" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | .. currentmodule:: mvp 3 | 4 | API Documentation 5 | ================= 6 | 7 | Viewport 8 | -------- 9 | 10 | .. autoclass:: mvp.Viewport 11 | :members: 12 | 13 | RenderGlobals 14 | ------------- 15 | 16 | .. autoclass:: mvp.RenderGlobals 17 | :members: 18 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import mock 5 | 6 | MOCK_MODULES = ['maya', 'maya.cmds', 'maya.OpenMaya', 'maya.OpenMayaUI', 7 | 'maya.utils', 'pymel', 'pymel.core', 'PySide', 'shiboken'] 8 | sys.modules.update((mod_name, mock.Mock()) for mod_name in MOCK_MODULES) 9 | 10 | import os 11 | sys.path.insert(0, os.path.abspath('..')) 12 | import mvp 13 | 14 | extensions = [ 15 | 'sphinx.ext.intersphinx', 16 | 'sphinx.ext.autodoc', 17 | ] 18 | 19 | source_suffix = '.rst' 20 | master_doc = 'index' 21 | project = mvp.__title__ 22 | copyright = u'2015, {0}'.format(mvp.__author__) 23 | version = mvp.__version__ 24 | release = mvp.__version__ 25 | pygments_style = 'sphinx' 26 | intersphinx_mapping = {'http://docs.python.org/': None} 27 | -------------------------------------------------------------------------------- /docs/guide.rst: -------------------------------------------------------------------------------- 1 | .. _guide: 2 | .. currentmodule:: mvp 3 | 4 | ===== 5 | Guide 6 | ===== 7 | This section will grow shortly. 8 | 9 | Getting the Active Viewport 10 | --------------------------- 11 | 12 | Setting the Active Viewport 13 | --------------------------- 14 | 15 | Identifying Viewports 16 | ===================== 17 | 18 | Getting an Inactive Viewport 19 | ============================ 20 | 21 | Setting focus 22 | ============= 23 | 24 | Manipulating Viewports 25 | ---------------------- 26 | 27 | Setting the camera and background 28 | ================================= 29 | 30 | Toggling visibility of node types 31 | ================================= 32 | 33 | Copy a Viewport 34 | --------------- 35 | 36 | Changing RenderGlobals 37 | ---------------------- 38 | 39 | Ambient Occlusion 40 | ================= 41 | 42 | Antialiasing 43 | ============ 44 | 45 | Motion Blur 46 | =========== 47 | 48 | Depth of Field 49 | ============== 50 | 51 | Playblasting 52 | ------------ 53 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://readthedocs.org/projects/mvp/badge/?version=latest 2 | :target: https://readthedocs.org/projects/mvp/?badge=latest 3 | :alt: Documentation Status 4 | 5 | .. image:: https://pypip.in/version/mvp/badge.svg 6 | :target: https://testpypi.python.org/pypi/mvp/ 7 | :alt: Latest Version 8 | 9 | ======================= 10 | MVP - Maya Viewport API 11 | ======================= 12 | 13 | I really needed this... 14 | 15 | This module exists to unify and pythonify the various commands and apis necessary to manipulate Maya's 3D Viewports. These include, hardwareRenderGlobal attributes, modelPanel and modelEditor commands, as well as some key features of OpenMayaUI's M3dView class. 16 | 17 | :: 18 | 19 | from mvp import Viewport 20 | 21 | view = Viewport.active() 22 | view.camera = 'top' 23 | view.background = 0.5, 0.5, 0.5 24 | view.nurbsCurves = False 25 | 26 | 27 | 28 | Features 29 | ======== 30 | 31 | * Unified api for manipulating Maya Viewports 32 | 33 | * Get or set every viewport attribute all at once. Making it easy to restore a Viewport to a previous state. 34 | 35 | * Easily set focus and playblast Viewports. Much more consistent than using active view. 36 | 37 | * Draw text in Viewports using QLabels. 38 | 39 | * Show identifiers in viewports, making it easy to grab the correct viewport at a glance. 40 | 41 | 42 | Get MVP 43 | ======= 44 | 45 | PyPi 46 | ---- 47 | MVP is available through the python package index as **mvp**. 48 | 49 | :: 50 | 51 | pip install mvp 52 | 53 | Distutils/Setuptools 54 | -------------------- 55 | 56 | :: 57 | 58 | git clone git@github.com/danbradham/mvp.git 59 | cd mvp 60 | python setup.py install 61 | 62 | 63 | Table of Contents 64 | ================= 65 | 66 | .. toctree:: 67 | :maxdepth: 2 68 | 69 | guide 70 | api 71 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Shout.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Shout.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /mvp/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __title__ = 'mvp' 4 | __author__ = 'Dan Bradham' 5 | __email__ = 'danielbradham@gmail.com' 6 | __url__ = 'http://github.com/danbradham/mvp.git' 7 | __version__ = '0.9.3' 8 | __license__ = 'MIT' 9 | __description__ = 'Manipulate Maya 3D Viewports.' 10 | 11 | from .viewport import Viewport, playblast 12 | from .renderglobals import RenderGlobals 13 | from . import config, utils, presets, hooks, resources 14 | from .ui import show 15 | 16 | # Initialize and configure mvp 17 | config.init() 18 | hooks.init() 19 | -------------------------------------------------------------------------------- /mvp/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Configure mvp presets and hooks 4 | ''' 5 | 6 | import os 7 | import sys 8 | 9 | 10 | USER_PRESETS_PATH = os.path.expanduser('~/.mvp') 11 | PRESETS_PATH = [USER_PRESETS_PATH] 12 | 13 | 14 | def init(): 15 | for path in os.environ.get('MVP_PRESETS', '').split(os.pathsep): 16 | if path: 17 | PRESETS_PATH.insert(0, path) 18 | 19 | for path in PRESETS_PATH: 20 | 21 | if not os.path.exists(path): 22 | os.makedirs(path) 23 | 24 | if path not in sys.path: 25 | sys.path.insert(1, path) 26 | -------------------------------------------------------------------------------- /mvp/extensions.py: -------------------------------------------------------------------------------- 1 | import os 2 | from . import hooks 3 | from .viewport import playblast 4 | 5 | 6 | def default_handler(data, options): 7 | 8 | _, ext = os.path.splitext(data['filename']) 9 | is_qt = ext == '.mov' 10 | 11 | kwargs = dict( 12 | camera=data['camera'], 13 | state=data['state'], 14 | width=data['width'], 15 | height=data['height'], 16 | ) 17 | 18 | if is_qt: 19 | kwargs.update( 20 | format='qt', 21 | compression='H.264', 22 | filename=data['filename'], 23 | sound=data['sound'], 24 | ) 25 | else: 26 | # PNG settings 27 | # No sound 28 | # .png extension removed 29 | kwargs.update( 30 | format='image', 31 | compression='png', 32 | filename=data['filename'].rsplit('.', 1)[0], 33 | ) 34 | 35 | return playblast(**kwargs) 36 | 37 | 38 | hooks.register_extension( 39 | name='h.264', 40 | ext='.mov', 41 | handler=default_handler, 42 | ) 43 | 44 | hooks.register_extension( 45 | name='png', 46 | ext='.png', 47 | handler=default_handler, 48 | ) 49 | -------------------------------------------------------------------------------- /mvp/hooks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import glob 5 | from collections import OrderedDict, namedtuple 6 | from . import config 7 | 8 | 9 | postrender = OrderedDict() 10 | pathgen = OrderedDict() 11 | integration = OrderedDict() 12 | extension = OrderedDict() 13 | 14 | PathGenerator = namedtuple('PathGenerator', ['name', 'handler']) 15 | PostRender = namedtuple('PostRender', ['name', 'handler', 'default']) 16 | Extension = namedtuple('Extension', ['name', 'ext', 'handler', 'options']) 17 | 18 | 19 | def register_postrender(name, handler, default=None): 20 | '''Add postrender function to registry 21 | 22 | :param name: Name of postrender function (used as label in ui) 23 | :param handler: Postrender function 24 | ''' 25 | 26 | postrender[name] = PostRender(name, handler, default) 27 | 28 | 29 | def unregister_postrender(name): 30 | '''Remove postrender function from registry''' 31 | 32 | postrender.pop(name, None) 33 | 34 | 35 | def register_path(name, handler): 36 | '''Add a path generator function to registry 37 | 38 | :param name: Name of path generator (used as label in ui) 39 | :param handler: Path generator function 40 | ''' 41 | 42 | pathgen[name] = PathGenerator(name, handler) 43 | 44 | 45 | def unregister_path(name): 46 | '''Remove path generator function from registry 47 | 48 | :param name: Name of path generator function 49 | ''' 50 | 51 | pathgen.pop(name, None) 52 | 53 | 54 | def register_integration(name, obj): 55 | '''Add a path generator function to registry 56 | 57 | :param name: Name of Integration 58 | :param obj: Integration obj 59 | ''' 60 | 61 | integration[name] = obj 62 | 63 | 64 | def unregister_integration(name): 65 | '''Remove path generator function from registry 66 | 67 | :param name: Name of integration to remove 68 | ''' 69 | 70 | integration.pop(name, None) 71 | 72 | 73 | def register_extension(name, ext, handler, options=None): 74 | '''Register a function to handle playblasting a specific file extension 75 | 76 | :param name: Nice name of the extension 77 | :param ext: File extension 78 | :param handler: Handler function that performs the playblasting 79 | ''' 80 | 81 | extension[name] = Extension(name, ext, handler, options) 82 | 83 | 84 | def unregister_extension(name): 85 | '''Register a function to handle playblasting a specific file extension 86 | 87 | :param name: Nice name of the extension to unregister 88 | ''' 89 | 90 | extension.pop(name, None) 91 | 92 | 93 | def init(): 94 | '''Discover presets''' 95 | 96 | postrender.clear() 97 | pathgen.clear() 98 | integration.clear() 99 | extension.clear() 100 | 101 | # Find registered presets in MVP_PRESETS path 102 | for path in config.PRESETS_PATH: 103 | 104 | # Import all python files on MVP_PRESETS path 105 | for f in glob.glob(os.path.join(path, '*.py')): 106 | base = os.path.basename(f) 107 | name = os.path.splitext(base)[0] 108 | __import__(name) 109 | 110 | # Import all packages on MVP_PRESETS path 111 | for f in glob.glob(os.path.join(path, '*', '__init__.py')): 112 | name = os.path.basename(os.path.dirname(f)) 113 | __import__(name) 114 | 115 | # Import builtin extensions 116 | from . import extensions 117 | -------------------------------------------------------------------------------- /mvp/integration.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class Integration(object): 5 | 6 | name = None 7 | description = None 8 | icon = None 9 | banner = None 10 | requires_confirmation = False 11 | enabled_by_default = False 12 | columns = 1 13 | 14 | def __init__(self): 15 | self.set_enabled(self.enabled_by_default) 16 | 17 | def fields(self): 18 | '''Return a list of fields. 19 | 20 | Example: 21 | return [ 22 | { 23 | 'name': 'StringField', 24 | 'type': 'str', 25 | 'default': None, 26 | 'options': [...], 27 | 'required': False, 28 | }, 29 | ... 30 | ] 31 | ''' 32 | return NotImplemented 33 | 34 | def on_filename_changed(self, form, value): 35 | return NotImplemented 36 | 37 | def set_enabled(self, value): 38 | '''Returns True if the integration was successfully enabled''' 39 | 40 | if value: 41 | return self._on_enable() 42 | else: 43 | return self._on_disable() 44 | 45 | def _on_enable(self): 46 | self.enabled = self.on_enable() 47 | return self.enabled 48 | 49 | def on_enable(self): 50 | '''Return True to enable integration and False to disable''' 51 | 52 | return True 53 | 54 | def _on_disable(self): 55 | self.enabled = not self.on_disable() 56 | return self.enabled 57 | 58 | def on_disable(self): 59 | '''Return True to disable integration and False to enable''' 60 | 61 | return True 62 | 63 | def before_playblast(self, form, data): 64 | '''Runs before playblasting.''' 65 | 66 | return NotImplemented 67 | 68 | def after_playblast(self, form, data): 69 | '''Runs after playblasting.''' 70 | 71 | return NotImplemented 72 | 73 | def finalize(self, form, data): 74 | '''Runs after entire playblast process is finished. 75 | 76 | Unlike after_playblast, this method will only run ONCE after all 77 | playblasting is finished. So, when playblasting multiple render layers 78 | you can use this to execute after all of those render layers have 79 | completed rendering. 80 | 81 | Arguments: 82 | form: The Form object including render options 83 | data: List of renders that were output 84 | ''' 85 | 86 | return NotImplemented 87 | -------------------------------------------------------------------------------- /mvp/presets.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import glob 5 | import os 6 | from . import config 7 | 8 | 9 | def get_presets(): 10 | '''Get a generator yielding preset name, data pairs''' 11 | 12 | for path in config.PRESETS_PATH: 13 | for f in glob.glob(os.path.join(path, '*.json')): 14 | 15 | base = os.path.basename(f) 16 | name = os.path.splitext(base)[0] 17 | 18 | with open(f, 'r') as f: 19 | data = json.loads(f.read()) 20 | 21 | yield name, data 22 | 23 | 24 | def get_preset(name): 25 | '''Get a preset by name''' 26 | 27 | for n, s in get_presets(): 28 | if name == n: 29 | return s 30 | 31 | 32 | def find_preset(name): 33 | '''Find the path to a given preset...''' 34 | 35 | for path in config.PRESETS_PATH: 36 | prospect = os.path.join(path, name + '.json') 37 | if os.path.isfile(prospect): 38 | return prospect 39 | 40 | raise ValueError('Could not find a preset named %s', name) 41 | 42 | 43 | def new_preset(name, data): 44 | '''Create a new preset from viewport state data 45 | 46 | :param name: Name of the preset 47 | :param data: Viewport state dict 48 | 49 | usage:: 50 | 51 | import mvp 52 | active = mvp.Viewport.active() 53 | mvp.new_preset('NewPreset1', active.get_state()) 54 | ''' 55 | 56 | preset_path = os.path.join(config.PRESETS_PATH[0], name + '.json') 57 | with open(preset_path, 'w') as f: 58 | f.write(json.dumps(data)) 59 | 60 | 61 | def del_preset(name): 62 | 63 | preset_path = find_preset(name) 64 | if os.path.exists(preset_path): 65 | os.remove(preset_path) 66 | -------------------------------------------------------------------------------- /mvp/renderglobals.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import maya.cmds as cmds 4 | 5 | RENDER_GLOBALS = [ 6 | 'multiSampleEnable', 'multiSampleCount', 'colorBakeResolution', 7 | 'bumpBakeResolution', 'motionBlurEnable', 'motionBlurSampleCount', 8 | 'ssaoEnable', 'ssaoAmount', 'ssaoRadius', 'ssaoFilterRadius', 9 | 'ssaoSamples' 10 | ] 11 | 12 | 13 | class RenderGlobals(object): 14 | '''Get and set hardwareRenderingGlobals attributes:: 15 | 16 | RenderGlobals.multiSampleEnable = True 17 | RenderGlobals.ssaoEnable = True 18 | ''' 19 | 20 | def __getattr__(self, name): 21 | return cmds.getAttr('hardwareRenderingGlobals.' + name) 22 | 23 | def __setattr__(self, name, value): 24 | cmds.setAttr('hardwareRenderingGlobals.' + name, value) 25 | 26 | @property 27 | def properties(self): 28 | '''A list of valid render global attributes''' 29 | return RENDER_GLOBALS 30 | 31 | def get_state(self): 32 | '''Collect hardwareRenderingGlobals attributes that effect 33 | Viewports.''' 34 | 35 | active_state = {} 36 | for attr in RENDER_GLOBALS: 37 | active_state[attr] = getattr(self, attr) 38 | 39 | return active_state 40 | 41 | def set_state(self, state): 42 | '''Set a bunch of hardwareRenderingGlobals all at once. 43 | 44 | :param state: Dict containing attr, value pairs''' 45 | 46 | for k, v in state.items(): 47 | setattr(self, k, v) 48 | 49 | 50 | RenderGlobals = RenderGlobals() 51 | -------------------------------------------------------------------------------- /mvp/renderlayers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from contextlib import contextmanager 3 | 4 | import maya.app.renderSetup.model.renderSetup as renderSetup 5 | from maya import cmds 6 | 7 | 8 | @contextmanager 9 | def enabled_render_layers(): 10 | old_layer = cmds.editRenderLayerGlobals( 11 | query=True, 12 | currentRenderLayer=True, 13 | ) 14 | try: 15 | rs = renderSetup.instance() 16 | 17 | def switchToLayer(layer): 18 | def _switch(): 19 | rs.switchToLayer(layer) 20 | return _switch 21 | 22 | enabled_layers = [] 23 | for layer in rs.getRenderLayers() + [rs.getDefaultRenderLayer()]: 24 | 25 | layer.switchToLayer = switchToLayer(layer) 26 | if layer.isRenderable(): 27 | enabled_layers.append(layer) 28 | 29 | yield enabled_layers 30 | finally: 31 | cmds.editRenderLayerGlobals(currentRenderLayer=old_layer) 32 | -------------------------------------------------------------------------------- /mvp/resources/__init__.py: -------------------------------------------------------------------------------- 1 | from os.path import abspath, dirname, join 2 | from ..vendor.Qt import QtCore, QtGui 3 | 4 | 5 | res_package = dirname(__file__) 6 | 7 | 8 | def get_path(*parts): 9 | return abspath(join(res_package, *parts)).replace('\\', '/') 10 | 11 | 12 | def get_qicon(resource, size=None): 13 | if size: 14 | pixmap = QtGui.QPixmap(get_path(resource)) 15 | icon = QtGui.QIcon(pixmap.scaled( 16 | size, 17 | QtCore.Qt.IgnoreAspectRatio, 18 | QtCore.Qt.SmoothTransformation, 19 | )) 20 | else: 21 | icon = QtGui.QIcon(get_path(resource)) 22 | return icon 23 | 24 | 25 | def get_style(): 26 | with open(get_path('style.css'), 'r') as f: 27 | style = f.read() 28 | return style 29 | -------------------------------------------------------------------------------- /mvp/resources/dots-vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danbradham/mvp/7471af9964ff789897792b23d59c597055d566f5/mvp/resources/dots-vertical.png -------------------------------------------------------------------------------- /mvp/resources/half.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danbradham/mvp/7471af9964ff789897792b23d59c597055d566f5/mvp/resources/half.png -------------------------------------------------------------------------------- /mvp/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danbradham/mvp/7471af9964ff789897792b23d59c597055d566f5/mvp/resources/icon.png -------------------------------------------------------------------------------- /mvp/resources/resolution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danbradham/mvp/7471af9964ff789897792b23d59c597055d566f5/mvp/resources/resolution.png -------------------------------------------------------------------------------- /mvp/resources/style.css: -------------------------------------------------------------------------------- 1 | QDialog, 2 | QWidget[form='true'] { 3 | border: 0; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | QWidget[header='true'] { 9 | border: 0; 10 | margin: 0; 11 | padding: 0; 12 | background: rgb(45, 45, 45); 13 | } 14 | 15 | QLabel[title='true'] { 16 | font: 14pt; 17 | } 18 | 19 | QLabel[description='true'] { 20 | font: 10pt; 21 | } 22 | 23 | QLabel{ 24 | font: 10pt; 25 | } 26 | 27 | QLabel[valid='false']{ 28 | color: rgb(255, 50, 50); 29 | } 30 | 31 | QLabel[err='true']{ 32 | font: 10pt; 33 | color: rgb(255, 50, 50); 34 | } 35 | 36 | QLabel[clickable='true']{ 37 | padding-bottom: 4; 38 | } 39 | 40 | QLabel[clickable='true']:hover{ 41 | color: #CCC; 42 | } 43 | 44 | QPushButton{ 45 | height: 24px; 46 | padding: 0 10 0 10; 47 | } 48 | QPushButton::menu-indicator{width:0px;} 49 | 50 | QPushButton[grouptitle='true']{ 51 | background: rgb(68, 68, 68); 52 | border: 0; 53 | border-top: 1px solid rgb(55, 55, 55); 54 | border-bottom: 1px solid rgb(55, 55, 55); 55 | margin: 0; 56 | padding: 0; 57 | padding-left: 20px; 58 | text-align: left; 59 | } 60 | 61 | QPushButton[grouptitle='true']:hover{ 62 | background: rgb(78, 78, 78); 63 | border-radius: 0; 64 | } 65 | 66 | QPushButton[grouptitle='true']:pressed{} 67 | 68 | QWidget[groupwidget='true']{ 69 | background: rgb(55, 55, 55); 70 | } 71 | 72 | QComboBox { 73 | padding-left: 13; 74 | height: 24px; 75 | } 76 | 77 | QLineEdit{ 78 | height: 24px; 79 | padding-left: 10; 80 | } 81 | 82 | 83 | QSpinBox, 84 | QDoubleSpinBox { 85 | background: #2B2B2B; 86 | border: 1px solid #505050; 87 | border-radius: 3; 88 | padding-left: 10; 89 | height: 26px; 90 | } 91 | 92 | QSpinBox:focus, 93 | QDoubleSpinBox:focus { 94 | background: #2B2B2B; 95 | border: 2px solid #507893; 96 | padding-left: 10; 97 | } 98 | 99 | QSpinBox[valid='false'], 100 | QDoubleSpinBox[valid='false'] { 101 | border: 1px solid rgb(203, 40, 40); 102 | } 103 | 104 | 105 | QSpinBox[valid='false']:focus, 106 | QDoubleSpinBox[valid='false']:focus { 107 | border: 1px solid rgb(203, 40, 40); 108 | } 109 | 110 | QSpinBox::up-button, 111 | QDoubleSpinBox::up-button { 112 | background: rgb(255, 255, 255, 0); 113 | border-bottom: 5px solid rgb(185,185,185); 114 | border-left: 5px solid rgb(255, 255, 255, 0); 115 | border-right: 5px solid rgb(255, 255, 255, 0); 116 | border-top: 0px solid rgb(255, 255, 255, 0); 117 | margin-bottom: 4px; 118 | margin-left: 2px; 119 | margin-right: 4px; 120 | margin-top: -2px; 121 | subcontrol-origin: border; 122 | subcontrol-position: top right; 123 | } 124 | 125 | QSpinBox::down-button, 126 | QDoubleSpinBox::down-button { 127 | background: rgb(255, 255, 255, 0); 128 | border-bottom: 0px solid rgb(255, 255, 255, 0); 129 | border-left: 5px solid rgb(255, 255, 255, 0); 130 | border-right: 5px solid rgb(255, 255, 255, 0); 131 | border-top: 5px solid rgb(185,185,185); 132 | margin-bottom: -2px; 133 | margin-left: 2px; 134 | margin-right: 4px; 135 | margin-top: 4px; 136 | subcontrol-origin: border; 137 | subcontrol-position: bottom right; 138 | } 139 | 140 | QSpinBox::up-button:hover, 141 | QDoubleSpinBox::up-button:hover { 142 | border-bottom: 5px solid rgb(235,235,235); 143 | border-left: 5px solid rgb(255, 255, 255, 0); 144 | border-right: 5px solid rgb(255, 255, 255, 0); 145 | border-top: 0px solid rgb(255, 255, 255, 0); 146 | } 147 | 148 | QSpinBox::down-button:hover, 149 | QDoubleSpinBox::down-button:hover { 150 | border-bottom: 0px solid rgb(255, 255, 255, 0); 151 | border-left: 5px solid rgb(255, 255, 255, 0); 152 | border-right: 5px solid rgb(255, 255, 255, 0); 153 | border-top: 5px solid rgb(235,235,235); 154 | } 155 | 156 | QSpinBox::up-button:pressed, 157 | QDoubleSpinBox::up-button:pressed { 158 | border-bottom: 5px solid #FFF; 159 | border-left: 5px solid rgb(255, 255, 255, 0); 160 | border-right: 5px solid rgb(255, 255, 255, 0); 161 | border-top: 0px solid rgb(255, 255, 255, 0); 162 | margin-bottom: 3px; 163 | } 164 | 165 | QSpinBox::down-button:pressed, 166 | QDoubleSpinBox::down-button:pressed { 167 | margin-top: 3px; 168 | border-bottom: 0px solid rgb(255, 255, 255, 0); 169 | border-left: 5px solid rgb(255, 255, 255, 0); 170 | border-right: 5px solid rgb(255, 255, 255, 0); 171 | border-top: 5px solid #FFF; 172 | } 173 | 174 | QSpinBox::up-button:disabled, 175 | QDoubleSpinBox::up-button:disabled { 176 | background: rgb(55, 55, 55); 177 | } 178 | 179 | QSpinBox::down-button:disabled, 180 | QDoubleSpinBox::down-button:disabled { 181 | background: rgb(55, 55, 55); 182 | } 183 | -------------------------------------------------------------------------------- /mvp/ui/__init__.py: -------------------------------------------------------------------------------- 1 | from .ui import * 2 | -------------------------------------------------------------------------------- /mvp/ui/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Third party imports 4 | from .. import resources 5 | from ..vendor.psforms import * 6 | from ..vendor.psforms.exc import ValidationError 7 | from ..vendor.psforms.validators import required 8 | from ..vendor.psforms.fields import * 9 | 10 | 11 | def check_resolution(value): 12 | if (value[0] % 2) != 0 or (value[1] % 2) != 0: 13 | raise ValidationError('Resolution must use even numbers.') 14 | return True 15 | 16 | 17 | class PostRenderForm(Form): 18 | 19 | meta = FormMetaData( 20 | title='Post Render', 21 | description='Post Render Callbacks', 22 | header=False, 23 | labels_on_top=False, 24 | columns=2, 25 | ) 26 | 27 | 28 | class PlayblastForm(Form): 29 | 30 | meta = FormMetaData( 31 | header=False, 32 | icon=resources.get_path('icon.png'), 33 | title='Review', 34 | subforms_as_groups=True, 35 | labels_on_top=False 36 | ) 37 | 38 | filename = SaveFileField( 39 | 'Filename', 40 | validators=(required,), 41 | ) 42 | preset = StringOptionField( 43 | 'Viewport Preset', 44 | options=['Current Settings'], 45 | ) 46 | capture_mode = StringOptionField( 47 | 'Capture Mode', 48 | options=['sequence', 'snapshot'], 49 | ) 50 | render_layers = StringOptionField( 51 | 'Render Layers', 52 | options=['current', 'all enabled'], 53 | ) 54 | camera = StringOptionField( 55 | 'Camera', 56 | ) 57 | resolution = Int2Field( 58 | 'Resolution', 59 | range1=(0, 8192), 60 | range2=(0, 8192), 61 | default=(960, 540), 62 | validators=(check_resolution,), 63 | ) 64 | 65 | postrender = PostRenderForm() 66 | 67 | 68 | class NewPresetForm(Form): 69 | 70 | meta = FormMetaData( 71 | title='New Preset', 72 | description='Create a new preset from the selected panel.', 73 | header=False, 74 | ) 75 | 76 | message = InfoField( 77 | 'Message', 78 | text='Create a new preset from the selected panel.', 79 | labeled=False, 80 | ) 81 | panel = StringOptionField('Panel') 82 | name = StringField('Preset Name', validators=(required,)) 83 | 84 | 85 | class DelPresetForm(Form): 86 | 87 | meta = FormMetaData( 88 | title='Delete Preset', 89 | description='Delete the active preset.', 90 | header=False, 91 | ) 92 | 93 | message = InfoField( 94 | 'Message', 95 | text='Are you sure you want to delete this preset?', 96 | labeled=False, 97 | ) 98 | -------------------------------------------------------------------------------- /mvp/ui/ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import json 5 | from functools import partial 6 | 7 | from maya import cmds, mel 8 | 9 | from .forms import PlayblastForm, NewPresetForm, DelPresetForm 10 | from .. import hooks, resources 11 | from ..renderlayers import enabled_render_layers 12 | from ..viewport import playblast, Viewport 13 | from ..utils import get_maya_window 14 | from ..presets import * 15 | from ..vendor.psforms import controls 16 | from ..vendor.psforms.widgets import FormGroup, FormWidget, IconButton 17 | from ..vendor.psforms.form import generate_form 18 | from ..vendor.Qt import QtCore, QtGui, QtWidgets 19 | 20 | 21 | MISSING = object() 22 | DIALOG_STATE = None 23 | SNAPSHOT_EXT_OPTIONS = { 24 | 'png': '.png', 25 | } 26 | SCENE_STATE_NODE = 'time1' 27 | SCENE_STATE_ATTR = 'mvp_dialog_state' 28 | SCENE_STATE_PATH = SCENE_STATE_NODE + '.' + SCENE_STATE_ATTR 29 | RESOLUTION_OPTIONS = ['Render Settings', '2048x2048', '1920x1080'] 30 | 31 | 32 | def get_sound_track(): 33 | '''Get the current sound track''' 34 | 35 | playback_slider = mel.eval('$tmpVar=$gPlayBackSlider') 36 | return cmds.timeControl(playback_slider, query=True, sound=True) 37 | 38 | 39 | def get_fps(): 40 | '''Get the scene fps''' 41 | 42 | # Returns a unit string like game or 5fps 43 | unit = cmds.currentUnit(query=True, time=True) 44 | 45 | # If unit is not in the following dict - we must have a string like 5fps 46 | fps = { 47 | 'game': '15fps', 48 | 'film': '24fps', 49 | 'pal': '25fps', 50 | 'ntsc': '30fps', 51 | 'show': '48fps', 52 | 'palf': '50fps', 53 | 'ntscf': '60fps', 54 | }.get(unit, unit) 55 | 56 | # Convert to a floating point value 57 | return float(fps.rstrip('fps')) 58 | 59 | 60 | def get_framerange(): 61 | '''Get the scene framerange''' 62 | 63 | return( 64 | cmds.playbackOptions(query=True, minTime=True), 65 | cmds.playbackOptions(query=True, maxTime=True), 66 | ) 67 | 68 | 69 | class IntegrationUI(object): 70 | '''Lazily generated UI Group for an Integration class.''' 71 | 72 | def __init__(self, integration, parent=None): 73 | self.integration = integration 74 | self.parent = parent 75 | self._group = None 76 | self._form = None 77 | 78 | def try_to_toggle(self, value): 79 | enabled = self.integration.set_enabled(value) 80 | 81 | if enabled and self.group.widget is not self.form: 82 | self.group.set_widget(self.form) 83 | 84 | self.group.set_enabled(enabled) 85 | 86 | @property 87 | def group(self): 88 | if not self._group: 89 | self._group = FormGroup( 90 | self.integration.name, 91 | FormWidget(self.integration.name), # Stub widget 92 | parent=self.parent, 93 | ) 94 | self._group.title.setIcon(QtGui.QIcon(self.integration.icon)) 95 | self._group.toggled.connect(self.try_to_toggle) 96 | self.try_to_toggle(self.integration.enabled) 97 | return self._group 98 | 99 | @property 100 | def form(self): 101 | if not self._form: 102 | form = generate_form( 103 | name=self.integration.name, 104 | fields=self.integration.fields(), 105 | title=self.integration.name.title(), 106 | description=self.integration.description or 'No description', 107 | icon=self.integration.icon, 108 | header=False, 109 | columns=self.integration.columns, 110 | labels_on_top=False, 111 | ) 112 | self._form = form.as_widget() 113 | 114 | # Attach controls to integration methods 115 | for control_name, control in self._form.controls.items(): 116 | if isinstance(control.widget, QtWidgets.QTextEdit): 117 | control.widget.setFixedHeight(48) 118 | method = getattr( 119 | self.integration, 120 | 'on_' + control_name + '_changed', 121 | None, 122 | ) 123 | 124 | def slot_method(form, method, control): 125 | def slot(): 126 | method(form, control.get_value()) 127 | return slot 128 | 129 | if method: 130 | control.changed.connect( 131 | slot_method(self._form, method, control) 132 | ) 133 | return self._form 134 | 135 | 136 | def get_scene_dialog_state(default=MISSING): 137 | '''Retrieve dialog state from a maya scene''' 138 | 139 | if cmds.objExists(SCENE_STATE_PATH): 140 | encoded_state = cmds.getAttr(SCENE_STATE_PATH) 141 | if encoded_state: 142 | return json.loads(encoded_state) 143 | 144 | if default is not MISSING: 145 | return default 146 | 147 | 148 | def set_scene_dialog_state(state): 149 | '''Store dialog state in a maya scene''' 150 | 151 | if not cmds.objExists(SCENE_STATE_PATH): 152 | cmds.addAttr(SCENE_STATE_NODE, ln=SCENE_STATE_ATTR, dataType='string') 153 | 154 | encoded_state = json.dumps(state) 155 | cmds.setAttr(SCENE_STATE_PATH, encoded_state, type='string') 156 | 157 | 158 | def new_preset_dialog(parent_dialog): 159 | 160 | dialog = NewPresetForm.as_dialog(parent=parent_dialog) 161 | dialog.panel.set_options([v.panel for v in Viewport.iter()]) 162 | 163 | def on_accept(): 164 | name = dialog.name.get_value() 165 | panel = dialog.panel.get_value() 166 | v = Viewport.from_panel(panel) 167 | state = v.get_state() 168 | new_preset(name, state) 169 | update_presets(parent_dialog, name) 170 | 171 | def identify_clicked(): 172 | panel = dialog.panel.get_value() 173 | v = Viewport.from_panel(panel) 174 | v.identify() 175 | 176 | identify = QtWidgets.QPushButton('identify') 177 | identify.clicked.connect(identify_clicked) 178 | 179 | dialog.panel.grid.addWidget(identify, 1, 2) 180 | dialog.accepted.connect(on_accept) 181 | dialog.setStyleSheet(resources.get_style()) 182 | dialog.show() 183 | 184 | 185 | def del_preset_dialog(parent_dialog): 186 | 187 | def on_accept(): 188 | name = parent_dialog.preset.get_value() 189 | del_preset(name) 190 | update_presets(parent_dialog) 191 | 192 | dialog = DelPresetForm.as_dialog(parent=parent_dialog) 193 | dialog.accepted.connect(on_accept) 194 | dialog.setStyleSheet(resources.get_style()) 195 | dialog.show() 196 | 197 | 198 | def update_presets(dialog, name=None): 199 | 200 | presets = ['Current Settings'] + sorted([n for n, s in get_presets()]) 201 | dialog.preset.set_options(presets) 202 | if name: 203 | dialog.preset.set_value(name) 204 | 205 | 206 | class PlayblastDialog(object): 207 | 208 | def __init__(self): 209 | self.form = PlayblastForm.as_dialog( 210 | frameless=False, 211 | parent=get_maya_window(), 212 | ) 213 | self.setup_controls() 214 | self.setup_connections() 215 | self.restore_form_state() 216 | self.apply_stylesheet() 217 | 218 | def show(self): 219 | self.form.show() 220 | 221 | def store_form_state(self): 222 | '''Store form state in scene''' 223 | global DIALOG_STATE 224 | DIALOG_STATE = self.form.get_value() 225 | 226 | # Update integration ui states 227 | for name, inst in self.integrations.items(): 228 | if not inst.enabled: 229 | # Only store integration state for enabled integrations 230 | DIALOG_STATE.pop(name, None) 231 | 232 | set_scene_dialog_state(DIALOG_STATE) 233 | 234 | def restore_form_state(self): 235 | '''Restore form state from scene''' 236 | dialog_state = get_scene_dialog_state(DIALOG_STATE) 237 | if dialog_state: 238 | # Update integration ui states 239 | for name, inst in self.integrations.items(): 240 | state = dialog_state.pop(name, None) 241 | if state: 242 | inst.ui.try_to_toggle(True) 243 | inst.ui.group.set_value(**state) 244 | else: 245 | inst.ui.try_to_toggle(False) 246 | 247 | # Update base dialog state 248 | self.form.set_value(strict=False, **dialog_state) 249 | 250 | if dialog_state['path_option'] != 'Custom': 251 | self.update_path() 252 | 253 | else: 254 | # Defaults when no dialog state is present 255 | self.form.resolution.set_value(( 256 | cmds.getAttr('defaultResolution.width'), 257 | cmds.getAttr('defaultResolution.height'), 258 | )) 259 | 260 | def setup_controls(self): 261 | '''Setup controls and options - Add additional widgets''' 262 | 263 | # Camera options 264 | cameras = cmds.ls(cameras=True) 265 | for c in ['frontShape', 'sideShape', 'topShape']: 266 | if c in cameras: 267 | cameras.remove(c) 268 | self.form.camera.set_options(cameras) 269 | 270 | # Viewport Presets 271 | self.form.preset.grid.setColumnStretch(1, 1) 272 | presets_menu_button = IconButton( 273 | icon=resources.get_path('dots-vertical.png'), 274 | tip='Resolution Presets.', 275 | name='resolution_menu', 276 | ) 277 | presets_menu = QtWidgets.QMenu(parent=presets_menu_button) 278 | presets_menu.addAction('New Preset', partial(new_preset_dialog, self.form)) 279 | presets_menu.addAction('Delete Preset', partial(del_preset_dialog, self.form)) 280 | presets_menu_button.setMenu(presets_menu) 281 | self.form.preset.grid.addWidget(presets_menu_button, 1, 2) 282 | update_presets(self.form) 283 | 284 | # Extension options 285 | ext_option = controls.StringOptionControl( 286 | 'Ext Option', 287 | labeled=False, 288 | ) 289 | ext_option.set_options(list(hooks.extension.keys())) 290 | self.form.controls['ext_option'] = ext_option 291 | self.form.ext_option = ext_option 292 | self.form.filename.grid.addWidget(ext_option.widget, 1, 2) 293 | 294 | # Add path option control 295 | path_options = ['Custom'] 296 | path_options.extend(hooks.pathgen.keys()) 297 | path_option = controls.StringOptionControl( 298 | 'Path Option', 299 | labeled=False, 300 | ) 301 | path_option.set_options(path_options) 302 | self.form.controls['path_option'] = path_option 303 | self.form.path_option = path_option 304 | self.form.filename.grid.addWidget(path_option.widget, 1, 3) 305 | 306 | # Identify button 307 | identify_button = QtWidgets.QPushButton('highlight viewport') 308 | identify_button.clicked.connect(self.on_identify) 309 | self.form.button_layout.insertWidget(0, identify_button) 310 | 311 | # Resolution Options 312 | res_menu_button = IconButton( 313 | icon=resources.get_path('dots-vertical.png'), 314 | tip='Resolution Presets.', 315 | name='resolution_menu', 316 | ) 317 | res_menu = QtWidgets.QMenu(parent=res_menu_button) 318 | for option in RESOLUTION_OPTIONS: 319 | res_menu.addAction( 320 | option, 321 | partial(self.apply_resolution_preset, option) 322 | ) 323 | res_menu_button.setMenu(res_menu) 324 | self.form.resolution.grid.addWidget(res_menu_button, 1, 2) 325 | 326 | half_res = controls.ToggleControl( 327 | name='Half Res', 328 | icon=resources.get_path('half.png'), 329 | tip='Render half resolution.', 330 | labeled=False, 331 | ) 332 | self.form.controls['half_res'] = half_res 333 | self.form.half_res = half_res 334 | self.form.resolution.grid.addWidget(half_res.widget, 1, 3) 335 | 336 | # Add postrender hook checkboxes 337 | self.form.postrender.after_toggled.connect(self.auto_resize) 338 | self.form.postrender.toggle() 339 | for postrender in hooks.postrender.values(): 340 | self.form.postrender.add_control( 341 | postrender.name, 342 | controls.BoolControl( 343 | postrender.name, 344 | label_on_top=False, 345 | default=postrender.default, 346 | ) 347 | ) 348 | 349 | # Add integration groups 350 | self.integrations = {} 351 | for name, integration in hooks.integration.items(): 352 | inst = integration() 353 | inst.ui = IntegrationUI(inst, self.form) 354 | inst.ui.group.after_toggled.connect(self.auto_resize) 355 | inst.form = inst.ui.group 356 | self.form.add_form( 357 | name, 358 | inst.ui.group, 359 | ) 360 | self.integrations[name] = inst 361 | 362 | def auto_resize(self): 363 | QtCore.QTimer.singleShot(20, self.form.adjustSize) 364 | 365 | def setup_connections(self): 366 | '''Connect form controls to callbacks''' 367 | 368 | self.form.capture_mode.changed.connect(self.on_capture_mode_changed) 369 | self.form.ext_option.changed.connect(self.on_ext_changed) 370 | self.form.path_option.changed.connect(self.update_path) 371 | self.form.filename.changed.connect(self.on_filename_changed) 372 | self.form.accepted.connect(self.on_accept) 373 | 374 | def apply_resolution_preset(self, preset): 375 | if preset == 'Render Settings': 376 | self.form.resolution.set_value(( 377 | cmds.getAttr('defaultResolution.width'), 378 | cmds.getAttr('defaultResolution.height'), 379 | )) 380 | else: 381 | self.form.resolution.set_value([int(num) for num in preset.split('x')]) 382 | 383 | def apply_stylesheet(self): 384 | '''Apply mvp stylesheet''' 385 | 386 | self.form.setStyleSheet(resources.get_style()) 387 | 388 | def on_ext_changed(self): 389 | '''Called when the extension changes.''' 390 | self.update_path() 391 | 392 | def update_path(self): 393 | '''Update the file path based on pathgen, ext, and capture mode''' 394 | 395 | path_opt = self.form.path_option.get_value() 396 | ext_opt = self.form.ext_option.get_value() 397 | capture_mode = self.form.capture_mode.get_value() 398 | 399 | path_gen = hooks.pathgen.get(path_opt, None) 400 | 401 | if not path_gen: 402 | return 403 | 404 | # Compute path using PathGenerator hook 405 | path = path_gen.handler() 406 | 407 | if capture_mode == 'snapshot': 408 | 409 | # Snapshots are just pngs 410 | ext = SNAPSHOT_EXT_OPTIONS.get(ext_opt, '.png') 411 | 412 | else: 413 | 414 | # Get ext from Extension hook 415 | extension = hooks.extension.get(ext_opt, None) 416 | if extension: 417 | ext = extension.ext 418 | else: 419 | ext = '.mov' 420 | 421 | # Put png sequences in a subdirectory 422 | if ext == '.png': 423 | scene_path = cmds.file(q=True, shn=True, sn=True) 424 | scene = os.path.splitext(scene_path)[0] 425 | path = os.path.join(path, scene).replace('\\', '/') 426 | 427 | # Add extension to path and set filename 428 | path += ext 429 | self.form.filename.set_value(path) 430 | 431 | # Notify integrations of filename change 432 | for integration in self.integrations.values(): 433 | if integration.enabled: 434 | integration.on_filename_changed(self.form, path) 435 | 436 | def on_capture_mode_changed(self): 437 | '''Update path when capture_mode changes.''' 438 | 439 | mode = self.form.capture_mode.get_value() 440 | if mode == 'sequence': 441 | self.form.ext_option.set_options(list(hooks.extension.keys())) 442 | else: 443 | self.form.ext_option.set_options(list(SNAPSHOT_EXT_OPTIONS)) 444 | 445 | self.update_path() 446 | 447 | def on_filename_changed(self): 448 | '''Update contol values when filename changes.''' 449 | 450 | self.form.path_option.set_value('Custom') 451 | 452 | name = self.form.filename.get_value() 453 | 454 | for ext in hooks.extension.values(): 455 | if name.endswith(ext.ext): 456 | self.form.ext_option.set_value(ext.name) 457 | return 458 | 459 | def on_identify(self): 460 | '''Highlight the active viewport.''' 461 | 462 | Viewport.active().identify() 463 | 464 | def on_accept(self): 465 | '''When form is accepted - parse options and capture''' 466 | 467 | self.store_form_state() 468 | data = self.form.get_value() 469 | 470 | # Prepare to render 471 | if data['preset'] == 'Current Settings': 472 | state = Viewport.active().get_state() 473 | else: 474 | state = get_preset(data['preset']) 475 | state.pop('camera') 476 | 477 | output_dir = os.path.dirname(data['filename']) 478 | if not os.path.exists(output_dir): 479 | os.makedirs(output_dir) 480 | 481 | renders = [] 482 | if data['render_layers'] == 'current': 483 | renders.append(self._render(state, data)) 484 | else: 485 | with enabled_render_layers() as layers: 486 | for layer in layers: 487 | # Copy data and add layer name to filename 488 | render_data = data.copy() 489 | base, ext = render_data['filename'].rsplit('.', 1) 490 | layer_name = '{}_{}.{}'.format(base, layer.name(), ext) 491 | render_data['filename'] = layer_name 492 | 493 | layer.switchToLayer() 494 | renders.append(self._render(state, render_data)) 495 | 496 | # Finalize 497 | for name, integration in self.integrations.items(): 498 | if integration.enabled: 499 | integration.finalize(integration.form, renders) 500 | 501 | def _render(self, state, data): 502 | 503 | # Execute integration before_playblast 504 | for name, integration in self.integrations.items(): 505 | if integration.enabled: 506 | integration.before_playblast(integration.form, data) 507 | 508 | # Prepare resolution 509 | if data.pop('half_res', False): 510 | data['resolution'] = ( 511 | round_to_even(data['resolution'][0] * 0.5), 512 | round_to_even(data['resolution'][1] * 0.5), 513 | ) 514 | 515 | if data['capture_mode'] == 'snapshot': 516 | # Render snapshot 517 | data['start_frame'] = cmds.currentTime(q=True) 518 | data['end_frame'] = data['start_frame'] 519 | out_file = playblast( 520 | camera=data['camera'], 521 | state=state, 522 | format='image', 523 | compression='png', 524 | completeFilename=data['filename'], 525 | frame=[data['start_frame']], 526 | width=data['resolution'][0], 527 | height=data['resolution'][1], 528 | ) 529 | else: 530 | # Call extension handler 531 | extension = hooks.extension.get(data['ext_option']) 532 | framerange = get_framerange() 533 | data['start_frame'] = framerange[0] 534 | data['end_frame'] = framerange[1] 535 | data['fps'] = get_fps() 536 | data['sound'] = get_sound_track() 537 | out_file = extension.handler( 538 | data=dict( 539 | state=state, 540 | camera=data['camera'], 541 | filename=data['filename'], 542 | width=data['resolution'][0], 543 | height=data['resolution'][1], 544 | sound=data['sound'], 545 | fps=data['fps'], 546 | start_frame=data['start_frame'], 547 | end_frame=data['end_frame'], 548 | ), 549 | options=extension.options or {}, 550 | ) 551 | 552 | # Execute postrender callbacks 553 | if 'postrender' in data: 554 | for name, enabled in data['postrender'].items(): 555 | if enabled: 556 | postrender = hooks.postrender.get(name) 557 | postrender.handler(data['filename']) 558 | 559 | # Update filename from playblast command 560 | data['filename'] = out_file 561 | 562 | # Execute integration after_playblast 563 | for name, integration in self.integrations.items(): 564 | if integration.enabled: 565 | integration.after_playblast(integration.form, data) 566 | 567 | return data 568 | 569 | 570 | def round_to_even(value): 571 | return value if value % 2 == 0 else value + 1 572 | 573 | 574 | def show(): 575 | '''Main playblast form.''' 576 | 577 | PlayblastDialog._instance = PlayblastDialog() 578 | PlayblastDialog._instance.show() 579 | return PlayblastDialog._instance 580 | -------------------------------------------------------------------------------- /mvp/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | from contextlib import contextmanager 5 | 6 | from .vendor.Qt import QtWidgets 7 | 8 | 9 | def get_maya_window(cache=[]): 10 | '''Get Maya MainWindow as a QWidget.''' 11 | 12 | if cache: 13 | return cache[0] 14 | 15 | for widget in QtWidgets.QApplication.instance().topLevelWidgets(): 16 | if widget.objectName() == 'MayaWindow': 17 | cache.append(widget) 18 | return widget 19 | 20 | raise RuntimeError('Could not locate MayaWindow...') 21 | 22 | 23 | def wait(delay=1): 24 | '''Delay python execution for a specified amount of time''' 25 | 26 | app = QtWidgets.QApplication.instance() 27 | 28 | s = time.clock() 29 | while True: 30 | if time.clock() - s >= delay: 31 | return 32 | 33 | app.processEvents() 34 | 35 | 36 | @contextmanager 37 | def viewport_state(viewport, state): 38 | '''Sets a viewports state options for the duration of the context. 39 | 40 | Example: 41 | 42 | # Turn off the display of nurbsCurves 43 | viewport = Viewport.active() 44 | state = viewport.state() 45 | state['nurbsCurves'] = False 46 | 47 | with viewport_state(viewport, state): 48 | # Do something with nurbsCurves off 49 | ''' 50 | 51 | previous_state = viewport.get_state() 52 | try: 53 | viewport.set_state(state) 54 | yield 55 | finally: 56 | viewport.set_state(previous_state) 57 | -------------------------------------------------------------------------------- /mvp/vendor/Qt.py: -------------------------------------------------------------------------------- 1 | """Minimal Python 2 & 3 shim around all Qt bindings 2 | 3 | DOCUMENTATION 4 | Qt.py was born in the film and visual effects industry to address 5 | the growing need for the development of software capable of running 6 | with more than one flavour of the Qt bindings for Python - PySide, 7 | PySide2, PyQt4 and PyQt5. 8 | 9 | 1. Build for one, run with all 10 | 2. Explicit is better than implicit 11 | 3. Support co-existence 12 | 13 | Default resolution order: 14 | - PySide2 15 | - PyQt5 16 | - PySide 17 | - PyQt4 18 | 19 | Usage: 20 | >> import sys 21 | >> from Qt import QtWidgets 22 | >> app = QtWidgets.QApplication(sys.argv) 23 | >> button = QtWidgets.QPushButton("Hello World") 24 | >> button.show() 25 | >> app.exec_() 26 | 27 | All members of PySide2 are mapped from other bindings, should they exist. 28 | If no equivalent member exist, it is excluded from Qt.py and inaccessible. 29 | The idea is to highlight members that exist across all supported binding, 30 | and guarantee that code that runs on one binding runs on all others. 31 | 32 | For more details, visit https://github.com/mottosso/Qt.py 33 | 34 | LICENSE 35 | 36 | See end of file for license (MIT, BSD) information. 37 | 38 | """ 39 | 40 | import os 41 | import sys 42 | import types 43 | import shutil 44 | import importlib 45 | 46 | 47 | __version__ = "1.2.1" 48 | 49 | # Enable support for `from Qt import *` 50 | __all__ = [] 51 | 52 | # Flags from environment variables 53 | QT_VERBOSE = bool(os.getenv("QT_VERBOSE")) 54 | QT_PREFERRED_BINDING = os.getenv("QT_PREFERRED_BINDING", "") 55 | QT_SIP_API_HINT = os.getenv("QT_SIP_API_HINT") 56 | 57 | # Reference to Qt.py 58 | Qt = sys.modules[__name__] 59 | Qt.QtCompat = types.ModuleType("QtCompat") 60 | 61 | try: 62 | long 63 | except NameError: 64 | # Python 3 compatibility 65 | long = int 66 | 67 | 68 | """Common members of all bindings 69 | 70 | This is where each member of Qt.py is explicitly defined. 71 | It is based on a "lowest common denominator" of all bindings; 72 | including members found in each of the 4 bindings. 73 | 74 | The "_common_members" dictionary is generated using the 75 | build_membership.sh script. 76 | 77 | """ 78 | 79 | _common_members = { 80 | "QtCore": [ 81 | "QAbstractAnimation", 82 | "QAbstractEventDispatcher", 83 | "QAbstractItemModel", 84 | "QAbstractListModel", 85 | "QAbstractState", 86 | "QAbstractTableModel", 87 | "QAbstractTransition", 88 | "QAnimationGroup", 89 | "QBasicTimer", 90 | "QBitArray", 91 | "QBuffer", 92 | "QByteArray", 93 | "QByteArrayMatcher", 94 | "QChildEvent", 95 | "QCoreApplication", 96 | "QCryptographicHash", 97 | "QDataStream", 98 | "QDate", 99 | "QDateTime", 100 | "QDir", 101 | "QDirIterator", 102 | "QDynamicPropertyChangeEvent", 103 | "QEasingCurve", 104 | "QElapsedTimer", 105 | "QEvent", 106 | "QEventLoop", 107 | "QEventTransition", 108 | "QFile", 109 | "QFileInfo", 110 | "QFileSystemWatcher", 111 | "QFinalState", 112 | "QGenericArgument", 113 | "QGenericReturnArgument", 114 | "QHistoryState", 115 | "QItemSelectionRange", 116 | "QIODevice", 117 | "QLibraryInfo", 118 | "QLine", 119 | "QLineF", 120 | "QLocale", 121 | "QMargins", 122 | "QMetaClassInfo", 123 | "QMetaEnum", 124 | "QMetaMethod", 125 | "QMetaObject", 126 | "QMetaProperty", 127 | "QMimeData", 128 | "QModelIndex", 129 | "QMutex", 130 | "QMutexLocker", 131 | "QObject", 132 | "QParallelAnimationGroup", 133 | "QPauseAnimation", 134 | "QPersistentModelIndex", 135 | "QPluginLoader", 136 | "QPoint", 137 | "QPointF", 138 | "QProcess", 139 | "QProcessEnvironment", 140 | "QPropertyAnimation", 141 | "QReadLocker", 142 | "QReadWriteLock", 143 | "QRect", 144 | "QRectF", 145 | "QRegExp", 146 | "QResource", 147 | "QRunnable", 148 | "QSemaphore", 149 | "QSequentialAnimationGroup", 150 | "QSettings", 151 | "QSignalMapper", 152 | "QSignalTransition", 153 | "QSize", 154 | "QSizeF", 155 | "QSocketNotifier", 156 | "QState", 157 | "QStateMachine", 158 | "QSysInfo", 159 | "QSystemSemaphore", 160 | "QT_TRANSLATE_NOOP", 161 | "QT_TR_NOOP", 162 | "QT_TR_NOOP_UTF8", 163 | "QTemporaryFile", 164 | "QTextBoundaryFinder", 165 | "QTextCodec", 166 | "QTextDecoder", 167 | "QTextEncoder", 168 | "QTextStream", 169 | "QTextStreamManipulator", 170 | "QThread", 171 | "QThreadPool", 172 | "QTime", 173 | "QTimeLine", 174 | "QTimer", 175 | "QTimerEvent", 176 | "QTranslator", 177 | "QUrl", 178 | "QVariantAnimation", 179 | "QWaitCondition", 180 | "QWriteLocker", 181 | "QXmlStreamAttribute", 182 | "QXmlStreamAttributes", 183 | "QXmlStreamEntityDeclaration", 184 | "QXmlStreamEntityResolver", 185 | "QXmlStreamNamespaceDeclaration", 186 | "QXmlStreamNotationDeclaration", 187 | "QXmlStreamReader", 188 | "QXmlStreamWriter", 189 | "Qt", 190 | "QtCriticalMsg", 191 | "QtDebugMsg", 192 | "QtFatalMsg", 193 | "QtMsgType", 194 | "QtSystemMsg", 195 | "QtWarningMsg", 196 | "qAbs", 197 | "qAddPostRoutine", 198 | "qChecksum", 199 | "qCritical", 200 | "qDebug", 201 | "qFatal", 202 | "qFuzzyCompare", 203 | "qIsFinite", 204 | "qIsInf", 205 | "qIsNaN", 206 | "qIsNull", 207 | "qRegisterResourceData", 208 | "qUnregisterResourceData", 209 | "qVersion", 210 | "qWarning", 211 | "qrand", 212 | "qsrand" 213 | ], 214 | "QtGui": [ 215 | "QAbstractTextDocumentLayout", 216 | "QActionEvent", 217 | "QBitmap", 218 | "QBrush", 219 | "QClipboard", 220 | "QCloseEvent", 221 | "QColor", 222 | "QConicalGradient", 223 | "QContextMenuEvent", 224 | "QCursor", 225 | "QDesktopServices", 226 | "QDoubleValidator", 227 | "QDrag", 228 | "QDragEnterEvent", 229 | "QDragLeaveEvent", 230 | "QDragMoveEvent", 231 | "QDropEvent", 232 | "QFileOpenEvent", 233 | "QFocusEvent", 234 | "QFont", 235 | "QFontDatabase", 236 | "QFontInfo", 237 | "QFontMetrics", 238 | "QFontMetricsF", 239 | "QGradient", 240 | "QHelpEvent", 241 | "QHideEvent", 242 | "QHoverEvent", 243 | "QIcon", 244 | "QIconDragEvent", 245 | "QIconEngine", 246 | "QImage", 247 | "QImageIOHandler", 248 | "QImageReader", 249 | "QImageWriter", 250 | "QInputEvent", 251 | "QInputMethodEvent", 252 | "QIntValidator", 253 | "QKeyEvent", 254 | "QKeySequence", 255 | "QLinearGradient", 256 | "QMatrix2x2", 257 | "QMatrix2x3", 258 | "QMatrix2x4", 259 | "QMatrix3x2", 260 | "QMatrix3x3", 261 | "QMatrix3x4", 262 | "QMatrix4x2", 263 | "QMatrix4x3", 264 | "QMatrix4x4", 265 | "QMouseEvent", 266 | "QMoveEvent", 267 | "QMovie", 268 | "QPaintDevice", 269 | "QPaintEngine", 270 | "QPaintEngineState", 271 | "QPaintEvent", 272 | "QPainter", 273 | "QPainterPath", 274 | "QPainterPathStroker", 275 | "QPalette", 276 | "QPen", 277 | "QPicture", 278 | "QPictureIO", 279 | "QPixmap", 280 | "QPixmapCache", 281 | "QPolygon", 282 | "QPolygonF", 283 | "QQuaternion", 284 | "QRadialGradient", 285 | "QRegExpValidator", 286 | "QRegion", 287 | "QResizeEvent", 288 | "QSessionManager", 289 | "QShortcutEvent", 290 | "QShowEvent", 291 | "QStandardItem", 292 | "QStandardItemModel", 293 | "QStatusTipEvent", 294 | "QSyntaxHighlighter", 295 | "QTabletEvent", 296 | "QTextBlock", 297 | "QTextBlockFormat", 298 | "QTextBlockGroup", 299 | "QTextBlockUserData", 300 | "QTextCharFormat", 301 | "QTextCursor", 302 | "QTextDocument", 303 | "QTextDocumentFragment", 304 | "QTextFormat", 305 | "QTextFragment", 306 | "QTextFrame", 307 | "QTextFrameFormat", 308 | "QTextImageFormat", 309 | "QTextInlineObject", 310 | "QTextItem", 311 | "QTextLayout", 312 | "QTextLength", 313 | "QTextLine", 314 | "QTextList", 315 | "QTextListFormat", 316 | "QTextObject", 317 | "QTextObjectInterface", 318 | "QTextOption", 319 | "QTextTable", 320 | "QTextTableCell", 321 | "QTextTableCellFormat", 322 | "QTextTableFormat", 323 | "QTouchEvent", 324 | "QTransform", 325 | "QValidator", 326 | "QVector2D", 327 | "QVector3D", 328 | "QVector4D", 329 | "QWhatsThisClickedEvent", 330 | "QWheelEvent", 331 | "QWindowStateChangeEvent", 332 | "qAlpha", 333 | "qBlue", 334 | "qGray", 335 | "qGreen", 336 | "qIsGray", 337 | "qRed", 338 | "qRgb", 339 | "qRgba" 340 | ], 341 | "QtHelp": [ 342 | "QHelpContentItem", 343 | "QHelpContentModel", 344 | "QHelpContentWidget", 345 | "QHelpEngine", 346 | "QHelpEngineCore", 347 | "QHelpIndexModel", 348 | "QHelpIndexWidget", 349 | "QHelpSearchEngine", 350 | "QHelpSearchQuery", 351 | "QHelpSearchQueryWidget", 352 | "QHelpSearchResultWidget" 353 | ], 354 | "QtMultimedia": [ 355 | "QAbstractVideoBuffer", 356 | "QAbstractVideoSurface", 357 | "QAudio", 358 | "QAudioDeviceInfo", 359 | "QAudioFormat", 360 | "QAudioInput", 361 | "QAudioOutput", 362 | "QVideoFrame", 363 | "QVideoSurfaceFormat" 364 | ], 365 | "QtNetwork": [ 366 | "QAbstractNetworkCache", 367 | "QAbstractSocket", 368 | "QAuthenticator", 369 | "QHostAddress", 370 | "QHostInfo", 371 | "QLocalServer", 372 | "QLocalSocket", 373 | "QNetworkAccessManager", 374 | "QNetworkAddressEntry", 375 | "QNetworkCacheMetaData", 376 | "QNetworkConfiguration", 377 | "QNetworkConfigurationManager", 378 | "QNetworkCookie", 379 | "QNetworkCookieJar", 380 | "QNetworkDiskCache", 381 | "QNetworkInterface", 382 | "QNetworkProxy", 383 | "QNetworkProxyFactory", 384 | "QNetworkProxyQuery", 385 | "QNetworkReply", 386 | "QNetworkRequest", 387 | "QNetworkSession", 388 | "QSsl", 389 | "QTcpServer", 390 | "QTcpSocket", 391 | "QUdpSocket" 392 | ], 393 | "QtOpenGL": [ 394 | "QGL", 395 | "QGLContext", 396 | "QGLFormat", 397 | "QGLWidget" 398 | ], 399 | "QtPrintSupport": [ 400 | "QAbstractPrintDialog", 401 | "QPageSetupDialog", 402 | "QPrintDialog", 403 | "QPrintEngine", 404 | "QPrintPreviewDialog", 405 | "QPrintPreviewWidget", 406 | "QPrinter", 407 | "QPrinterInfo" 408 | ], 409 | "QtSql": [ 410 | "QSql", 411 | "QSqlDatabase", 412 | "QSqlDriver", 413 | "QSqlDriverCreatorBase", 414 | "QSqlError", 415 | "QSqlField", 416 | "QSqlIndex", 417 | "QSqlQuery", 418 | "QSqlQueryModel", 419 | "QSqlRecord", 420 | "QSqlRelation", 421 | "QSqlRelationalDelegate", 422 | "QSqlRelationalTableModel", 423 | "QSqlResult", 424 | "QSqlTableModel" 425 | ], 426 | "QtSvg": [ 427 | "QGraphicsSvgItem", 428 | "QSvgGenerator", 429 | "QSvgRenderer", 430 | "QSvgWidget" 431 | ], 432 | "QtTest": [ 433 | "QTest" 434 | ], 435 | "QtWidgets": [ 436 | "QAbstractButton", 437 | "QAbstractGraphicsShapeItem", 438 | "QAbstractItemDelegate", 439 | "QAbstractItemView", 440 | "QAbstractScrollArea", 441 | "QAbstractSlider", 442 | "QAbstractSpinBox", 443 | "QAction", 444 | "QActionGroup", 445 | "QApplication", 446 | "QBoxLayout", 447 | "QButtonGroup", 448 | "QCalendarWidget", 449 | "QCheckBox", 450 | "QColorDialog", 451 | "QColumnView", 452 | "QComboBox", 453 | "QCommandLinkButton", 454 | "QCommonStyle", 455 | "QCompleter", 456 | "QDataWidgetMapper", 457 | "QDateEdit", 458 | "QDateTimeEdit", 459 | "QDesktopWidget", 460 | "QDial", 461 | "QDialog", 462 | "QDialogButtonBox", 463 | "QDirModel", 464 | "QDockWidget", 465 | "QDoubleSpinBox", 466 | "QErrorMessage", 467 | "QFileDialog", 468 | "QFileIconProvider", 469 | "QFileSystemModel", 470 | "QFocusFrame", 471 | "QFontComboBox", 472 | "QFontDialog", 473 | "QFormLayout", 474 | "QFrame", 475 | "QGesture", 476 | "QGestureEvent", 477 | "QGestureRecognizer", 478 | "QGraphicsAnchor", 479 | "QGraphicsAnchorLayout", 480 | "QGraphicsBlurEffect", 481 | "QGraphicsColorizeEffect", 482 | "QGraphicsDropShadowEffect", 483 | "QGraphicsEffect", 484 | "QGraphicsEllipseItem", 485 | "QGraphicsGridLayout", 486 | "QGraphicsItem", 487 | "QGraphicsItemGroup", 488 | "QGraphicsLayout", 489 | "QGraphicsLayoutItem", 490 | "QGraphicsLineItem", 491 | "QGraphicsLinearLayout", 492 | "QGraphicsObject", 493 | "QGraphicsOpacityEffect", 494 | "QGraphicsPathItem", 495 | "QGraphicsPixmapItem", 496 | "QGraphicsPolygonItem", 497 | "QGraphicsProxyWidget", 498 | "QGraphicsRectItem", 499 | "QGraphicsRotation", 500 | "QGraphicsScale", 501 | "QGraphicsScene", 502 | "QGraphicsSceneContextMenuEvent", 503 | "QGraphicsSceneDragDropEvent", 504 | "QGraphicsSceneEvent", 505 | "QGraphicsSceneHelpEvent", 506 | "QGraphicsSceneHoverEvent", 507 | "QGraphicsSceneMouseEvent", 508 | "QGraphicsSceneMoveEvent", 509 | "QGraphicsSceneResizeEvent", 510 | "QGraphicsSceneWheelEvent", 511 | "QGraphicsSimpleTextItem", 512 | "QGraphicsTextItem", 513 | "QGraphicsTransform", 514 | "QGraphicsView", 515 | "QGraphicsWidget", 516 | "QGridLayout", 517 | "QGroupBox", 518 | "QHBoxLayout", 519 | "QHeaderView", 520 | "QInputDialog", 521 | "QItemDelegate", 522 | "QItemEditorCreatorBase", 523 | "QItemEditorFactory", 524 | "QKeyEventTransition", 525 | "QLCDNumber", 526 | "QLabel", 527 | "QLayout", 528 | "QLayoutItem", 529 | "QLineEdit", 530 | "QListView", 531 | "QListWidget", 532 | "QListWidgetItem", 533 | "QMainWindow", 534 | "QMdiArea", 535 | "QMdiSubWindow", 536 | "QMenu", 537 | "QMenuBar", 538 | "QMessageBox", 539 | "QMouseEventTransition", 540 | "QPanGesture", 541 | "QPinchGesture", 542 | "QPlainTextDocumentLayout", 543 | "QPlainTextEdit", 544 | "QProgressBar", 545 | "QProgressDialog", 546 | "QPushButton", 547 | "QRadioButton", 548 | "QRubberBand", 549 | "QScrollArea", 550 | "QScrollBar", 551 | "QShortcut", 552 | "QSizeGrip", 553 | "QSizePolicy", 554 | "QSlider", 555 | "QSpacerItem", 556 | "QSpinBox", 557 | "QSplashScreen", 558 | "QSplitter", 559 | "QSplitterHandle", 560 | "QStackedLayout", 561 | "QStackedWidget", 562 | "QStatusBar", 563 | "QStyle", 564 | "QStyleFactory", 565 | "QStyleHintReturn", 566 | "QStyleHintReturnMask", 567 | "QStyleHintReturnVariant", 568 | "QStyleOption", 569 | "QStyleOptionButton", 570 | "QStyleOptionComboBox", 571 | "QStyleOptionComplex", 572 | "QStyleOptionDockWidget", 573 | "QStyleOptionFocusRect", 574 | "QStyleOptionFrame", 575 | "QStyleOptionGraphicsItem", 576 | "QStyleOptionGroupBox", 577 | "QStyleOptionHeader", 578 | "QStyleOptionMenuItem", 579 | "QStyleOptionProgressBar", 580 | "QStyleOptionRubberBand", 581 | "QStyleOptionSizeGrip", 582 | "QStyleOptionSlider", 583 | "QStyleOptionSpinBox", 584 | "QStyleOptionTab", 585 | "QStyleOptionTabBarBase", 586 | "QStyleOptionTabWidgetFrame", 587 | "QStyleOptionTitleBar", 588 | "QStyleOptionToolBar", 589 | "QStyleOptionToolBox", 590 | "QStyleOptionToolButton", 591 | "QStyleOptionViewItem", 592 | "QStylePainter", 593 | "QStyledItemDelegate", 594 | "QSwipeGesture", 595 | "QSystemTrayIcon", 596 | "QTabBar", 597 | "QTabWidget", 598 | "QTableView", 599 | "QTableWidget", 600 | "QTableWidgetItem", 601 | "QTableWidgetSelectionRange", 602 | "QTapAndHoldGesture", 603 | "QTapGesture", 604 | "QTextBrowser", 605 | "QTextEdit", 606 | "QTimeEdit", 607 | "QToolBar", 608 | "QToolBox", 609 | "QToolButton", 610 | "QToolTip", 611 | "QTreeView", 612 | "QTreeWidget", 613 | "QTreeWidgetItem", 614 | "QTreeWidgetItemIterator", 615 | "QUndoCommand", 616 | "QUndoGroup", 617 | "QUndoStack", 618 | "QUndoView", 619 | "QVBoxLayout", 620 | "QWhatsThis", 621 | "QWidget", 622 | "QWidgetAction", 623 | "QWidgetItem", 624 | "QWizard", 625 | "QWizardPage" 626 | ], 627 | "QtX11Extras": [ 628 | "QX11Info" 629 | ], 630 | "QtXml": [ 631 | "QDomAttr", 632 | "QDomCDATASection", 633 | "QDomCharacterData", 634 | "QDomComment", 635 | "QDomDocument", 636 | "QDomDocumentFragment", 637 | "QDomDocumentType", 638 | "QDomElement", 639 | "QDomEntity", 640 | "QDomEntityReference", 641 | "QDomImplementation", 642 | "QDomNamedNodeMap", 643 | "QDomNode", 644 | "QDomNodeList", 645 | "QDomNotation", 646 | "QDomProcessingInstruction", 647 | "QDomText", 648 | "QXmlAttributes", 649 | "QXmlContentHandler", 650 | "QXmlDTDHandler", 651 | "QXmlDeclHandler", 652 | "QXmlDefaultHandler", 653 | "QXmlEntityResolver", 654 | "QXmlErrorHandler", 655 | "QXmlInputSource", 656 | "QXmlLexicalHandler", 657 | "QXmlLocator", 658 | "QXmlNamespaceSupport", 659 | "QXmlParseException", 660 | "QXmlReader", 661 | "QXmlSimpleReader" 662 | ], 663 | "QtXmlPatterns": [ 664 | "QAbstractMessageHandler", 665 | "QAbstractUriResolver", 666 | "QAbstractXmlNodeModel", 667 | "QAbstractXmlReceiver", 668 | "QSourceLocation", 669 | "QXmlFormatter", 670 | "QXmlItem", 671 | "QXmlName", 672 | "QXmlNamePool", 673 | "QXmlNodeModelIndex", 674 | "QXmlQuery", 675 | "QXmlResultItems", 676 | "QXmlSchema", 677 | "QXmlSchemaValidator", 678 | "QXmlSerializer" 679 | ] 680 | } 681 | 682 | 683 | def _qInstallMessageHandler(handler): 684 | """Install a message handler that works in all bindings 685 | 686 | Args: 687 | handler: A function that takes 3 arguments, or None 688 | """ 689 | def messageOutputHandler(*args): 690 | # In Qt4 bindings, message handlers are passed 2 arguments 691 | # In Qt5 bindings, message handlers are passed 3 arguments 692 | # The first argument is a QtMsgType 693 | # The last argument is the message to be printed 694 | # The Middle argument (if passed) is a QMessageLogContext 695 | if len(args) == 3: 696 | msgType, logContext, msg = args 697 | elif len(args) == 2: 698 | msgType, msg = args 699 | logContext = None 700 | else: 701 | raise TypeError( 702 | "handler expected 2 or 3 arguments, got {0}".format(len(args))) 703 | 704 | if isinstance(msg, bytes): 705 | # In python 3, some bindings pass a bytestring, which cannot be 706 | # used elsewhere. Decoding a python 2 or 3 bytestring object will 707 | # consistently return a unicode object. 708 | msg = msg.decode() 709 | 710 | handler(msgType, logContext, msg) 711 | 712 | passObject = messageOutputHandler if handler else handler 713 | if Qt.IsPySide or Qt.IsPyQt4: 714 | return Qt._QtCore.qInstallMsgHandler(passObject) 715 | elif Qt.IsPySide2 or Qt.IsPyQt5: 716 | return Qt._QtCore.qInstallMessageHandler(passObject) 717 | 718 | 719 | def _getcpppointer(object): 720 | if hasattr(Qt, "_shiboken2"): 721 | return getattr(Qt, "_shiboken2").getCppPointer(object)[0] 722 | elif hasattr(Qt, "_shiboken"): 723 | return getattr(Qt, "_shiboken").getCppPointer(object)[0] 724 | elif hasattr(Qt, "_sip"): 725 | return getattr(Qt, "_sip").unwrapinstance(object) 726 | raise AttributeError("'module' has no attribute 'getCppPointer'") 727 | 728 | 729 | def _wrapinstance(ptr, base=None): 730 | """Enable implicit cast of pointer to most suitable class 731 | 732 | This behaviour is available in sip per default. 733 | 734 | Based on http://nathanhorne.com/pyqtpyside-wrap-instance 735 | 736 | Usage: 737 | This mechanism kicks in under these circumstances. 738 | 1. Qt.py is using PySide 1 or 2. 739 | 2. A `base` argument is not provided. 740 | 741 | See :func:`QtCompat.wrapInstance()` 742 | 743 | Arguments: 744 | ptr (long): Pointer to QObject in memory 745 | base (QObject, optional): Base class to wrap with. Defaults to QObject, 746 | which should handle anything. 747 | 748 | """ 749 | 750 | assert isinstance(ptr, long), "Argument 'ptr' must be of type " 751 | assert (base is None) or issubclass(base, Qt.QtCore.QObject), ( 752 | "Argument 'base' must be of type ") 753 | 754 | if Qt.IsPyQt4 or Qt.IsPyQt5: 755 | func = getattr(Qt, "_sip").wrapinstance 756 | elif Qt.IsPySide2: 757 | func = getattr(Qt, "_shiboken2").wrapInstance 758 | elif Qt.IsPySide: 759 | func = getattr(Qt, "_shiboken").wrapInstance 760 | else: 761 | raise AttributeError("'module' has no attribute 'wrapInstance'") 762 | 763 | if base is None: 764 | q_object = func(long(ptr), Qt.QtCore.QObject) 765 | meta_object = q_object.metaObject() 766 | class_name = meta_object.className() 767 | super_class_name = meta_object.superClass().className() 768 | 769 | if hasattr(Qt.QtWidgets, class_name): 770 | base = getattr(Qt.QtWidgets, class_name) 771 | 772 | elif hasattr(Qt.QtWidgets, super_class_name): 773 | base = getattr(Qt.QtWidgets, super_class_name) 774 | 775 | else: 776 | base = Qt.QtCore.QObject 777 | 778 | return func(long(ptr), base) 779 | 780 | 781 | def _isvalid(object): 782 | """Check if the object is valid to use in Python runtime. 783 | 784 | Usage: 785 | See :func:`QtCompat.isValid()` 786 | 787 | Arguments: 788 | object (QObject): QObject to check the validity of. 789 | 790 | """ 791 | 792 | assert isinstance(object, Qt.QtCore.QObject) 793 | 794 | if hasattr(Qt, "_shiboken2"): 795 | return getattr(Qt, "_shiboken2").isValid(object) 796 | 797 | elif hasattr(Qt, "_shiboken"): 798 | return getattr(Qt, "_shiboken").isValid(object) 799 | 800 | elif hasattr(Qt, "_sip"): 801 | return not getattr(Qt, "_sip").isdeleted(object) 802 | 803 | else: 804 | raise AttributeError("'module' has no attribute isValid") 805 | 806 | 807 | def _translate(context, sourceText, *args): 808 | # In Qt4 bindings, translate can be passed 2 or 3 arguments 809 | # In Qt5 bindings, translate can be passed 2 arguments 810 | # The first argument is disambiguation[str] 811 | # The last argument is n[int] 812 | # The middle argument can be encoding[QtCore.QCoreApplication.Encoding] 813 | if len(args) == 3: 814 | disambiguation, encoding, n = args 815 | elif len(args) == 2: 816 | disambiguation, n = args 817 | encoding = None 818 | else: 819 | raise TypeError( 820 | "Expected 4 or 5 arguments, got {0}.".format(len(args) + 2)) 821 | 822 | if hasattr(Qt.QtCore, "QCoreApplication"): 823 | app = getattr(Qt.QtCore, "QCoreApplication") 824 | else: 825 | raise NotImplementedError( 826 | "Missing QCoreApplication implementation for {binding}".format( 827 | binding=Qt.__binding__, 828 | ) 829 | ) 830 | if Qt.__binding__ in ("PySide2", "PyQt5"): 831 | sanitized_args = [context, sourceText, disambiguation, n] 832 | else: 833 | sanitized_args = [ 834 | context, 835 | sourceText, 836 | disambiguation, 837 | encoding or app.CodecForTr, 838 | n 839 | ] 840 | return app.translate(*sanitized_args) 841 | 842 | 843 | def _loadUi(uifile, baseinstance=None): 844 | """Dynamically load a user interface from the given `uifile` 845 | 846 | This function calls `uic.loadUi` if using PyQt bindings, 847 | else it implements a comparable binding for PySide. 848 | 849 | Documentation: 850 | http://pyqt.sourceforge.net/Docs/PyQt5/designer.html#PyQt5.uic.loadUi 851 | 852 | Arguments: 853 | uifile (str): Absolute path to Qt Designer file. 854 | baseinstance (QWidget): Instantiated QWidget or subclass thereof 855 | 856 | Return: 857 | baseinstance if `baseinstance` is not `None`. Otherwise 858 | return the newly created instance of the user interface. 859 | 860 | """ 861 | if hasattr(Qt, "_uic"): 862 | return Qt._uic.loadUi(uifile, baseinstance) 863 | 864 | elif hasattr(Qt, "_QtUiTools"): 865 | # Implement `PyQt5.uic.loadUi` for PySide(2) 866 | 867 | class _UiLoader(Qt._QtUiTools.QUiLoader): 868 | """Create the user interface in a base instance. 869 | 870 | Unlike `Qt._QtUiTools.QUiLoader` itself this class does not 871 | create a new instance of the top-level widget, but creates the user 872 | interface in an existing instance of the top-level class if needed. 873 | 874 | This mimics the behaviour of `PyQt5.uic.loadUi`. 875 | 876 | """ 877 | 878 | def __init__(self, baseinstance): 879 | super(_UiLoader, self).__init__(baseinstance) 880 | self.baseinstance = baseinstance 881 | self.custom_widgets = {} 882 | 883 | def _loadCustomWidgets(self, etree): 884 | """ 885 | Workaround to pyside-77 bug. 886 | 887 | From QUiLoader doc we should use registerCustomWidget method. 888 | But this causes a segfault on some platforms. 889 | 890 | Instead we fetch from customwidgets DOM node the python class 891 | objects. Then we can directly use them in createWidget method. 892 | """ 893 | 894 | def headerToModule(header): 895 | """ 896 | Translate a header file to python module path 897 | foo/bar.h => foo.bar 898 | """ 899 | # Remove header extension 900 | module = os.path.splitext(header)[0] 901 | 902 | # Replace os separator by python module separator 903 | return module.replace("/", ".").replace("\\", ".") 904 | 905 | custom_widgets = etree.find("customwidgets") 906 | 907 | if custom_widgets is None: 908 | return 909 | 910 | for custom_widget in custom_widgets: 911 | class_name = custom_widget.find("class").text 912 | header = custom_widget.find("header").text 913 | module = importlib.import_module(headerToModule(header)) 914 | self.custom_widgets[class_name] = getattr(module, 915 | class_name) 916 | 917 | def load(self, uifile, *args, **kwargs): 918 | from xml.etree.ElementTree import ElementTree 919 | 920 | # For whatever reason, if this doesn't happen then 921 | # reading an invalid or non-existing .ui file throws 922 | # a RuntimeError. 923 | etree = ElementTree() 924 | etree.parse(uifile) 925 | self._loadCustomWidgets(etree) 926 | 927 | widget = Qt._QtUiTools.QUiLoader.load( 928 | self, uifile, *args, **kwargs) 929 | 930 | # Workaround for PySide 1.0.9, see issue #208 931 | widget.parentWidget() 932 | 933 | return widget 934 | 935 | def createWidget(self, class_name, parent=None, name=""): 936 | """Called for each widget defined in ui file 937 | 938 | Overridden here to populate `baseinstance` instead. 939 | 940 | """ 941 | 942 | if parent is None and self.baseinstance: 943 | # Supposed to create the top-level widget, 944 | # return the base instance instead 945 | return self.baseinstance 946 | 947 | # For some reason, Line is not in the list of available 948 | # widgets, but works fine, so we have to special case it here. 949 | if class_name in self.availableWidgets() + ["Line"]: 950 | # Create a new widget for child widgets 951 | widget = Qt._QtUiTools.QUiLoader.createWidget(self, 952 | class_name, 953 | parent, 954 | name) 955 | elif class_name in self.custom_widgets: 956 | widget = self.custom_widgets[class_name](parent) 957 | else: 958 | raise Exception("Custom widget '%s' not supported" 959 | % class_name) 960 | 961 | if self.baseinstance: 962 | # Set an attribute for the new child widget on the base 963 | # instance, just like PyQt5.uic.loadUi does. 964 | setattr(self.baseinstance, name, widget) 965 | 966 | return widget 967 | 968 | widget = _UiLoader(baseinstance).load(uifile) 969 | Qt.QtCore.QMetaObject.connectSlotsByName(widget) 970 | 971 | return widget 972 | 973 | else: 974 | raise NotImplementedError("No implementation available for loadUi") 975 | 976 | 977 | """Misplaced members 978 | 979 | These members from the original submodule are misplaced relative PySide2 980 | 981 | """ 982 | _misplaced_members = { 983 | "PySide2": { 984 | "QtCore.QStringListModel": "QtCore.QStringListModel", 985 | "QtGui.QStringListModel": "QtCore.QStringListModel", 986 | "QtCore.Property": "QtCore.Property", 987 | "QtCore.Signal": "QtCore.Signal", 988 | "QtCore.Slot": "QtCore.Slot", 989 | "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel", 990 | "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", 991 | "QtCore.QItemSelection": "QtCore.QItemSelection", 992 | "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel", 993 | "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange", 994 | "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], 995 | "shiboken2.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], 996 | "shiboken2.getCppPointer": ["QtCompat.getCppPointer", _getcpppointer], 997 | "shiboken2.isValid": ["QtCompat.isValid", _isvalid], 998 | "QtWidgets.qApp": "QtWidgets.QApplication.instance()", 999 | "QtCore.QCoreApplication.translate": [ 1000 | "QtCompat.translate", _translate 1001 | ], 1002 | "QtWidgets.QApplication.translate": [ 1003 | "QtCompat.translate", _translate 1004 | ], 1005 | "QtCore.qInstallMessageHandler": [ 1006 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler 1007 | ], 1008 | }, 1009 | "PyQt5": { 1010 | "QtCore.pyqtProperty": "QtCore.Property", 1011 | "QtCore.pyqtSignal": "QtCore.Signal", 1012 | "QtCore.pyqtSlot": "QtCore.Slot", 1013 | "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel", 1014 | "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", 1015 | "QtCore.QStringListModel": "QtCore.QStringListModel", 1016 | "QtCore.QItemSelection": "QtCore.QItemSelection", 1017 | "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel", 1018 | "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange", 1019 | "uic.loadUi": ["QtCompat.loadUi", _loadUi], 1020 | "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance], 1021 | "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer], 1022 | "sip.isdeleted": ["QtCompat.isValid", _isvalid], 1023 | "QtWidgets.qApp": "QtWidgets.QApplication.instance()", 1024 | "QtCore.QCoreApplication.translate": [ 1025 | "QtCompat.translate", _translate 1026 | ], 1027 | "QtWidgets.QApplication.translate": [ 1028 | "QtCompat.translate", _translate 1029 | ], 1030 | "QtCore.qInstallMessageHandler": [ 1031 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler 1032 | ], 1033 | }, 1034 | "PySide": { 1035 | "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", 1036 | "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", 1037 | "QtGui.QStringListModel": "QtCore.QStringListModel", 1038 | "QtGui.QItemSelection": "QtCore.QItemSelection", 1039 | "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel", 1040 | "QtCore.Property": "QtCore.Property", 1041 | "QtCore.Signal": "QtCore.Signal", 1042 | "QtCore.Slot": "QtCore.Slot", 1043 | "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange", 1044 | "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog", 1045 | "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog", 1046 | "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog", 1047 | "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine", 1048 | "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog", 1049 | "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", 1050 | "QtGui.QPrinter": "QtPrintSupport.QPrinter", 1051 | "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo", 1052 | "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], 1053 | "shiboken.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], 1054 | "shiboken.unwrapInstance": ["QtCompat.getCppPointer", _getcpppointer], 1055 | "shiboken.isValid": ["QtCompat.isValid", _isvalid], 1056 | "QtGui.qApp": "QtWidgets.QApplication.instance()", 1057 | "QtCore.QCoreApplication.translate": [ 1058 | "QtCompat.translate", _translate 1059 | ], 1060 | "QtGui.QApplication.translate": [ 1061 | "QtCompat.translate", _translate 1062 | ], 1063 | "QtCore.qInstallMsgHandler": [ 1064 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler 1065 | ], 1066 | }, 1067 | "PyQt4": { 1068 | "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", 1069 | "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", 1070 | "QtGui.QItemSelection": "QtCore.QItemSelection", 1071 | "QtGui.QStringListModel": "QtCore.QStringListModel", 1072 | "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel", 1073 | "QtCore.pyqtProperty": "QtCore.Property", 1074 | "QtCore.pyqtSignal": "QtCore.Signal", 1075 | "QtCore.pyqtSlot": "QtCore.Slot", 1076 | "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange", 1077 | "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog", 1078 | "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog", 1079 | "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog", 1080 | "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine", 1081 | "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog", 1082 | "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", 1083 | "QtGui.QPrinter": "QtPrintSupport.QPrinter", 1084 | "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo", 1085 | # "QtCore.pyqtSignature": "QtCore.Slot", 1086 | "uic.loadUi": ["QtCompat.loadUi", _loadUi], 1087 | "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance], 1088 | "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer], 1089 | "sip.isdeleted": ["QtCompat.isValid", _isvalid], 1090 | "QtCore.QString": "str", 1091 | "QtGui.qApp": "QtWidgets.QApplication.instance()", 1092 | "QtCore.QCoreApplication.translate": [ 1093 | "QtCompat.translate", _translate 1094 | ], 1095 | "QtGui.QApplication.translate": [ 1096 | "QtCompat.translate", _translate 1097 | ], 1098 | "QtCore.qInstallMsgHandler": [ 1099 | "QtCompat.qInstallMessageHandler", _qInstallMessageHandler 1100 | ], 1101 | } 1102 | } 1103 | 1104 | """ Compatibility Members 1105 | 1106 | This dictionary is used to build Qt.QtCompat objects that provide a consistent 1107 | interface for obsolete members, and differences in binding return values. 1108 | 1109 | { 1110 | "binding": { 1111 | "classname": { 1112 | "targetname": "binding_namespace", 1113 | } 1114 | } 1115 | } 1116 | """ 1117 | _compatibility_members = { 1118 | "PySide2": { 1119 | "QWidget": { 1120 | "grab": "QtWidgets.QWidget.grab", 1121 | }, 1122 | "QHeaderView": { 1123 | "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable", 1124 | "setSectionsClickable": 1125 | "QtWidgets.QHeaderView.setSectionsClickable", 1126 | "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode", 1127 | "setSectionResizeMode": 1128 | "QtWidgets.QHeaderView.setSectionResizeMode", 1129 | "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable", 1130 | "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable", 1131 | }, 1132 | "QFileDialog": { 1133 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", 1134 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", 1135 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", 1136 | }, 1137 | }, 1138 | "PyQt5": { 1139 | "QWidget": { 1140 | "grab": "QtWidgets.QWidget.grab", 1141 | }, 1142 | "QHeaderView": { 1143 | "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable", 1144 | "setSectionsClickable": 1145 | "QtWidgets.QHeaderView.setSectionsClickable", 1146 | "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode", 1147 | "setSectionResizeMode": 1148 | "QtWidgets.QHeaderView.setSectionResizeMode", 1149 | "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable", 1150 | "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable", 1151 | }, 1152 | "QFileDialog": { 1153 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", 1154 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", 1155 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", 1156 | }, 1157 | }, 1158 | "PySide": { 1159 | "QWidget": { 1160 | "grab": "QtWidgets.QPixmap.grabWidget", 1161 | }, 1162 | "QHeaderView": { 1163 | "sectionsClickable": "QtWidgets.QHeaderView.isClickable", 1164 | "setSectionsClickable": "QtWidgets.QHeaderView.setClickable", 1165 | "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode", 1166 | "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode", 1167 | "sectionsMovable": "QtWidgets.QHeaderView.isMovable", 1168 | "setSectionsMovable": "QtWidgets.QHeaderView.setMovable", 1169 | }, 1170 | "QFileDialog": { 1171 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", 1172 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", 1173 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", 1174 | }, 1175 | }, 1176 | "PyQt4": { 1177 | "QWidget": { 1178 | "grab": "QtWidgets.QPixmap.grabWidget", 1179 | }, 1180 | "QHeaderView": { 1181 | "sectionsClickable": "QtWidgets.QHeaderView.isClickable", 1182 | "setSectionsClickable": "QtWidgets.QHeaderView.setClickable", 1183 | "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode", 1184 | "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode", 1185 | "sectionsMovable": "QtWidgets.QHeaderView.isMovable", 1186 | "setSectionsMovable": "QtWidgets.QHeaderView.setMovable", 1187 | }, 1188 | "QFileDialog": { 1189 | "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", 1190 | "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", 1191 | "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", 1192 | }, 1193 | }, 1194 | } 1195 | 1196 | 1197 | def _apply_site_config(): 1198 | try: 1199 | import QtSiteConfig 1200 | except ImportError: 1201 | # If no QtSiteConfig module found, no modifications 1202 | # to _common_members are needed. 1203 | pass 1204 | else: 1205 | # Provide the ability to modify the dicts used to build Qt.py 1206 | if hasattr(QtSiteConfig, 'update_members'): 1207 | QtSiteConfig.update_members(_common_members) 1208 | 1209 | if hasattr(QtSiteConfig, 'update_misplaced_members'): 1210 | QtSiteConfig.update_misplaced_members(members=_misplaced_members) 1211 | 1212 | if hasattr(QtSiteConfig, 'update_compatibility_members'): 1213 | QtSiteConfig.update_compatibility_members( 1214 | members=_compatibility_members) 1215 | 1216 | 1217 | def _new_module(name): 1218 | return types.ModuleType(__name__ + "." + name) 1219 | 1220 | 1221 | def _import_sub_module(module, name): 1222 | """import_sub_module will mimic the function of importlib.import_module""" 1223 | module = __import__(module.__name__ + "." + name) 1224 | for level in name.split("."): 1225 | module = getattr(module, level) 1226 | return module 1227 | 1228 | 1229 | def _setup(module, extras): 1230 | """Install common submodules""" 1231 | 1232 | Qt.__binding__ = module.__name__ 1233 | 1234 | for name in list(_common_members) + extras: 1235 | try: 1236 | submodule = _import_sub_module( 1237 | module, name) 1238 | except ImportError: 1239 | try: 1240 | # For extra modules like sip and shiboken that may not be 1241 | # children of the binding. 1242 | submodule = __import__(name) 1243 | except ImportError: 1244 | continue 1245 | 1246 | setattr(Qt, "_" + name, submodule) 1247 | 1248 | if name not in extras: 1249 | # Store reference to original binding, 1250 | # but don't store speciality modules 1251 | # such as uic or QtUiTools 1252 | setattr(Qt, name, _new_module(name)) 1253 | 1254 | 1255 | def _reassign_misplaced_members(binding): 1256 | """Apply misplaced members from `binding` to Qt.py 1257 | 1258 | Arguments: 1259 | binding (dict): Misplaced members 1260 | 1261 | """ 1262 | 1263 | for src, dst in _misplaced_members[binding].items(): 1264 | dst_value = None 1265 | 1266 | src_parts = src.split(".") 1267 | src_module = src_parts[0] 1268 | src_member = None 1269 | if len(src_parts) > 1: 1270 | src_member = src_parts[1:] 1271 | 1272 | if isinstance(dst, (list, tuple)): 1273 | dst, dst_value = dst 1274 | 1275 | dst_parts = dst.split(".") 1276 | dst_module = dst_parts[0] 1277 | dst_member = None 1278 | if len(dst_parts) > 1: 1279 | dst_member = dst_parts[1] 1280 | 1281 | # Get the member we want to store in the namesapce. 1282 | if not dst_value: 1283 | try: 1284 | _part = getattr(Qt, "_" + src_module) 1285 | while src_member: 1286 | member = src_member.pop(0) 1287 | _part = getattr(_part, member) 1288 | dst_value = _part 1289 | except AttributeError: 1290 | # If the member we want to store in the namespace does not 1291 | # exist, there is no need to continue. This can happen if a 1292 | # request was made to rename a member that didn't exist, for 1293 | # example if QtWidgets isn't available on the target platform. 1294 | _log("Misplaced member has no source: {0}".format(src)) 1295 | continue 1296 | 1297 | try: 1298 | src_object = getattr(Qt, dst_module) 1299 | except AttributeError: 1300 | if dst_module not in _common_members: 1301 | # Only create the Qt parent module if its listed in 1302 | # _common_members. Without this check, if you remove QtCore 1303 | # from _common_members, the default _misplaced_members will add 1304 | # Qt.QtCore so it can add Signal, Slot, etc. 1305 | msg = 'Not creating missing member module "{m}" for "{c}"' 1306 | _log(msg.format(m=dst_module, c=dst_member)) 1307 | continue 1308 | # If the dst is valid but the Qt parent module does not exist 1309 | # then go ahead and create a new module to contain the member. 1310 | setattr(Qt, dst_module, _new_module(dst_module)) 1311 | src_object = getattr(Qt, dst_module) 1312 | # Enable direct import of the new module 1313 | sys.modules[__name__ + "." + dst_module] = src_object 1314 | 1315 | if not dst_value: 1316 | dst_value = getattr(Qt, "_" + src_module) 1317 | if src_member: 1318 | dst_value = getattr(dst_value, src_member) 1319 | 1320 | setattr( 1321 | src_object, 1322 | dst_member or dst_module, 1323 | dst_value 1324 | ) 1325 | 1326 | 1327 | def _build_compatibility_members(binding, decorators=None): 1328 | """Apply `binding` to QtCompat 1329 | 1330 | Arguments: 1331 | binding (str): Top level binding in _compatibility_members. 1332 | decorators (dict, optional): Provides the ability to decorate the 1333 | original Qt methods when needed by a binding. This can be used 1334 | to change the returned value to a standard value. The key should 1335 | be the classname, the value is a dict where the keys are the 1336 | target method names, and the values are the decorator functions. 1337 | 1338 | """ 1339 | 1340 | decorators = decorators or dict() 1341 | 1342 | # Allow optional site-level customization of the compatibility members. 1343 | # This method does not need to be implemented in QtSiteConfig. 1344 | try: 1345 | import QtSiteConfig 1346 | except ImportError: 1347 | pass 1348 | else: 1349 | if hasattr(QtSiteConfig, 'update_compatibility_decorators'): 1350 | QtSiteConfig.update_compatibility_decorators(binding, decorators) 1351 | 1352 | _QtCompat = type("QtCompat", (object,), {}) 1353 | 1354 | for classname, bindings in _compatibility_members[binding].items(): 1355 | attrs = {} 1356 | for target, binding in bindings.items(): 1357 | namespaces = binding.split('.') 1358 | try: 1359 | src_object = getattr(Qt, "_" + namespaces[0]) 1360 | except AttributeError as e: 1361 | _log("QtCompat: AttributeError: %s" % e) 1362 | # Skip reassignment of non-existing members. 1363 | # This can happen if a request was made to 1364 | # rename a member that didn't exist, for example 1365 | # if QtWidgets isn't available on the target platform. 1366 | continue 1367 | 1368 | # Walk down any remaining namespace getting the object assuming 1369 | # that if the first namespace exists the rest will exist. 1370 | for namespace in namespaces[1:]: 1371 | src_object = getattr(src_object, namespace) 1372 | 1373 | # decorate the Qt method if a decorator was provided. 1374 | if target in decorators.get(classname, []): 1375 | # staticmethod must be called on the decorated method to 1376 | # prevent a TypeError being raised when the decorated method 1377 | # is called. 1378 | src_object = staticmethod( 1379 | decorators[classname][target](src_object)) 1380 | 1381 | attrs[target] = src_object 1382 | 1383 | # Create the QtCompat class and install it into the namespace 1384 | compat_class = type(classname, (_QtCompat,), attrs) 1385 | setattr(Qt.QtCompat, classname, compat_class) 1386 | 1387 | 1388 | def _pyside2(): 1389 | """Initialise PySide2 1390 | 1391 | These functions serve to test the existence of a binding 1392 | along with set it up in such a way that it aligns with 1393 | the final step; adding members from the original binding 1394 | to Qt.py 1395 | 1396 | """ 1397 | 1398 | import PySide2 as module 1399 | extras = ["QtUiTools"] 1400 | try: 1401 | try: 1402 | # Before merge of PySide and shiboken 1403 | import shiboken2 1404 | except ImportError: 1405 | # After merge of PySide and shiboken, May 2017 1406 | from PySide2 import shiboken2 1407 | extras.append("shiboken2") 1408 | except ImportError: 1409 | pass 1410 | 1411 | _setup(module, extras) 1412 | Qt.__binding_version__ = module.__version__ 1413 | 1414 | if hasattr(Qt, "_shiboken2"): 1415 | Qt.QtCompat.wrapInstance = _wrapinstance 1416 | Qt.QtCompat.getCppPointer = _getcpppointer 1417 | Qt.QtCompat.delete = shiboken2.delete 1418 | 1419 | if hasattr(Qt, "_QtUiTools"): 1420 | Qt.QtCompat.loadUi = _loadUi 1421 | 1422 | if hasattr(Qt, "_QtCore"): 1423 | Qt.__qt_version__ = Qt._QtCore.qVersion() 1424 | Qt.QtCompat.dataChanged = ( 1425 | lambda self, topleft, bottomright, roles=None: 1426 | self.dataChanged.emit(topleft, bottomright, roles or []) 1427 | ) 1428 | 1429 | if hasattr(Qt, "_QtWidgets"): 1430 | Qt.QtCompat.setSectionResizeMode = \ 1431 | Qt._QtWidgets.QHeaderView.setSectionResizeMode 1432 | 1433 | _reassign_misplaced_members("PySide2") 1434 | _build_compatibility_members("PySide2") 1435 | 1436 | 1437 | def _pyside(): 1438 | """Initialise PySide""" 1439 | 1440 | import PySide as module 1441 | extras = ["QtUiTools"] 1442 | try: 1443 | try: 1444 | # Before merge of PySide and shiboken 1445 | import shiboken 1446 | except ImportError: 1447 | # After merge of PySide and shiboken, May 2017 1448 | from PySide import shiboken 1449 | extras.append("shiboken") 1450 | except ImportError: 1451 | pass 1452 | 1453 | _setup(module, extras) 1454 | Qt.__binding_version__ = module.__version__ 1455 | 1456 | if hasattr(Qt, "_shiboken"): 1457 | Qt.QtCompat.wrapInstance = _wrapinstance 1458 | Qt.QtCompat.getCppPointer = _getcpppointer 1459 | Qt.QtCompat.delete = shiboken.delete 1460 | 1461 | if hasattr(Qt, "_QtUiTools"): 1462 | Qt.QtCompat.loadUi = _loadUi 1463 | 1464 | if hasattr(Qt, "_QtGui"): 1465 | setattr(Qt, "QtWidgets", _new_module("QtWidgets")) 1466 | setattr(Qt, "_QtWidgets", Qt._QtGui) 1467 | if hasattr(Qt._QtGui, "QX11Info"): 1468 | setattr(Qt, "QtX11Extras", _new_module("QtX11Extras")) 1469 | Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info 1470 | 1471 | Qt.QtCompat.setSectionResizeMode = Qt._QtGui.QHeaderView.setResizeMode 1472 | 1473 | if hasattr(Qt, "_QtCore"): 1474 | Qt.__qt_version__ = Qt._QtCore.qVersion() 1475 | Qt.QtCompat.dataChanged = ( 1476 | lambda self, topleft, bottomright, roles=None: 1477 | self.dataChanged.emit(topleft, bottomright) 1478 | ) 1479 | 1480 | _reassign_misplaced_members("PySide") 1481 | _build_compatibility_members("PySide") 1482 | 1483 | 1484 | def _pyqt5(): 1485 | """Initialise PyQt5""" 1486 | 1487 | import PyQt5 as module 1488 | extras = ["uic"] 1489 | 1490 | try: 1491 | import sip 1492 | extras += ["sip"] 1493 | except ImportError: 1494 | 1495 | # Relevant to PyQt5 5.11 and above 1496 | try: 1497 | from PyQt5 import sip 1498 | extras += ["sip"] 1499 | except ImportError: 1500 | sip = None 1501 | 1502 | _setup(module, extras) 1503 | if hasattr(Qt, "_sip"): 1504 | Qt.QtCompat.wrapInstance = _wrapinstance 1505 | Qt.QtCompat.getCppPointer = _getcpppointer 1506 | Qt.QtCompat.delete = sip.delete 1507 | 1508 | if hasattr(Qt, "_uic"): 1509 | Qt.QtCompat.loadUi = _loadUi 1510 | 1511 | if hasattr(Qt, "_QtCore"): 1512 | Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR 1513 | Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR 1514 | Qt.QtCompat.dataChanged = ( 1515 | lambda self, topleft, bottomright, roles=None: 1516 | self.dataChanged.emit(topleft, bottomright, roles or []) 1517 | ) 1518 | 1519 | if hasattr(Qt, "_QtWidgets"): 1520 | Qt.QtCompat.setSectionResizeMode = \ 1521 | Qt._QtWidgets.QHeaderView.setSectionResizeMode 1522 | 1523 | _reassign_misplaced_members("PyQt5") 1524 | _build_compatibility_members('PyQt5') 1525 | 1526 | 1527 | def _pyqt4(): 1528 | """Initialise PyQt4""" 1529 | 1530 | import sip 1531 | 1532 | # Validation of envivornment variable. Prevents an error if 1533 | # the variable is invalid since it's just a hint. 1534 | try: 1535 | hint = int(QT_SIP_API_HINT) 1536 | except TypeError: 1537 | hint = None # Variable was None, i.e. not set. 1538 | except ValueError: 1539 | raise ImportError("QT_SIP_API_HINT=%s must be a 1 or 2") 1540 | 1541 | for api in ("QString", 1542 | "QVariant", 1543 | "QDate", 1544 | "QDateTime", 1545 | "QTextStream", 1546 | "QTime", 1547 | "QUrl"): 1548 | try: 1549 | sip.setapi(api, hint or 2) 1550 | except AttributeError: 1551 | raise ImportError("PyQt4 < 4.6 isn't supported by Qt.py") 1552 | except ValueError: 1553 | actual = sip.getapi(api) 1554 | if not hint: 1555 | raise ImportError("API version already set to %d" % actual) 1556 | else: 1557 | # Having provided a hint indicates a soft constraint, one 1558 | # that doesn't throw an exception. 1559 | sys.stderr.write( 1560 | "Warning: API '%s' has already been set to %d.\n" 1561 | % (api, actual) 1562 | ) 1563 | 1564 | import PyQt4 as module 1565 | extras = ["uic"] 1566 | try: 1567 | import sip 1568 | extras.append(sip.__name__) 1569 | except ImportError: 1570 | sip = None 1571 | 1572 | _setup(module, extras) 1573 | if hasattr(Qt, "_sip"): 1574 | Qt.QtCompat.wrapInstance = _wrapinstance 1575 | Qt.QtCompat.getCppPointer = _getcpppointer 1576 | Qt.QtCompat.delete = sip.delete 1577 | 1578 | if hasattr(Qt, "_uic"): 1579 | Qt.QtCompat.loadUi = _loadUi 1580 | 1581 | if hasattr(Qt, "_QtGui"): 1582 | setattr(Qt, "QtWidgets", _new_module("QtWidgets")) 1583 | setattr(Qt, "_QtWidgets", Qt._QtGui) 1584 | if hasattr(Qt._QtGui, "QX11Info"): 1585 | setattr(Qt, "QtX11Extras", _new_module("QtX11Extras")) 1586 | Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info 1587 | 1588 | Qt.QtCompat.setSectionResizeMode = \ 1589 | Qt._QtGui.QHeaderView.setResizeMode 1590 | 1591 | if hasattr(Qt, "_QtCore"): 1592 | Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR 1593 | Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR 1594 | Qt.QtCompat.dataChanged = ( 1595 | lambda self, topleft, bottomright, roles=None: 1596 | self.dataChanged.emit(topleft, bottomright) 1597 | ) 1598 | 1599 | _reassign_misplaced_members("PyQt4") 1600 | 1601 | # QFileDialog QtCompat decorator 1602 | def _standardizeQFileDialog(some_function): 1603 | """Decorator that makes PyQt4 return conform to other bindings""" 1604 | def wrapper(*args, **kwargs): 1605 | ret = (some_function(*args, **kwargs)) 1606 | 1607 | # PyQt4 only returns the selected filename, force it to a 1608 | # standard return of the selected filename, and a empty string 1609 | # for the selected filter 1610 | return ret, '' 1611 | 1612 | wrapper.__doc__ = some_function.__doc__ 1613 | wrapper.__name__ = some_function.__name__ 1614 | 1615 | return wrapper 1616 | 1617 | decorators = { 1618 | "QFileDialog": { 1619 | "getOpenFileName": _standardizeQFileDialog, 1620 | "getOpenFileNames": _standardizeQFileDialog, 1621 | "getSaveFileName": _standardizeQFileDialog, 1622 | } 1623 | } 1624 | _build_compatibility_members('PyQt4', decorators) 1625 | 1626 | 1627 | def _none(): 1628 | """Internal option (used in installer)""" 1629 | 1630 | Mock = type("Mock", (), {"__getattr__": lambda Qt, attr: None}) 1631 | 1632 | Qt.__binding__ = "None" 1633 | Qt.__qt_version__ = "0.0.0" 1634 | Qt.__binding_version__ = "0.0.0" 1635 | Qt.QtCompat.loadUi = lambda uifile, baseinstance=None: None 1636 | Qt.QtCompat.setSectionResizeMode = lambda *args, **kwargs: None 1637 | 1638 | for submodule in _common_members.keys(): 1639 | setattr(Qt, submodule, Mock()) 1640 | setattr(Qt, "_" + submodule, Mock()) 1641 | 1642 | 1643 | def _log(text): 1644 | if QT_VERBOSE: 1645 | sys.stdout.write(text + "\n") 1646 | 1647 | 1648 | def _convert(lines): 1649 | """Convert compiled .ui file from PySide2 to Qt.py 1650 | 1651 | Arguments: 1652 | lines (list): Each line of of .ui file 1653 | 1654 | Usage: 1655 | >> with open("myui.py") as f: 1656 | .. lines = _convert(f.readlines()) 1657 | 1658 | """ 1659 | 1660 | def parse(line): 1661 | line = line.replace("from PySide2 import", "from Qt import QtCompat,") 1662 | line = line.replace("QtWidgets.QApplication.translate", 1663 | "QtCompat.translate") 1664 | if "QtCore.SIGNAL" in line: 1665 | raise NotImplementedError("QtCore.SIGNAL is missing from PyQt5 " 1666 | "and so Qt.py does not support it: you " 1667 | "should avoid defining signals inside " 1668 | "your ui files.") 1669 | return line 1670 | 1671 | parsed = list() 1672 | for line in lines: 1673 | line = parse(line) 1674 | parsed.append(line) 1675 | 1676 | return parsed 1677 | 1678 | 1679 | def _cli(args): 1680 | """Qt.py command-line interface""" 1681 | import argparse 1682 | 1683 | parser = argparse.ArgumentParser() 1684 | parser.add_argument("--convert", 1685 | help="Path to compiled Python module, e.g. my_ui.py") 1686 | parser.add_argument("--compile", 1687 | help="Accept raw .ui file and compile with native " 1688 | "PySide2 compiler.") 1689 | parser.add_argument("--stdout", 1690 | help="Write to stdout instead of file", 1691 | action="store_true") 1692 | parser.add_argument("--stdin", 1693 | help="Read from stdin instead of file", 1694 | action="store_true") 1695 | 1696 | args = parser.parse_args(args) 1697 | 1698 | if args.stdout: 1699 | raise NotImplementedError("--stdout") 1700 | 1701 | if args.stdin: 1702 | raise NotImplementedError("--stdin") 1703 | 1704 | if args.compile: 1705 | raise NotImplementedError("--compile") 1706 | 1707 | if args.convert: 1708 | sys.stdout.write("#\n" 1709 | "# WARNING: --convert is an ALPHA feature.\n#\n" 1710 | "# See https://github.com/mottosso/Qt.py/pull/132\n" 1711 | "# for details.\n" 1712 | "#\n") 1713 | 1714 | # 1715 | # ------> Read 1716 | # 1717 | with open(args.convert) as f: 1718 | lines = _convert(f.readlines()) 1719 | 1720 | backup = "%s_backup%s" % os.path.splitext(args.convert) 1721 | sys.stdout.write("Creating \"%s\"..\n" % backup) 1722 | shutil.copy(args.convert, backup) 1723 | 1724 | # 1725 | # <------ Write 1726 | # 1727 | with open(args.convert, "w") as f: 1728 | f.write("".join(lines)) 1729 | 1730 | sys.stdout.write("Successfully converted \"%s\"\n" % args.convert) 1731 | 1732 | 1733 | def _install(): 1734 | # Default order (customise order and content via QT_PREFERRED_BINDING) 1735 | default_order = ("PySide2", "PyQt5", "PySide", "PyQt4") 1736 | preferred_order = list( 1737 | b for b in QT_PREFERRED_BINDING.split(os.pathsep) if b 1738 | ) 1739 | 1740 | order = preferred_order or default_order 1741 | 1742 | available = { 1743 | "PySide2": _pyside2, 1744 | "PyQt5": _pyqt5, 1745 | "PySide": _pyside, 1746 | "PyQt4": _pyqt4, 1747 | "None": _none 1748 | } 1749 | 1750 | _log("Order: '%s'" % "', '".join(order)) 1751 | 1752 | # Allow site-level customization of the available modules. 1753 | _apply_site_config() 1754 | 1755 | found_binding = False 1756 | for name in order: 1757 | _log("Trying %s" % name) 1758 | 1759 | try: 1760 | available[name]() 1761 | found_binding = True 1762 | break 1763 | 1764 | except ImportError as e: 1765 | _log("ImportError: %s" % e) 1766 | 1767 | except KeyError: 1768 | _log("ImportError: Preferred binding '%s' not found." % name) 1769 | 1770 | if not found_binding: 1771 | # If not binding were found, throw this error 1772 | raise ImportError("No Qt binding were found.") 1773 | 1774 | # Install individual members 1775 | for name, members in _common_members.items(): 1776 | try: 1777 | their_submodule = getattr(Qt, "_%s" % name) 1778 | except AttributeError: 1779 | continue 1780 | 1781 | our_submodule = getattr(Qt, name) 1782 | 1783 | # Enable import * 1784 | __all__.append(name) 1785 | 1786 | # Enable direct import of submodule, 1787 | # e.g. import Qt.QtCore 1788 | sys.modules[__name__ + "." + name] = our_submodule 1789 | 1790 | for member in members: 1791 | # Accept that a submodule may miss certain members. 1792 | try: 1793 | their_member = getattr(their_submodule, member) 1794 | except AttributeError: 1795 | _log("'%s.%s' was missing." % (name, member)) 1796 | continue 1797 | 1798 | setattr(our_submodule, member, their_member) 1799 | 1800 | # Enable direct import of QtCompat 1801 | sys.modules['Qt.QtCompat'] = Qt.QtCompat 1802 | 1803 | # Backwards compatibility 1804 | if hasattr(Qt.QtCompat, 'loadUi'): 1805 | Qt.QtCompat.load_ui = Qt.QtCompat.loadUi 1806 | 1807 | 1808 | _install() 1809 | 1810 | # Setup Binding Enum states 1811 | Qt.IsPySide2 = Qt.__binding__ == 'PySide2' 1812 | Qt.IsPyQt5 = Qt.__binding__ == 'PyQt5' 1813 | Qt.IsPySide = Qt.__binding__ == 'PySide' 1814 | Qt.IsPyQt4 = Qt.__binding__ == 'PyQt4' 1815 | 1816 | """Augment QtCompat 1817 | 1818 | QtCompat contains wrappers and added functionality 1819 | to the original bindings, such as the CLI interface 1820 | and otherwise incompatible members between bindings, 1821 | such as `QHeaderView.setSectionResizeMode`. 1822 | 1823 | """ 1824 | 1825 | Qt.QtCompat._cli = _cli 1826 | Qt.QtCompat._convert = _convert 1827 | 1828 | # Enable command-line interface 1829 | if __name__ == "__main__": 1830 | _cli(sys.argv[1:]) 1831 | 1832 | 1833 | # The MIT License (MIT) 1834 | # 1835 | # Copyright (c) 2016-2017 Marcus Ottosson 1836 | # 1837 | # Permission is hereby granted, free of charge, to any person obtaining a copy 1838 | # of this software and associated documentation files (the "Software"), to deal 1839 | # in the Software without restriction, including without limitation the rights 1840 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 1841 | # copies of the Software, and to permit persons to whom the Software is 1842 | # furnished to do so, subject to the following conditions: 1843 | # 1844 | # The above copyright notice and this permission notice shall be included in 1845 | # all copies or substantial portions of the Software. 1846 | # 1847 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1848 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 1849 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 1850 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 1851 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 1852 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 1853 | # SOFTWARE. 1854 | # 1855 | # In PySide(2), loadUi does not exist, so we implement it 1856 | # 1857 | # `_UiLoader` is adapted from the qtpy project, which was further influenced 1858 | # by qt-helpers which was released under a 3-clause BSD license which in turn 1859 | # is based on a solution at: 1860 | # 1861 | # - https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8 1862 | # 1863 | # The License for this code is as follows: 1864 | # 1865 | # qt-helpers - a common front-end to various Qt modules 1866 | # 1867 | # Copyright (c) 2015, Chris Beaumont and Thomas Robitaille 1868 | # 1869 | # All rights reserved. 1870 | # 1871 | # Redistribution and use in source and binary forms, with or without 1872 | # modification, are permitted provided that the following conditions are 1873 | # met: 1874 | # 1875 | # * Redistributions of source code must retain the above copyright 1876 | # notice, this list of conditions and the following disclaimer. 1877 | # * Redistributions in binary form must reproduce the above copyright 1878 | # notice, this list of conditions and the following disclaimer in the 1879 | # documentation and/or other materials provided with the 1880 | # distribution. 1881 | # * Neither the name of the Glue project nor the names of its contributors 1882 | # may be used to endorse or promote products derived from this software 1883 | # without specific prior written permission. 1884 | # 1885 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 1886 | # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 1887 | # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 1888 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 1889 | # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 1890 | # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 1891 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 1892 | # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 1893 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 1894 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 1895 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 1896 | # 1897 | # Which itself was based on the solution at 1898 | # 1899 | # https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8 1900 | # 1901 | # which was released under the MIT license: 1902 | # 1903 | # Copyright (c) 2011 Sebastian Wiesner 1904 | # Modifications by Charl Botha 1905 | # 1906 | # Permission is hereby granted, free of charge, to any person obtaining a 1907 | # copy of this software and associated documentation files 1908 | # (the "Software"),to deal in the Software without restriction, 1909 | # including without limitation 1910 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, 1911 | # and/or sell copies of the Software, and to permit persons to whom the 1912 | # Software is furnished to do so, subject to the following conditions: 1913 | # 1914 | # The above copyright notice and this permission notice shall be included 1915 | # in all copies or substantial portions of the Software. 1916 | # 1917 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 1918 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 1919 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 1920 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 1921 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 1922 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 1923 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 1924 | -------------------------------------------------------------------------------- /mvp/vendor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danbradham/mvp/7471af9964ff789897792b23d59c597055d566f5/mvp/vendor/__init__.py -------------------------------------------------------------------------------- /mvp/vendor/psforms/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __title__ = 'psforms' 4 | __author__ = 'Dan Bradham' 5 | __email__ = 'danielbradham@gmail.com' 6 | __url__ = 'http://github.com/danbradham/psforms.git' 7 | __version__ = '0.7.0' 8 | __license__ = 'MIT' 9 | __description__ = 'Hassle free PySide forms.' 10 | 11 | import os 12 | 13 | from . import (controls, exc, fields, resource, widgets) 14 | from .form import Form, FormMetaData 15 | from .validators import * 16 | 17 | with open(os.path.join(os.path.dirname(__file__), 'style.css')) as f: 18 | stylesheet = f.read() 19 | -------------------------------------------------------------------------------- /mvp/vendor/psforms/controls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | psforms.controls 4 | ================ 5 | Wraps standard PySide input widgets providing a unified api for getting and 6 | setting their values. Each control implements :meth:`get_value` and 7 | :meth:`set_value`. A required position argument ``value`` or 8 | ``values`` is used to set the default value of the control or in the case 9 | of :class:`ComboBox` and :class:`IntComboBox` a sequence of items to add to 10 | the wrapped QComboBox. In addition each control emits a Signal named `changed` 11 | whenever the value is changed by user interaction. 12 | ''' 13 | 14 | import os 15 | from copy import deepcopy 16 | from .Qt import QtWidgets, QtCore, QtGui 17 | from . import resource 18 | from .widgets import ScalingImage, IconButton 19 | from .exc import ValidationError 20 | 21 | 22 | class BaseControl(QtCore.QObject): 23 | '''Composite Control Object. Used as a base class for all Control Types. 24 | Subclasses must implement init_widgets, set_value, and get_value methods. 25 | ''' 26 | 27 | changed = QtCore.Signal() 28 | validate = QtCore.Signal() 29 | properties = dict( 30 | valid=True, 31 | ) 32 | 33 | def __init__(self, name, labeled=True, label_on_top=True, 34 | default=None, validators=None, *args, **kwargs): 35 | super(BaseControl, self).__init__(*args, **kwargs) 36 | 37 | self._name = name 38 | self._labeled = labeled 39 | self._label_on_top = label_on_top 40 | 41 | self._init_widgets() 42 | self._init_properties() 43 | 44 | self.validators = validators 45 | 46 | if default: 47 | self.set_value(default) 48 | 49 | @property 50 | def name(self): 51 | '''Property that adjusts the name and label of this control.''' 52 | 53 | return self._name 54 | 55 | @name.setter 56 | def name(self, value): 57 | if self.label: 58 | self.label.setText(self._name) 59 | self._name = value 60 | 61 | @property 62 | def labeled(self): 63 | '''Determines whether this label is visible or hidden.''' 64 | 65 | return self._labeled 66 | 67 | @labeled.setter 68 | def labeled(self, value): 69 | self._labeled = value 70 | if self._labeled: 71 | self.label.show() 72 | else: 73 | self.label.hide() 74 | 75 | @property 76 | def label_on_top(self): 77 | '''Determines where the label is drawn, on top or left.''' 78 | 79 | return self._label_on_top 80 | 81 | @label_on_top.setter 82 | def label_on_top(self, value): 83 | self._label_on_top = value 84 | if self._label_on_top: 85 | self.layout.setDirection(QtWidgets.QBoxLayout.TopToBottom) 86 | else: 87 | self.layout.setDirection(QtWidgets.QBoxLayout.LeftToRight) 88 | self.layout.setSpacing(10) 89 | if isinstance(self.widget, QtWidgets.QCheckBox): 90 | self.label.setAlignment( 91 | QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter 92 | ) 93 | else: 94 | self.label.setAlignment( 95 | QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter 96 | ) 97 | 98 | @property 99 | def valid(self): 100 | return self.get_property('valid') 101 | 102 | @valid.setter 103 | def valid(self, value): 104 | self.set_property('valid', value) 105 | 106 | def validate(self): 107 | '''Validate this control''' 108 | if not self.validators: 109 | return 110 | 111 | value = self.get_value() 112 | 113 | for v in self.validators: 114 | try: 115 | v(value) 116 | except ValidationError as e: 117 | self.valid = False 118 | self.errlabel.setText('*' + str(e)) 119 | return 120 | 121 | if not self.valid: 122 | self.valid = True 123 | self.errlabel.setText('') 124 | 125 | def emit_changed(self, *args): 126 | self.changed.emit() 127 | self.validate() 128 | 129 | def get_property(self, name): 130 | '''Used to get the value of a property of this control.''' 131 | 132 | return self.properties[name] 133 | 134 | def set_property(self, name, value): 135 | '''Used to set the value of a property of this control. Consequently 136 | sets a Qt Property of the same name on all widgets this control 137 | manages. Allowing the use of these properties in stylesheets. 138 | ''' 139 | 140 | for w in self.widgets: 141 | w.setProperty(name, value) 142 | self.properties[name] = value 143 | self.update_style() 144 | 145 | def update_style(self): 146 | '''Used to update the style of all widgets this control manages.''' 147 | 148 | for w in self.widgets: 149 | w.style().unpolish(w) 150 | w.style().polish(w) 151 | 152 | def init_widgets(self): 153 | '''Subclasses must implement this method... 154 | 155 | Used to build the widgets for this control. 156 | Must return a tuple of widgets, where the first element is the main 157 | widget used to parent this control to a layout. Additionally users 158 | must attach their widgets to emit the changed signal of this control 159 | to properly allow for active form validation. 160 | ''' 161 | 162 | raise NotImplementedError() 163 | 164 | def _init_widgets(self): 165 | '''Binds the widgets returned by init_widgets to self.widget and 166 | self.widgets. 167 | ''' 168 | 169 | self.widgets = self.init_widgets() 170 | self.widget = self.widgets[0] 171 | 172 | self.errlabel = QtWidgets.QLabel() 173 | self.errlabel.setFixedHeight(14) 174 | self.errlabel.setProperty('err', True) 175 | self.errlayout = QtWidgets.QHBoxLayout() 176 | self.errlayout.addWidget(self.errlabel) 177 | 178 | self.label = QtWidgets.QLabel(self.name) 179 | if isinstance(self.widget, QtWidgets.QCheckBox): 180 | self.label.setProperty('clickable', True) 181 | 182 | def _mousePressEvent(event): 183 | self.widget.toggle() 184 | self.emit_changed() 185 | 186 | self.label.mousePressEvent = _mousePressEvent 187 | self.errlabel.setAlignment(QtCore.Qt.AlignRight) 188 | 189 | self.widgets = tuple(list(self.widgets) + [self.label]) 190 | 191 | if self.label_on_top: 192 | self.layout = QtWidgets.QBoxLayout( 193 | QtWidgets.QBoxLayout.TopToBottom 194 | ) 195 | elif isinstance(self.widget, QtWidgets.QCheckBox): 196 | self.layout = QtWidgets.QBoxLayout( 197 | QtWidgets.QBoxLayout.RightToLeft 198 | ) 199 | else: 200 | self.label.setAlignment( 201 | QtCore.Qt.AlignRight | 202 | QtCore.Qt.AlignVCenter 203 | ) 204 | self.layout = QtWidgets.QBoxLayout( 205 | QtWidgets.QBoxLayout.LeftToRight 206 | ) 207 | self.layout.setSpacing(10) 208 | 209 | self.grid = QtWidgets.QGridLayout() 210 | self.grid.setContentsMargins(0, 0, 0, 0) 211 | self.grid.setSpacing(10) 212 | self.grid.addWidget(self.widget, 1, 1) 213 | self.layout.setContentsMargins(0, 0, 0, 0) 214 | self.layout.addWidget(self.label) 215 | self.layout.addLayout(self.grid) 216 | self.vlayout = QtWidgets.QVBoxLayout() 217 | self.vlayout.setContentsMargins(0, 0, 0, 0) 218 | self.vlayout.setSpacing(0) 219 | self.vlayout.addStretch() 220 | self.vlayout.addLayout(self.layout) 221 | self.vlayout.addLayout(self.errlayout) 222 | 223 | if not self.labeled: 224 | self.label.hide() 225 | 226 | self.main_widget = QtWidgets.QWidget() 227 | self.main_widget.setLayout(self.vlayout) 228 | 229 | def _init_properties(self): 230 | '''Initializes the qt properties on all this controls widgets.''' 231 | 232 | self.properties = deepcopy(self.properties) 233 | for p, v in self.properties.items(): 234 | self.set_property(p, v) 235 | 236 | def get_value(self): 237 | '''Subclasses must implement this method... 238 | 239 | Must return the value of this control. Use the widgets you create in 240 | init_widgets, accessible through the widgets attribute after 241 | initialization. 242 | ''' 243 | 244 | raise NotImplementedError() 245 | 246 | def set_value(self, value): 247 | '''Subclasses must implement this method... 248 | 249 | Must set the value of this control. Use the widgets you create in 250 | init_widgets, accessible through the widgets attribute after 251 | initialization. 252 | ''' 253 | 254 | raise NotImplementedError() 255 | 256 | 257 | class SpinControl(BaseControl): 258 | 259 | widget_cls = QtWidgets.QSpinBox 260 | 261 | def __init__(self, name, range=None, *args, **kwargs): 262 | self.range = range 263 | super(SpinControl, self).__init__(name, *args, **kwargs) 264 | 265 | def init_widgets(self): 266 | sb = self.widget_cls(parent=self.parent()) 267 | sb.valueChanged.connect(self.emit_changed) 268 | if self.range: 269 | sb.setRange(*self.range) 270 | return (sb,) 271 | 272 | def get_value(self): 273 | return self.widget.value() 274 | 275 | def set_value(self, value): 276 | self.widget.setValue(value) 277 | 278 | 279 | class Spin2Control(BaseControl): 280 | 281 | widget_cls = QtWidgets.QSpinBox 282 | 283 | def __init__(self, name, range1=None, range2=None, *args, **kwargs): 284 | self.range1 = range1 285 | self.range2 = range2 286 | super(Spin2Control, self).__init__(name, *args, **kwargs) 287 | 288 | def init_widgets(self): 289 | 290 | sb1 = self.widget_cls(parent=self.parent()) 291 | sb1.valueChanged.connect(self.emit_changed) 292 | sb2 = self.widget_cls(parent=self.parent()) 293 | sb2.valueChanged.connect(self.emit_changed) 294 | if self.range1: 295 | sb1.setRange(*self.range1) 296 | if self.range2: 297 | sb2.setRange(*self.range2) 298 | 299 | w = QtWidgets.QWidget() 300 | w.setAttribute(QtCore.Qt.WA_StyledBackground, True) 301 | l = QtWidgets.QHBoxLayout() 302 | l.setSpacing(10) 303 | l.setContentsMargins(0, 0, 0, 0) 304 | l.addWidget(sb1) 305 | l.addWidget(sb2) 306 | w.setLayout(l) 307 | 308 | return w, sb1, sb2 309 | 310 | def get_value(self): 311 | return self.widgets[1].value(), self.widgets[2].value() 312 | 313 | def set_value(self, value): 314 | self.widgets[1].setValue(value[0]) 315 | self.widgets[2].setValue(value[1]) 316 | 317 | 318 | class IntControl(SpinControl): 319 | 320 | widget_cls = QtWidgets.QSpinBox 321 | 322 | 323 | class Int2Control(Spin2Control): 324 | 325 | widget_cls = QtWidgets.QSpinBox 326 | 327 | 328 | class FloatControl(SpinControl): 329 | 330 | widget_cls = QtWidgets.QDoubleSpinBox 331 | 332 | 333 | class Float2Control(Spin2Control): 334 | 335 | widget_cls = QtWidgets.QDoubleSpinBox 336 | 337 | 338 | class OptionControl(BaseControl): 339 | 340 | def __init__(self, name, options=None, *args, **kwargs): 341 | self._init_options = options 342 | super(OptionControl, self).__init__(name, *args, **kwargs) 343 | 344 | def init_widgets(self): 345 | c = QtWidgets.QComboBox(parent=self.parent()) 346 | c.activated.connect(self.emit_changed) 347 | if self._init_options: 348 | c.addItems(self._init_options) 349 | return (c,) 350 | 351 | def set_options(self, options): 352 | self.widget.clear() 353 | self.widget.addItems(options) 354 | 355 | def get_data(self): 356 | return self.widget.itemData( 357 | self.widget.currentIndex(), 358 | QtCore.Qt.UserRole 359 | ) 360 | 361 | def get_text(self): 362 | return self.widget.currentText() 363 | 364 | def set_text(self, value): 365 | self.widget.setCurrentIndex(self.widget.findText(value)) 366 | 367 | def get_value(self): 368 | return self.widget.currentText() 369 | 370 | def set_value(self, value): 371 | self.widget.setCurrentIndex(self.widget.findText(value)) 372 | 373 | 374 | StringOptionControl = OptionControl 375 | 376 | 377 | class IntOptionControl(OptionControl): 378 | 379 | def init_widgets(self): 380 | 381 | c = QtWidgets.QComboBox(parent=self.parent()) 382 | c.activated.connect(self.emit_changed) 383 | return (c,) 384 | 385 | def get_value(self): 386 | return self.widget.currentIndex() 387 | 388 | def set_value(self, value): 389 | self.widget.setCurrentIndex(value) 390 | 391 | 392 | class ButtonOptionControl(BaseControl): 393 | 394 | def __init__(self, name, options, *args, **kwargs): 395 | self.options = options 396 | super(ButtonOptionControl, self).__init__(name, *args, **kwargs) 397 | 398 | def init_widgets(self): 399 | w = QtWidgets.QWidget() 400 | w.setAttribute(QtCore.Qt.WA_StyledBackground, True) 401 | l = QtWidgets.QHBoxLayout() 402 | l.setSpacing(20) 403 | w.setLayout(l) 404 | 405 | def group_changed(*args): 406 | self.emit_changed() 407 | 408 | def press_button(index): 409 | def do_press(*args): 410 | self.button_group.button(index).setChecked(True) 411 | self.emit_changed() 412 | return do_press 413 | 414 | self.button_group = QtWidgets.QButtonGroup() 415 | self.button_group.buttonClicked.connect(group_changed) 416 | 417 | for i, opt in enumerate(self.options): 418 | c = QtWidgets.QCheckBox(self.parent()) 419 | if i == 0: 420 | c.setChecked(True) 421 | c.setFixedSize(20, 20) 422 | 423 | cl = QtWidgets.QLabel(opt) 424 | cl.setProperty('clickable', True) 425 | cl.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) 426 | cl.mousePressEvent = press_button(i) 427 | 428 | bl = QtWidgets.QHBoxLayout() 429 | bl.setSpacing(0) 430 | bl.addWidget(c) 431 | bl.addWidget(cl) 432 | self.button_group.addButton(c, i) 433 | l.addLayout(bl) 434 | 435 | return (w,) 436 | 437 | def get_value(self): 438 | return self.options[self.button_group.checkedId()] 439 | 440 | def set_value(self, value): 441 | 442 | index = value if isinstance(value, int) else self.options.index(value) 443 | self.button_group.button(index).setChecked(True) 444 | 445 | 446 | class IntButtonOptionControl(ButtonOptionControl): 447 | 448 | def get_value(self): 449 | return self.button_group.checkedId() 450 | 451 | 452 | class ToggleControl(BaseControl): 453 | 454 | def __init__(self, name, icon, tip=None, *args, **kwargs): 455 | self.icon = icon 456 | self.tip = tip 457 | super(ToggleControl, self).__init__(name, *args, **kwargs) 458 | 459 | def init_widgets(self): 460 | b = IconButton(self.icon, self.tip, 'checkable') 461 | b.setCheckable(True) 462 | b.toggled.connect(self.emit_changed) 463 | return (b,) 464 | 465 | def get_value(self): 466 | return self.widget.isChecked() 467 | 468 | def set_value(self, value): 469 | self.widget.setChecked(value) 470 | 471 | 472 | class BoolControl(BaseControl): 473 | 474 | def init_widgets(self): 475 | c = QtWidgets.QCheckBox(parent=self.parent()) 476 | c.setFixedSize(20, 20) 477 | c.clicked.connect(self.emit_changed) 478 | return (c, ) 479 | 480 | def get_value(self): 481 | return self.widget.isChecked() 482 | 483 | def set_value(self, value): 484 | self.widget.setChecked(value) 485 | 486 | 487 | class StringControl(BaseControl): 488 | 489 | def init_widgets(self): 490 | le = QtWidgets.QLineEdit(parent=self.parent()) 491 | le.textEdited.connect(self.emit_changed) 492 | return (le,) 493 | 494 | def get_value(self): 495 | return self.widget.text() 496 | 497 | def set_value(self, value): 498 | self.widget.setText(value) 499 | 500 | 501 | class InfoControl(BaseControl): 502 | 503 | def __init__(self, name, text=None, align='left', *args, **kwargs): 504 | self.alignment = { 505 | 'left': QtCore.Qt.AlignLeft, 506 | 'center': QtCore.Qt.AlignCenter, 507 | 'left': QtCore.Qt.AlignRight, 508 | }.get(align.lower(), QtCore.Qt.AlignLeft) 509 | 510 | super(InfoControl, self).__init__(name, *args, **kwargs) 511 | 512 | if text: 513 | self.set_value(text) 514 | 515 | 516 | def init_widgets(self): 517 | w = QtWidgets.QLabel(alignment=self.alignment, parent=self.parent()) 518 | return (w,) 519 | 520 | def get_value(self): 521 | return self.widget.text() 522 | 523 | def set_value(self, value): 524 | self.widget.setText(value) 525 | 526 | 527 | class TextControl(BaseControl): 528 | 529 | def init_widgets(self): 530 | te = QtWidgets.QTextEdit(parent=self.parent()) 531 | te.textChanged.connect(self.emit_changed) 532 | return (te,) 533 | 534 | def get_value(self): 535 | return self.widget.toPlainText() 536 | 537 | def set_value(self, value): 538 | self.blockSignals(True) 539 | self.widget.setText(value) 540 | self.blockSignals(False) 541 | 542 | 543 | class BrowseControl(BaseControl): 544 | 545 | browse_method = QtWidgets.QFileDialog.getOpenFileName 546 | 547 | def __init__(self, name, caption=None, filters=None, *args, **kwargs): 548 | super(BrowseControl, self).__init__(name, *args, **kwargs) 549 | self.caption = caption or name 550 | self.filters = filters or ["Any files (*)"] 551 | 552 | def init_widgets(self): 553 | 554 | le = QtWidgets.QLineEdit(parent=self.parent()) 555 | le.setProperty('browse', True) 556 | le.textEdited.connect(self.emit_changed) 557 | b = IconButton( 558 | icon=':/icons/browse_hover', 559 | tip='Browse', 560 | name='browse_button', 561 | ) 562 | b.setProperty('browse', True) 563 | b.clicked.connect(self.browse) 564 | 565 | w = QtWidgets.QWidget(parent=self.parent()) 566 | l = QtWidgets.QGridLayout() 567 | l.setContentsMargins(0, 0, 0, 0) 568 | l.setSpacing(0) 569 | l.setColumnStretch(0, 1) 570 | l.addWidget(le, 0, 0) 571 | l.addWidget(b, 0, 1) 572 | w.setLayout(l) 573 | 574 | return (w, le, b) 575 | 576 | def get_value(self): 577 | return self.widgets[1].text() 578 | 579 | def set_value(self, value): 580 | self.widgets[1].setText(value) 581 | 582 | @property 583 | def basedir(self): 584 | line_text = self.get_value() 585 | if line_text: 586 | line_dir = os.path.dirname(line_text) 587 | if os.path.exists(line_dir): 588 | return line_dir 589 | return '' 590 | 591 | def browse(self): 592 | value = self.browse_method( 593 | self.main_widget, 594 | caption=self.caption, 595 | dir=self.basedir) 596 | if value: 597 | if self.browse_method is QtWidgets.QFileDialog.getExistingDirectory: 598 | self.set_value(value) 599 | return 600 | self.set_value(value[0]) 601 | 602 | 603 | class FileControl(BrowseControl): 604 | 605 | browse_method = QtWidgets.QFileDialog.getOpenFileName 606 | 607 | 608 | class FolderControl(BrowseControl): 609 | 610 | browse_method = QtWidgets.QFileDialog.getExistingDirectory 611 | 612 | 613 | class SaveFileControl(BrowseControl): 614 | 615 | browse_method = QtWidgets.QFileDialog.getSaveFileName 616 | 617 | 618 | class ImageControl(BaseControl): 619 | 620 | def init_widgets(self): 621 | w = QtWidgets.QWidget(parent=self.parent()) 622 | i = ScalingImage(parent=w) 623 | f = FileControl(self.name + '_line', parent=w) 624 | f.changed.connect(self.emit_changed) 625 | self.file_control = f 626 | 627 | l = QtWidgets.QVBoxLayout() 628 | l.setContentsMargins(0, 0, 0, 0) 629 | l.setSpacing(10) 630 | l.addWidget(i) 631 | l.addWidget(f.main_widget) 632 | w.setLayout(l) 633 | 634 | return [w, i] + list(f.widgets) 635 | 636 | def emit_changed(self, *args): 637 | self.changed.emit() 638 | self.widgets[1].set_image(self.get_value()) 639 | 640 | def get_value(self): 641 | return self.file_control.get_value() 642 | 643 | def set_value(self, value): 644 | if QtCore.QFile.exists(value): 645 | self.file_control.set_value(value) 646 | 647 | 648 | class ListControl(BaseControl): 649 | 650 | def __init__(self, name, options=None, *args, **kwargs): 651 | super(ListControl, self).__init__(name, *args, **kwargs) 652 | if options: 653 | self.widget.addItems(options) 654 | 655 | def init_widgets(self): 656 | l = QtWidgets.QListWidget() 657 | l.itemSelectionChanged.connect(self.emit_changed) 658 | return (l,) 659 | 660 | def add_item(self, label, icon=None, data=None): 661 | item_widget = QtWidgets.QListWidgetItem() 662 | if icon: 663 | item_widget.setIcon(QtGui.QIcon(icon)) 664 | if data: 665 | item_widget.setData(QtCore.Qt.UserRole, data) 666 | self.widget.addItem(item_widget) 667 | 668 | def get_data(self): 669 | ''':return: Data for selected items in :class:`QtWidgets.QListWidget` 670 | :rtype: list''' 671 | items = self.widget.selectedItems() 672 | items_data = [] 673 | for item in items: 674 | items_data.append(item.data(QtCore.Qt.UserRole)) 675 | return items_data 676 | 677 | def get_value(self): 678 | ''':return: Value of the underlying :class:`QtWidgets.QTreeWidget` 679 | :rtype: str''' 680 | 681 | items = self.widget.selectedItems() 682 | item_values = [] 683 | for item in items: 684 | item_values.append(item.text()) 685 | return item_values 686 | 687 | def set_value(self, value): 688 | '''Sets the selection of the list to the specified value, label or 689 | index''' 690 | 691 | if isinstance(value, (str, unicode)): 692 | items = self.widget.findItems(value) 693 | if items: 694 | self.widget.setCurrentItem(items[0]) 695 | elif isinstance(value, int): 696 | self.widget.setCurrentIndex(int) 697 | 698 | 699 | control_map = {cls.__name__: cls for cls in BaseControl.__subclasses__()} 700 | -------------------------------------------------------------------------------- /mvp/vendor/psforms/exc.py: -------------------------------------------------------------------------------- 1 | class FormNotInstantiated(Exception): 2 | pass 3 | 4 | class FieldNotInstantiated(Exception): 5 | pass 6 | 7 | class FieldNotFound(Exception): 8 | pass 9 | 10 | class FormNotFound(Exception): 11 | pass 12 | 13 | class ValidationError(Exception): 14 | pass 15 | -------------------------------------------------------------------------------- /mvp/vendor/psforms/fields.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .exc import FieldNotInstantiated 3 | from . import controls 4 | from .utils import Ordered 5 | from .validators import required 6 | from copy import deepcopy 7 | 8 | 9 | missing = object() 10 | 11 | 12 | def get_key(*args): 13 | '''Like dict.get but looks up keys in multiple dicts. 14 | 15 | :param key: Key to lookup 16 | :param dicts: Tuple of dicts to use in lookup 17 | :param default: If passed return this if lookup fails 18 | ''' 19 | 20 | arglen = len(args) 21 | if arglen not in (2, 3): 22 | raise ValueError('Must pass at least two args...key and (dicts...)') 23 | elif arglen == 2: 24 | key, dicts = args 25 | else: 26 | key, dicts, default = args 27 | 28 | for d in dicts: 29 | value = d.get(key, missing) 30 | if value is not missing: 31 | return value 32 | 33 | if 'default' in locals(): 34 | return default 35 | 36 | raise KeyError('{0} does not exist in {1}'.format(key, dicts)) 37 | 38 | 39 | class FieldType(Ordered): 40 | ''':class:`Form` calls the :meth:`create` to retrieve an appropriate 41 | control. 42 | 43 | :param nice_name: Nice nice_name of the field (str) 44 | :param labeled: Field Control has label (bool) 45 | Overrides the parent Forms labeled attribute for this field only 46 | :param label_on_top: Label appears on top of the field control (bool) 47 | Overrides the parent Forms label_on_top attribute for this field only 48 | :param default: Default value (str) 49 | ''' 50 | 51 | control_cls = None 52 | control_defaults = None 53 | field_defaults = { 54 | 'labeled': True, 55 | 'label_on_top': True, 56 | 'default': None, 57 | 'validators': None 58 | } 59 | field_keys = ('labeled', 'label_on_top', 'default', 'validators') 60 | 61 | def __init__(self, nice_name, **kwargs): 62 | super(FieldType, self).__init__() 63 | 64 | self.nice_name = nice_name 65 | self.control_kwargs = {'name': nice_name} 66 | 67 | # Convert required keyword to required validator 68 | validators = list(kwargs.pop('validators', [])) 69 | is_required = kwargs.pop('required', False) 70 | if is_required: 71 | validators.append(required) 72 | kwargs['validators'] = validators 73 | 74 | # Set instance attributes from field keys 75 | for key in self.field_keys: 76 | set_field_attribute = key in kwargs 77 | value = kwargs.pop(key, self.field_defaults[key]) 78 | self.control_kwargs[key] = value 79 | 80 | if set_field_attribute: 81 | setattr(self, key, value) 82 | else: 83 | setattr(self, key, None) 84 | 85 | if self.control_defaults: # If the control has defaults, get em 86 | for key in self.control_defaults.keys(): 87 | value = kwargs.get(key, self.control_defaults[key]) 88 | self.control_kwargs[key] = value 89 | 90 | def __repr__(self): 91 | r = '<{}>(nice_name={}, default={})' 92 | return r.format(self.__class__.__name__, self.nice_name, self.default) 93 | 94 | def create(self): 95 | return self.control_cls(**self.control_kwargs) 96 | 97 | 98 | def create_fieldtype(clsname, control_cls, control_defaults=None, 99 | field_defaults=None, bases=(FieldType,)): 100 | '''Convenience function to create a new subclass of :class:`FieldType`. 101 | *control_defaults* are passed on to *control_cls*. *field_defaults* are 102 | used as the values of attributes on the returned :class:`FieldType` 103 | subclass. The keys for both control_defaults and field_defaults are looked 104 | up in __init__ kwargs param first. 105 | 106 | :param control_cls: PySide widget used to create the control. 107 | :param control_defaults: Default kwargs to pass to control_cls 108 | :param field_defaults: Default attr values (labeled, label_on_top, default) 109 | 110 | .. note:: 111 | 112 | *control_cls* must implement the :class:`ControlType` interface 113 | ''' 114 | 115 | bdicts = [b.__dict__ for b in bases] 116 | orig_defaults = get_key('field_defaults', bdicts) 117 | 118 | if field_defaults: 119 | defaults = deepcopy(orig_defaults) 120 | defaults.update(field_defaults) 121 | field_defaults = defaults 122 | else: 123 | field_defaults = orig_defaults 124 | 125 | attrs = { 126 | 'control_cls': control_cls, 127 | 'control_defaults': control_defaults, 128 | 'field_defaults': field_defaults, 129 | } 130 | return type(clsname, bases, attrs) 131 | 132 | 133 | ListField = create_fieldtype( 134 | 'ListField', 135 | control_cls=controls.ListControl, 136 | control_defaults={'options': None} 137 | ) 138 | 139 | BoolField = create_fieldtype( 140 | 'BoolField', 141 | control_cls=controls.BoolControl, 142 | field_defaults={'label_on_top': False} 143 | ) 144 | 145 | StringField = create_fieldtype( 146 | 'StringField', 147 | control_cls=controls.StringControl, 148 | ) 149 | 150 | IntField = create_fieldtype( 151 | 'IntField', 152 | control_cls=controls.IntControl, 153 | control_defaults={'range': None}, 154 | ) 155 | 156 | FloatField = create_fieldtype( 157 | 'FloatField', 158 | control_cls=controls.FloatControl, 159 | control_defaults={'range': None}, 160 | ) 161 | 162 | Int2Field = create_fieldtype( 163 | 'Int2Field', 164 | control_cls=controls.Int2Control, 165 | control_defaults={'range1': None, 'range2': None}, 166 | ) 167 | 168 | Float2Field = create_fieldtype( 169 | 'Float2Field', 170 | control_cls=controls.Float2Control, 171 | control_defaults={'range1': None, 'range2': None}, 172 | ) 173 | 174 | IntOptionField = create_fieldtype( 175 | 'IntOptionField', 176 | control_cls=controls.IntOptionControl, 177 | control_defaults={'options': None}, 178 | ) 179 | 180 | StringOptionField = create_fieldtype( 181 | 'StringOptionField', 182 | control_cls=controls.StringOptionControl, 183 | control_defaults={'options': None}, 184 | ) 185 | 186 | ButtonOptionField = create_fieldtype( 187 | 'ButtonOptionField', 188 | control_cls=controls.ButtonOptionControl, 189 | control_defaults={'options': None}, 190 | ) 191 | 192 | IntButtonOptionField = create_fieldtype( 193 | 'IntButtonOptionField', 194 | control_cls=controls.IntButtonOptionControl, 195 | control_defaults={'options': None}, 196 | ) 197 | 198 | FileField = create_fieldtype( 199 | 'FileField', 200 | control_cls=controls.FileControl, 201 | control_defaults={'caption': None, 'filters': None}, 202 | ) 203 | 204 | FolderField = create_fieldtype( 205 | 'FolderField', 206 | control_cls=controls.FolderControl, 207 | control_defaults={'caption': None, 'filters': None}, 208 | ) 209 | 210 | SaveFileField = create_fieldtype( 211 | 'SaveFileField', 212 | control_cls=controls.SaveFileControl, 213 | control_defaults={'caption': None, 'filters': None}, 214 | ) 215 | 216 | ImageField = create_fieldtype( 217 | 'ImageField', 218 | control_cls=controls.ImageControl, 219 | ) 220 | 221 | TextField = create_fieldtype( 222 | 'TextField', 223 | control_cls=controls.TextControl, 224 | ) 225 | InfoField = create_fieldtype( 226 | 'InfoField', 227 | control_cls=controls.InfoControl, 228 | control_defaults={'text': None, 'align': 'left'}, 229 | field_defaults={'labeled': False}, 230 | ) 231 | 232 | 233 | field_map = {cls.__name__: cls for cls in FieldType.__subclasses__()} 234 | type_map = { 235 | 'image': ImageField, 236 | 'folder': FolderField, 237 | 'text': TextField, 238 | 'file': FileField, 239 | 'str': StringField, 240 | '(bool,)': ButtonOptionField, 241 | '(int,)': IntOptionField, 242 | '(str,)': StringOptionField, 243 | 'int': IntField, 244 | 'float': FloatField, 245 | '(int, int)': Int2Field, 246 | '(float, float)': Float2Field, 247 | 'bool': BoolField, 248 | 'list': ListField, 249 | 'info': InfoField, 250 | str: StringField, 251 | (bool,): ButtonOptionField, 252 | (int,): IntOptionField, 253 | (str,): StringOptionField, 254 | int: IntField, 255 | float: FloatField, 256 | (int, int): Int2Field, 257 | (float, float): Float2Field, 258 | bool: BoolField, 259 | list: ListField, 260 | } 261 | -------------------------------------------------------------------------------- /mvp/vendor/psforms/form.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | try: 3 | from collections import OrderedDict 4 | except ImportError: 5 | from ordereddict import OrderedDict 6 | from .Qt import QtWidgets, QtCore, QtGui 7 | 8 | from .fields import FieldType, type_map, field_map 9 | from .widgets import FormDialog, FormWidget, FormGroup 10 | from .utils import Ordered, itemattrgetter 11 | 12 | 13 | class FormMetaData(object): 14 | 15 | defaults = dict( 16 | title='No Title', 17 | description=None, 18 | icon=None, 19 | header=False, 20 | columns=1, 21 | labeled=True, 22 | labels_on_top=True, 23 | layout_horizontal=False, 24 | subforms_as_groups=False, 25 | ) 26 | 27 | def __init__(self, **kwargs): 28 | self.__dict__.update(self.defaults) 29 | self.__dict__.update(kwargs) 30 | 31 | 32 | class Form(Ordered): 33 | 34 | meta = FormMetaData() 35 | _max_width = None 36 | 37 | @classmethod 38 | def fields(cls): 39 | '''Returns FieldType objects in sorted order''' 40 | 41 | cls_fields = [] 42 | for name, attr in cls.__dict__.items(): 43 | if issubclass(attr.__class__, FieldType): 44 | cls_fields.append((name, attr)) 45 | return sorted(cls_fields, key=itemattrgetter(1, '_order')) 46 | 47 | @classmethod 48 | def forms(cls): 49 | '''Returns Form objects in sorted order''' 50 | 51 | cls_forms = [] 52 | for name, attr in cls.__dict__.items(): 53 | if issubclass(attr.__class__, Form): 54 | cls_forms.append((name, attr)) 55 | return sorted(cls_forms, key=itemattrgetter(1, '_order')) 56 | 57 | @classmethod 58 | def max_width(cls): 59 | if not cls._max_width: 60 | # Get the width of the maximum length label 61 | _max_label = max([y.nice_name for x, y in cls.fields()], key=len) 62 | _label = QtWidgets.QLabel(_max_label) 63 | cls._max_width = _label.sizeHint().width() + 20 64 | return cls._max_width 65 | 66 | @classmethod 67 | def _create_controls(cls): 68 | '''Create and return controls from Field objects.''' 69 | 70 | controls = OrderedDict() 71 | 72 | for name, field in cls.fields(): 73 | control = field.create() 74 | control.setObjectName(name) 75 | if field.labeled is None: 76 | control.labeled = cls.meta.labeled 77 | if field.label_on_top is None: 78 | control.label_on_top = cls.meta.labels_on_top 79 | control.label.setFixedWidth(cls.max_width()) 80 | if not control.label_on_top: 81 | control.errlayout.insertSpacing(0, cls.max_width() + 12) 82 | controls[name] = control 83 | 84 | return controls 85 | 86 | @classmethod 87 | def as_widget(cls, parent=None): 88 | '''Get this form as a widget''' 89 | 90 | form_widget = FormWidget( 91 | cls.meta.title, 92 | cls.meta.columns, 93 | cls.meta.layout_horizontal, 94 | parent=parent) 95 | 96 | if cls.meta.header: 97 | form_widget.add_header( 98 | cls.meta.title, 99 | cls.meta.description, 100 | cls.meta.icon 101 | ) 102 | 103 | if cls.fields(): 104 | controls = cls._create_controls() 105 | for name, control in controls.items(): 106 | form_widget.add_control(name, control) 107 | 108 | for name, form in cls.forms(): 109 | if cls.meta.subforms_as_groups: 110 | form_widget.add_form(name, form.as_group(form_widget)) 111 | else: 112 | form_widget.add_form(name, form.as_widget(form_widget)) 113 | 114 | return form_widget 115 | 116 | @classmethod 117 | def as_group(cls, parent=None): 118 | 119 | widget = cls.as_widget() 120 | group = FormGroup(widget.name, widget, parent=parent) 121 | return group 122 | 123 | @classmethod 124 | def as_dialog(cls, frameless=False, dim=False, parent=None): 125 | '''Get this form as a dialog''' 126 | 127 | dialog = FormDialog(cls.as_widget(), parent=parent) 128 | dialog.setWindowTitle(cls.meta.title) 129 | dialog.setWindowIcon(QtGui.QIcon(cls.meta.icon)) 130 | 131 | window_flags = dialog.windowFlags() 132 | if frameless: 133 | window_flags |= QtCore.Qt.FramelessWindowHint 134 | dialog.setWindowFlags(window_flags) 135 | 136 | if dim: # Dim all monitors when showing the dialog 137 | def _bg_widgets(): 138 | qapp = QtWidgets.QApplication.instance() 139 | desktop = qapp.desktop() 140 | screens = desktop.screenCount() 141 | widgets = [] 142 | 143 | for i in range(screens): 144 | geo = desktop.screenGeometry(i) 145 | w = QtWidgets.QWidget() 146 | w.setGeometry(geo) 147 | w.setStyleSheet('QWidget {background:black}') 148 | w.setWindowOpacity(0.3) 149 | widgets.append(w) 150 | 151 | def show(): 152 | for w in widgets: 153 | w.show() 154 | 155 | def hide(): 156 | for w in widgets: 157 | w.hide() 158 | 159 | return show, hide 160 | 161 | old_exec = dialog.exec_ 162 | old_show = dialog.show 163 | 164 | def _exec_(*args, **kwargs): 165 | bgshow, bghide = _bg_widgets() 166 | bgshow() 167 | result = old_exec(*args, **kwargs) 168 | bghide() 169 | return result 170 | 171 | def _show(*args, **kwargs): 172 | bgshow, bghide = _bg_widgets() 173 | bgshow() 174 | result = old_show(*args, **kwargs) 175 | bghide() 176 | return result 177 | 178 | dialog.exec_ = _exec_ 179 | dialog.show = _show 180 | 181 | return dialog 182 | 183 | 184 | def generate_form(name, fields, **metadata): 185 | '''Generate a form from a name and a list of fields.''' 186 | 187 | metadata.setdefault('title', name.title()) 188 | 189 | bases = (Form,) 190 | attrs = {'meta': FormMetaData(**metadata)} 191 | for field in fields: 192 | if field['type'] not in type_map: 193 | raise Exception('Invalid field type %s', field['type']) 194 | 195 | field_type = type_map[field['type']] 196 | name = field.pop('name') 197 | label = field.pop('label', name) 198 | form_field = field_type(label, **field) 199 | attrs[name] = form_field 200 | 201 | return type(name, bases, attrs) 202 | -------------------------------------------------------------------------------- /mvp/vendor/psforms/style.css: -------------------------------------------------------------------------------- 1 | QDialog, 2 | QWidget[form='true'] { 3 | background: rgb(235, 235, 235); 4 | border: 0; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | QWidget[header='true'] { 9 | background: rgb(45, 45, 45); 10 | border: 0; 11 | margin: 0; 12 | padding: 0; 13 | } 14 | QLabel[title='true'] { 15 | color: white; 16 | font: 24pt "Arial"; 17 | } 18 | QLabel[description='true'] { 19 | color: white; 20 | font: 12pt "Arial"; 21 | } 22 | QLabel{ 23 | color: rgb(45, 45, 45); 24 | font: 10pt "Arial"; 25 | } 26 | QLabel[valid='false']{ 27 | color: rgb(203, 40, 40); 28 | } 29 | QLabel[err='true']{ 30 | font: 8pt "Arial"; 31 | color: rgb(203, 40, 40); 32 | } 33 | QLabel[clickable='true']:hover{ 34 | color: rgb(135, 135, 135); 35 | font: 10pt "Arial"; 36 | } 37 | 38 | QPushButton{ 39 | background: rgb(67, 203, 142); 40 | border-radius: 3; 41 | border: 0; 42 | color: rgb(255, 255, 255); 43 | font: 10pt "Arial"; 44 | height: 24px; 45 | padding: 0 10 0 10; 46 | } 47 | 48 | QPushButton[browse='true']{ 49 | border-top-left-radius: 0; 50 | border-bottom-left-radius: 0; 51 | } 52 | 53 | 54 | QPushButton:hover{ 55 | background: rgb(75, 229, 160); 56 | } 57 | 58 | QPushButton[flat='true']{ 59 | background: rgb(0, 0, 0, 0); 60 | border: 0; 61 | } 62 | 63 | QPushButton:pressed{ 64 | border-bottom: 0px; 65 | margin-top: 3; 66 | } 67 | 68 | QPushButton[grouptitle='true']{ 69 | background: rgb(235, 235, 235); 70 | color: rgb(45, 45, 45); 71 | border: 0; 72 | margin: 0; 73 | padding: 0; 74 | padding-left: 10px; 75 | text-align: left; 76 | } 77 | 78 | QPushButton[grouptitle='true']:hover{ 79 | background: rgb(215, 215, 215); 80 | border-radius: 0; 81 | } 82 | 83 | QPushButton[grouptitle='true']:pressed{} 84 | 85 | QWidget[groupwidget='true']{ 86 | background: rgb(215, 215, 215); 87 | } 88 | 89 | QComboBox { 90 | background: rgb(255, 255, 255); 91 | border-radius: 3; 92 | border: 1px solid rgb(185, 185, 185); 93 | color: rgb(45, 45, 45); 94 | font: 10pt "Arial"; 95 | outline: none; 96 | padding-left: 13; 97 | height: 24px; 98 | } 99 | 100 | QComboBox:focus { 101 | border: 1px solid rgb(67, 203, 142); 102 | } 103 | 104 | QComboBox[valid='false'] { 105 | border: 1px solid rgb(203, 40, 40); 106 | } 107 | 108 | QComboBox::drop-down{ 109 | background: rgb(255, 255, 255, 0); 110 | border-bottom: 0px solid rgb(235,235,235, 0); 111 | border-left: 5px solid rgb(255,255,255,0); 112 | border-right: 5px solid rgb(255,255,255,0); 113 | border-top: 5px solid rgb(185,185,185); 114 | margin-bottom: -1px; 115 | margin-left: 3px; 116 | margin-right: 3px; 117 | margin-top: 12px; 118 | } 119 | 120 | QComboBox QAbstractItemView{ 121 | background-color: rgb(185, 185, 185); 122 | border: none; 123 | color: rgb(110, 110, 110); 124 | outline: none; 125 | selection-background-color: rgb(255, 255, 255); 126 | selection-color: rgb(110, 110, 110); 127 | } 128 | 129 | QCheckBox{ 130 | background: rgb(255, 255, 255); 131 | border-radius: 3; 132 | border: 1 solid rgb(185, 185, 185); 133 | color: rgb(235, 235, 235); 134 | height: 20; 135 | width: 20; 136 | } 137 | 138 | QCheckBox::indicator{ 139 | background: rgb(255, 255, 255, 0); 140 | border-radius: 3; 141 | height: 10; 142 | left: 4; 143 | width: 10; 144 | } 145 | 146 | QCheckBox::indicator:checked{ 147 | background: rgb(67, 203, 142); 148 | } 149 | 150 | QStatusBar{ 151 | background: rgb(185, 185, 185) 152 | } 153 | 154 | QSpinBox, 155 | QDoubleSpinBox { 156 | background: rgb(255, 255, 255); 157 | border-radius: 3; 158 | border: 1px solid rgb(185, 185, 185); 159 | color: rgb(45, 45, 45); 160 | font: 10pt "Arial"; 161 | padding-left: 10; 162 | } 163 | 164 | QSpinBox:focus, 165 | QDoubleSpinBox:focus { 166 | background: rgb(255, 255, 255); 167 | border-radius: 3; 168 | border: 1px solid rgb(67, 203, 142); 169 | color: rgb(45, 45, 45); 170 | font: 10pt "Arial"; 171 | padding-left: 10; 172 | } 173 | 174 | QSpinBox[valid='false'], 175 | QDoubleSpinBox[valid='false'] { 176 | border: 1px solid rgb(203, 40, 40); 177 | } 178 | 179 | 180 | QSpinBox[valid='false']:focus, 181 | QDoubleSpinBox[valid='false']:focus { 182 | border: 1px solid rgb(203, 40, 40); 183 | } 184 | 185 | QSpinBox:disabled, 186 | QDoubleSpinBox:disabled { 187 | background: rgb(55, 55, 55); 188 | color: rgb(235, 235, 235); 189 | font: 10pt "Arial"; 190 | padding-left: 10; 191 | } 192 | 193 | QSpinBox::up-button, 194 | QDoubleSpinBox::up-button { 195 | background: rgb(255, 255, 255, 0); 196 | border-bottom: 5px solid rgb(185,185,185); 197 | border-left: 5px solid rgb(255,255,255,0); 198 | border-right: 5px solid rgb(255,255,255,0); 199 | border-top: 0px solid rgb(235,235,235, 0); 200 | margin-bottom: 2px; 201 | margin-left: 3px; 202 | margin-right: 3px; 203 | margin-top: -2px; 204 | subcontrol-origin: border; 205 | subcontrol-position: top right; 206 | } 207 | 208 | QSpinBox::down-button, 209 | QDoubleSpinBox::down-button { 210 | background: rgb(255, 255, 255, 0); 211 | border-bottom: 0px solid rgb(235,235,235, 0); 212 | border-left: 5px solid rgb(255,255,255,0); 213 | border-right: 5px solid rgb(255,255,255,0); 214 | border-top: 5px solid rgb(185,185,185); 215 | margin-bottom: -2px; 216 | margin-left: 3px; 217 | margin-right: 3px; 218 | margin-top: 2px; 219 | subcontrol-origin: border; 220 | subcontrol-position: bottom right; 221 | } 222 | 223 | QSpinBox::up-button:disabled, 224 | QDoubleSpinBox::up-button:disabled { 225 | background: rgb(55, 55, 55); 226 | } 227 | 228 | QSpinBox::down-button:disabled, 229 | QDoubleSpinBox::down-button:disabled { 230 | background: rgb(55, 55, 55); 231 | } 232 | 233 | QLineEdit{ 234 | background: rgb(255, 255, 255); 235 | border-radius: 3; 236 | border: 1px solid rgb(185, 185, 185); 237 | color: rgb(45, 45, 45); 238 | font: 10pt "Arial"; 239 | height: 24px; 240 | padding-left: 10; 241 | } 242 | 243 | QLineEdit:focus{ 244 | background: rgb(255, 255, 255); 245 | border-radius: 3; 246 | border: 1px solid rgb(67, 203, 142); 247 | color: rgb(45, 45, 45); 248 | font: 10pt "Arial"; 249 | height: 24px; 250 | padding-left: 10; 251 | } 252 | 253 | QLineEdit[browse='true']{ 254 | border-right: 0; 255 | border-top-right-radius: 0; 256 | border-bottom-right-radius: 0; 257 | } 258 | 259 | QLineEdit[valid='false']{ 260 | border: 1px solid rgb(203, 40, 40); 261 | } 262 | 263 | QLineEdit[valid='false']:focus{ 264 | border: 1px solid rgb(203, 40, 40); 265 | } 266 | 267 | QLineEdit:disabled{ 268 | background: rgb(55, 55, 55); 269 | border-top: 0; 270 | color: rgb(200, 200, 200); 271 | font: 10pt "Arial"; 272 | height: 24px; 273 | padding: 0; 274 | } 275 | 276 | QGroupBox{ 277 | border: 1px solid rgb(45, 45, 45); 278 | color: rgb(235,235,235); 279 | font: 12pt "Arial"; 280 | margin-top: 1ex; 281 | } 282 | 283 | QGroupBox::title{ 284 | padding: 0 3px; 285 | subcontrol-origin: margin; 286 | subcontrol-position: top left; 287 | } 288 | 289 | QGroupBox::title:hover{ 290 | color: white; 291 | padding: 0 3px; 292 | subcontrol-origin: margin; 293 | subcontrol-position: top left; 294 | } 295 | 296 | QGroupBox[unfolded='false']::indicator { 297 | height: 12px; 298 | image: url(:icons/plus); 299 | width: 12px; 300 | } 301 | 302 | QGroupBox[unfolded='false']::indicator::hover { 303 | height: 12px; 304 | image: url(:icons/plus_hover); 305 | width: 12px; 306 | } 307 | 308 | QGroupBox[unfolded='true']::indicator { 309 | height: 12px; 310 | image: url(:icons/minus); 311 | width: 12px; 312 | } 313 | 314 | QGroupBox[unfolded='true']::indicator::hover { 315 | height: 12px; 316 | image: url(:icons/minus_hover); 317 | width: 12px; 318 | } 319 | 320 | QListView { 321 | background-color: rgb(255, 255, 255); 322 | color: rgb(45, 45, 45); 323 | border: 1px solid rgb(185, 185, 185); 324 | border-radius: 3; 325 | padding: 10px; 326 | show-decoration-selected: 1; 327 | } 328 | 329 | QListView::item:selected, 330 | QListView::item:hover { 331 | background-color: rgb(135, 135, 135); 332 | color: rgb(255, 255, 255); 333 | } 334 | 335 | QScrollBar:vertical { 336 | border: 0; 337 | background: rgb(255, 255, 255); 338 | width: 16px; 339 | padding: 4px; 340 | margin: 22px 0px 22px 0px; 341 | } 342 | QScrollBar::handle:vertical { 343 | background: rgb(185, 185, 185); 344 | border: 0px solid rgb(255, 255, 255); 345 | min-height: 20px; 346 | } 347 | QScrollBar::add-line:vertical { 348 | border: 0px solid rgb(185, 185, 185); 349 | background: rgb(255, 255, 255); 350 | height: 10; 351 | subcontrol-position: bottom; 352 | subcontrol-origin: margin; 353 | } 354 | 355 | QScrollBar::sub-line:vertical { 356 | border: 0; 357 | background: rgb(255, 255, 255); 358 | height: 10; 359 | subcontrol-position: top; 360 | subcontrol-origin: margin; 361 | } 362 | QScrollBar::up-arrow:vertical{ 363 | background: rgb(255, 255, 255, 0); 364 | border-bottom: 5px solid rgb(185,185,185); 365 | border-left: 5px solid rgb(255,255,255,0); 366 | border-right: 5px solid rgb(255,255,255,0); 367 | border-top: 0px solid rgb(235,235,235, 0); 368 | margin-bottom: 2px; 369 | margin-left: 3px; 370 | margin-right: 3px; 371 | margin-top: -2px; 372 | subcontrol-origin: border; 373 | subcontrol-position: top right; 374 | } 375 | 376 | QScrollBar::down-arrow:vertical { 377 | background: rgb(255, 255, 255, 0); 378 | border-bottom: 0px solid rgb(235,235,235, 0); 379 | border-left: 5px solid rgb(255,255,255,0); 380 | border-right: 5px solid rgb(255,255,255,0); 381 | border-top: 5px solid rgb(185,185,185); 382 | margin-bottom: -2px; 383 | margin-left: 3px; 384 | margin-right: 3px; 385 | margin-top: 2px; 386 | subcontrol-origin: border; 387 | subcontrol-position: bottom right; 388 | } 389 | 390 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { 391 | background: none; 392 | } 393 | 394 | 395 | QScrollBar:horizontal { 396 | border: 0; 397 | background: rgb(255, 255, 255); 398 | width: 16px; 399 | padding: 4px; 400 | margin: 0px 22px 0px 22px; 401 | } 402 | QScrollBar::handle:horizontal { 403 | background: rgb(185, 185, 185); 404 | border: 0px solid rgb(255, 255, 255); 405 | min-width: 20px; 406 | } 407 | QScrollBar::add-line:horizontal { 408 | border: 0px solid rgb(185, 185, 185); 409 | background: rgb(255, 255, 255); 410 | width: 10; 411 | subcontrol-position: left; 412 | subcontrol-origin: margin; 413 | } 414 | 415 | QScrollBar::sub-line:horizontal { 416 | border: 0; 417 | background: rgb(255, 255, 255); 418 | width: 10; 419 | subcontrol-position: right; 420 | subcontrol-origin: margin; 421 | } 422 | QScrollBar::left-arrow:horizontal{ 423 | background: rgb(255, 255, 255, 0); 424 | border-right: 5px solid rgb(185,185,185); 425 | border-left: 5px solid rgb(255,255,255,0); 426 | border-bottom: 5px solid rgb(255,255,255,0); 427 | border-top: 0px solid rgb(235,235,235, 0); 428 | margin-right: 2px; 429 | margin-left: 3px; 430 | margin-bottom: 3px; 431 | margin-top: -2px; 432 | subcontrol-origin: border; 433 | subcontrol-position: bottom left; 434 | } 435 | 436 | QScrollBar::right-arrow:horizontal { 437 | background: rgb(255, 255, 255, 0); 438 | border-left: 0px solid rgb(235,235,235, 0); 439 | border-bottom: 5px solid rgb(255,255,255,0); 440 | border-right: 5px solid rgb(255,255,255,0); 441 | border-top: 5px solid rgb(185,185,185); 442 | margin-left: -2px; 443 | margin-bottom: 3px; 444 | margin-right: 3px; 445 | margin-top: 2px; 446 | subcontrol-origin: border; 447 | subcontrol-position: bottom right; 448 | } 449 | 450 | QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { 451 | background: none; 452 | } 453 | -------------------------------------------------------------------------------- /mvp/vendor/psforms/utils.py: -------------------------------------------------------------------------------- 1 | ''' 2 | General purpose classes and functions. 3 | ''' 4 | 5 | 6 | def itemattrgetter(index, attr): 7 | '''Returns a function which gets an object in a sequence, then looks up 8 | an attribute on that object and returns its value.''' 9 | 10 | def get_it(obj): 11 | return getattr(obj[index], attr) 12 | 13 | return get_it 14 | 15 | 16 | class Ordered(object): 17 | '''Maintains the order of creation for all instances/subclasses''' 18 | 19 | _count = 0 20 | 21 | def __init__(self): 22 | Ordered._count += 1 23 | self._order = self._count 24 | -------------------------------------------------------------------------------- /mvp/vendor/psforms/validators.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Standard Validators 3 | =================== 4 | ''' 5 | import re 6 | from .exc import ValidationError 7 | 8 | 9 | def regex(rstr, msg='Does not match regex'): 10 | r = re.compile(rstr) 11 | def check_string(value): 12 | match = r.search(value) 13 | if not match: 14 | raise ValidationError(msg) 15 | return True 16 | return check_string 17 | 18 | 19 | def checked(value): 20 | if not value: 21 | raise ValidationError('Must be checked') 22 | return True 23 | 24 | 25 | def email(value): 26 | r = re.compile(r'^[\w\d!#$%^&*(){\-_}|]+@[\w\d\-_]+[.][a-z]{2,4}') 27 | match = r.search(value) 28 | if not match: 29 | raise ValidationError('Not a valid email address') 30 | return True 31 | 32 | 33 | def required(value): 34 | if not bool(value): 35 | raise ValidationError('Missing required field') 36 | return True 37 | 38 | 39 | def min_length(num_characters): 40 | 41 | def check_length(value): 42 | if len(value) < num_characters: 43 | raise ValidationError('Min Length {0}'.format(num_characters)) 44 | return check_length 45 | 46 | 47 | def max_length(num_characters): 48 | 49 | def check_length(value): 50 | if len(value) > num_characters: 51 | raise ValidationError('Max Length {0}'.format(num_characters)) 52 | return check_length 53 | 54 | -------------------------------------------------------------------------------- /mvp/vendor/psforms/widgets.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from . import resource 4 | from .exc import * 5 | from .Qt import QtWidgets, QtCore, QtGui 6 | 7 | 8 | class ControlLayout(QtWidgets.QGridLayout): 9 | 10 | def __init__(self, columns=1, parent=None): 11 | super(ControlLayout, self).__init__(parent) 12 | 13 | self._columns = columns 14 | self.setContentsMargins(20, 20, 20, 20) 15 | self.setHorizontalSpacing(10) 16 | self.setRowStretch(1000, 1) 17 | self.widgets = [] 18 | 19 | @property 20 | def columns(self): 21 | return self._columns 22 | 23 | @columns.setter 24 | def columns(self, value): 25 | self._columns = value 26 | widgets = list(self.widgets) 27 | for w in widgets: 28 | self.takeWidget(w) 29 | for w in widgets: 30 | self.addWidget(w) 31 | 32 | @property 33 | def count(self): 34 | return len(self.widgets) 35 | 36 | def takeWidget(self, widget): 37 | if widget not in self.widgets: 38 | return None 39 | 40 | self.widgets.pop(self.widgets.index(widget)) 41 | self.takeAt(self.indexOf(widget)) 42 | return widget 43 | 44 | def addWidget(self, widget): 45 | count = self.count 46 | row = math.floor(count / self.columns) 47 | column = (count % self.columns) 48 | super(ControlLayout, self).addWidget(widget, row, column) 49 | self.widgets.append(widget) 50 | 51 | 52 | class FormWidget(QtWidgets.QWidget): 53 | 54 | def __init__(self, name, columns=1, layout_horizontal=False, parent=None): 55 | super(FormWidget, self).__init__(parent) 56 | 57 | self.name = name 58 | self.controls = {} 59 | self.forms = {} 60 | self.parent = parent 61 | 62 | self.layout = QtWidgets.QVBoxLayout() 63 | self.layout.setContentsMargins(0, 0, 0, 0) 64 | self.layout.setSpacing(0) 65 | self.setLayout(self.layout) 66 | 67 | if layout_horizontal: 68 | self.form_layout = QtWidgets.QBoxLayout( 69 | QtWidgets.QBoxLayout.LeftToRight 70 | ) 71 | else: 72 | self.form_layout = QtWidgets.QBoxLayout( 73 | QtWidgets.QBoxLayout.TopToBottom 74 | ) 75 | self.form_layout.setContentsMargins(0, 0, 0, 0) 76 | self.form_layout.setSpacing(0) 77 | 78 | self.control_layout = ControlLayout(columns=columns) 79 | self.form_layout.addLayout(self.control_layout) 80 | self.layout.addLayout(self.form_layout) 81 | 82 | self.setProperty('form', True) 83 | self.setAttribute(QtCore.Qt.WA_StyledBackground, True) 84 | 85 | @property 86 | def valid(self): 87 | is_valid = [] 88 | 89 | for name, control in self.controls.items(): 90 | control.validate() 91 | is_valid.append(control.valid) 92 | 93 | for name, form in self.forms.items(): 94 | is_valid.append(form.valid) 95 | 96 | return all(is_valid) 97 | 98 | def get_value(self, flatten=False): 99 | '''Get the value of this forms fields and subforms fields. 100 | 101 | :param flatten: If set to True, return a flattened dict 102 | ''' 103 | 104 | form_data = {} 105 | for name, control in self.controls.items(): 106 | form_data[name] = control.get_value() 107 | 108 | for name, form in self.forms.items(): 109 | form_value = form.get_value(flatten=flatten) 110 | if flatten: 111 | form_data.update(form_value) 112 | else: 113 | form_data[name] = form_value 114 | 115 | return form_data 116 | 117 | def set_value(self, strict=True, **data): 118 | '''Set the value of all the forms subforms and fields. You can pass 119 | an additional keyword argument strict to False to ignore mismatched 120 | names and subforms. 121 | 122 | :param strict: raise exceptions for any invalid names in data 123 | :param data: Field data used to set the values of the form 124 | 125 | usage:: 126 | 127 | myform.set_value( 128 | strict=True, 129 | **{ 130 | 'strfield': 'ABCDEFG', 131 | 'intfield': 1, 132 | 'subform': { 133 | 'subform_strfield': 'BCDEFGH', 134 | 'subform_intfield': 2,}}, 135 | ) 136 | ''' 137 | for name, value in data.items(): 138 | 139 | if isinstance(value, dict): 140 | try: 141 | self.forms[name].set_value(**value) 142 | except KeyError: 143 | if strict: 144 | raise FormNotFound(name + ' does not exist') 145 | continue 146 | 147 | try: 148 | self.controls[name].set_value(value) 149 | except KeyError: 150 | if strict: 151 | raise FieldNotFound(name + ' does not exist') 152 | 153 | def add_header(self, title, description=None, icon=None): 154 | '''Add a header''' 155 | 156 | self.header = Header(title, description, icon, self) 157 | self.layout.insertWidget(0, self.header) 158 | 159 | def insert_form(self, index, name, form): 160 | '''Insert a subform''' 161 | 162 | self.form_layout.insertWidget(index, form) 163 | self.forms[name] = form 164 | setattr(self, name, form) 165 | 166 | def add_form(self, name, form): 167 | '''Add a subform''' 168 | 169 | self.form_layout.addWidget(form) 170 | self.forms[name] = form 171 | setattr(self, name, form) 172 | 173 | def add_control(self, name, control): 174 | '''Add a control''' 175 | 176 | self.control_layout.addWidget(control.main_widget) 177 | self.controls[name] = control 178 | setattr(self, name, control) 179 | 180 | 181 | class FormDialog(QtWidgets.QDialog): 182 | 183 | def __init__(self, widget, *args, **kwargs): 184 | super(FormDialog, self).__init__(*args, **kwargs) 185 | 186 | self.widget = widget 187 | self.cancel_button = QtWidgets.QPushButton('&cancel') 188 | self.accept_button = QtWidgets.QPushButton('&accept') 189 | self.cancel_button.clicked.connect(self.reject) 190 | self.accept_button.clicked.connect(self.on_accept) 191 | 192 | self.layout = QtWidgets.QGridLayout() 193 | self.layout.setContentsMargins(0, 0, 0, 0) 194 | self.layout.setRowStretch(1, 1) 195 | self.setLayout(self.layout) 196 | 197 | self.button_layout = QtWidgets.QHBoxLayout() 198 | self.button_layout.setContentsMargins(20, 20, 20, 20) 199 | self.button_layout.addWidget(self.cancel_button) 200 | self.button_layout.addWidget(self.accept_button) 201 | 202 | self.layout.addWidget(self.widget, 0, 0) 203 | self.layout.addLayout(self.button_layout, 2, 0) 204 | 205 | def set_widget(self, widget): 206 | self.widget = widget 207 | self.widget.setProperty('groupwidget', True) 208 | 209 | if self.layout.count() == 2: 210 | self.layout.takeAt(0) 211 | 212 | self.layout.insertWidget(0, self.widget) 213 | 214 | def __getattr__(self, attr): 215 | try: 216 | return getattr(self.widget, attr) 217 | except AttributeError: 218 | raise AttributeError('FormDialog has no attr: {}'.format(attr)) 219 | 220 | def on_accept(self): 221 | if self.widget.valid: 222 | self.accept() 223 | return 224 | 225 | 226 | class FormGroup(QtWidgets.QWidget): 227 | 228 | toggled = QtCore.Signal(bool) 229 | after_toggled = QtCore.Signal(bool) 230 | 231 | def __init__(self, name, widget, *args, **kwargs): 232 | super(FormGroup, self).__init__(*args, **kwargs) 233 | 234 | self.layout = QtWidgets.QVBoxLayout() 235 | self.layout.setContentsMargins(0, 0, 0, 0) 236 | self.layout.setStretch(1, 0) 237 | self.layout.setSpacing(0) 238 | self.setLayout(self.layout) 239 | 240 | self.title = QtWidgets.QPushButton(name) 241 | icon = QtGui.QIcon() 242 | icon.addPixmap( 243 | QtGui.QPixmap(':/icons/plus'), 244 | QtGui.QIcon.Normal, 245 | QtGui.QIcon.Off 246 | ) 247 | icon.addPixmap( 248 | QtGui.QPixmap(':/icons/minus'), 249 | QtGui.QIcon.Normal, 250 | QtGui.QIcon.On 251 | ) 252 | self.title.setIcon(icon) 253 | self.title.setProperty('grouptitle', True) 254 | self.title.setCheckable(True) 255 | self.title.setChecked(True) 256 | self.title.toggled.connect(self.toggle_collapsed) 257 | self.layout.addWidget(self.title) 258 | 259 | self.widget = widget 260 | if isinstance(self.widget, QtWidgets.QWidget): 261 | self.widget.setProperty('groupwidget', True) 262 | self.layout.addWidget(self.widget) 263 | 264 | def set_widget(self, widget): 265 | self.widget = widget 266 | self.widget.setProperty('groupwidget', True) 267 | 268 | if self.layout.count() == 2: 269 | self.layout.takeAt(1) 270 | 271 | self.layout.addWidget(self.widget) 272 | 273 | def set_enabled(self, value): 274 | self.title.blockSignals(True) 275 | self.title.setChecked(value) 276 | self.widget.setVisible(value) 277 | self.title.blockSignals(False) 278 | 279 | def toggle_collapsed(self, collapsed): 280 | self.toggled.emit(collapsed) 281 | enabled = self.title.isChecked() 282 | self.widget.setVisible(enabled) 283 | self.after_toggled.emit(enabled) 284 | 285 | def toggle(self): 286 | value = not self.title.isChecked() 287 | self.title.setChecked(value) 288 | self.toggle_collapsed(value) 289 | 290 | def __getattr__(self, attr): 291 | try: 292 | return getattr(self.widget, attr) 293 | except AttributeError: 294 | raise AttributeError('FormDialog has no attr: {}'.format(attr)) 295 | 296 | 297 | class Header(QtWidgets.QWidget): 298 | 299 | def __init__(self, title, description=None, icon=None, parent=None): 300 | super(Header, self).__init__(parent) 301 | 302 | self.grid = QtWidgets.QGridLayout() 303 | self.grid.setColumnStretch(1, 1) 304 | 305 | self.setLayout(self.grid) 306 | 307 | self.title = QtWidgets.QLabel(title) 308 | self.title.setProperty('title', True) 309 | self.descr = QtWidgets.QLabel(description or 'No Description') 310 | self.descr.setProperty('description', True) 311 | if not description: 312 | self.descr.hide() 313 | 314 | if icon: 315 | self.descr.setAlignment( 316 | QtCore.Qt.AlignLeft 317 | | QtCore.Qt.AlignVCenter 318 | ) 319 | self.title.setAlignment( 320 | QtCore.Qt.AlignLeft 321 | | QtCore.Qt.AlignVCenter 322 | ) 323 | self.icon = QtWidgets.QLabel() 324 | self.icon.setPixmap(icon) 325 | if description: 326 | self.grid.addWidget(self.icon, 0, 0, 2, 1) 327 | self.grid.addWidget(self.title, 0, 1) 328 | self.grid.addWidget(self.descr, 1, 1) 329 | else: 330 | self.grid.addWidget(self.icon, 0, 0) 331 | self.grid.addWidget(self.title, 0, 1) 332 | else: 333 | self.descr.setAlignment(QtCore.Qt.AlignCenter) 334 | self.title.setAlignment(QtCore.Qt.AlignCenter) 335 | self.grid.addWidget(self.title, 0, 0) 336 | self.grid.addWidget(self.descr, 1, 0) 337 | 338 | self.setProperty('header', True) 339 | self.setAttribute(QtCore.Qt.WA_StyledBackground, True) 340 | 341 | self._mouse_button = None 342 | self._mouse_last_pos = None 343 | 344 | def mousePressEvent(self, event): 345 | self._mouse_button = event.button() 346 | super(Header, self).mousePressEvent(event) 347 | self._window = self.window() 348 | 349 | def mouseMoveEvent(self, event): 350 | '''Click + Dragging moves window''' 351 | 352 | if self._mouse_button == QtCore.Qt.LeftButton: 353 | if self._mouse_last_pos: 354 | 355 | p = self._window.pos() 356 | v = event.globalPos() - self._mouse_last_pos 357 | self._window.move(p + v) 358 | 359 | self._mouse_last_pos = event.globalPos() 360 | 361 | super(Header, self).mouseMoveEvent(event) 362 | 363 | def mouseReleaseEvent(self, event): 364 | self._mouse_button = None 365 | self._mouse_last_pos = None 366 | self._window = None 367 | super(Header, self).mouseReleaseEvent(event) 368 | 369 | 370 | class ScalingImage(QtWidgets.QLabel): 371 | 372 | __images = {} 373 | 374 | def __init__(self, image=None, parent=None): 375 | super(ScalingImage, self).__init__(parent) 376 | self.images = self.__images 377 | if not image: 378 | image = ':/images/noimg' 379 | self.set_image(image) 380 | self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, 381 | QtWidgets.QSizePolicy.Expanding) 382 | 383 | def set_image(self, image): 384 | if image not in self.images: 385 | if not isinstance(image, QtGui.QImage): 386 | if not QtCore.QFile.exists(image): 387 | return 388 | self.img = QtGui.QImage(image) 389 | self.images[image] = self.img 390 | else: 391 | self.img = self.images[image] 392 | 393 | self.setMinimumSize(227, 128) 394 | self.scale_pixmap() 395 | self.repaint() 396 | 397 | def scale_pixmap(self): 398 | scaled_image = self.img.scaled( 399 | self.width(), 400 | self.height(), 401 | QtCore.Qt.KeepAspectRatioByExpanding, 402 | QtCore.Qt.FastTransformation) 403 | self.pixmap = QtGui.QPixmap(scaled_image) 404 | 405 | def resizeEvent(self, event): 406 | self.do_resize = True 407 | super(ScalingImage, self).resizeEvent(event) 408 | 409 | def paintEvent(self, event): 410 | if self.do_resize: 411 | self.scale_pixmap() 412 | self.do_resize = False 413 | 414 | offsetX = -((self.pixmap.width() - self.width()) * 0.5) 415 | offsetY = -((self.pixmap.height() - self.height()) * 0.5) 416 | painter = QtGui.QPainter(self) 417 | painter.drawPixmap(offsetX, offsetY, self.pixmap) 418 | 419 | 420 | class IconButton(QtWidgets.QPushButton): 421 | '''A button with an icon. 422 | 423 | :param icon: path to icon file or resource 424 | :param tip: tooltip text 425 | :param name: object name 426 | :param size: width, height tuple (default: (24, 24)) 427 | ''' 428 | 429 | def __init__(self, icon, tip, name, size=(26, 26), *args, **kwargs): 430 | super(IconButton, self).__init__(*args, **kwargs) 431 | 432 | self.setObjectName(name) 433 | self.setIcon(QtGui.QIcon(icon)) 434 | self.setIconSize(QtCore.QSize(*size)) 435 | self.setSizePolicy( 436 | QtWidgets.QSizePolicy.Fixed, 437 | QtWidgets.QSizePolicy.Fixed) 438 | self.setFixedHeight(size[0]) 439 | self.setFixedWidth(size[1]) 440 | self.setToolTip(tip) 441 | -------------------------------------------------------------------------------- /mvp/viewport.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, absolute_import 3 | 4 | import sys 5 | 6 | import maya.cmds as cmds 7 | import maya.OpenMayaUI as OpenMayaUI 8 | import maya.OpenMaya as OpenMaya 9 | import maya.utils as utils 10 | from Qt import QtGui, QtCore, QtWidgets 11 | 12 | from .renderglobals import RenderGlobals 13 | from .utils import wait, viewport_state, get_maya_window 14 | 15 | # Py3 compat 16 | if sys.version_info > (3, 0): 17 | basestring = str 18 | long = int 19 | 20 | 21 | EDITOR_PROPERTIES = [ 22 | 'activeComponentsXray', 23 | 'activeOnly', 24 | 'backfaceCulling', 25 | 'bufferMode', 26 | 'bumpResolution', 27 | 'camera', 28 | 'cameras', 29 | 'clipGhosts', 30 | 'colorResolution', 31 | 'controllers', 32 | 'controlVertices', 33 | 'cullingOverride', 34 | 'deformers', 35 | 'depthOfField', 36 | 'dimensions', 37 | 'displayAppearance', 38 | 'displayLights', 39 | 'displayTextures', 40 | 'dynamicConstraints', 41 | 'dynamics', 42 | 'fluids', 43 | 'fogColor', 44 | 'fogDensity', 45 | 'fogEnd', 46 | 'fogging', 47 | 'fogMode', 48 | 'fogSource', 49 | 'fogStart', 50 | 'follicles', 51 | 'gpuCacheDisplayFilter', 52 | 'greasePencils', 53 | 'grid', 54 | 'hairSystems', 55 | 'handles', 56 | 'headsUpDisplay', 57 | 'hulls', 58 | 'ignorePanZoom', 59 | 'ikHandles', 60 | 'imagePlane', 61 | 'interactiveBackFaceCull', 62 | 'interactiveDisableShadows', 63 | 'isFiltered', 64 | 'joints', 65 | 'jointXray', 66 | 'lights', 67 | 'lineWidth', 68 | 'locators', 69 | 'lowQualityLighting', 70 | 'manipulators', 71 | 'maxConstantTransparency', 72 | 'maximumNumHardwareLights', 73 | 'motionTrails', 74 | 'nCloths', 75 | 'nParticles', 76 | 'nRigids', 77 | 'nurbsCurves', 78 | 'nurbsSurfaces', 79 | 'objectFilterShowInHUD', 80 | 'occlusionCulling', 81 | 'particleInstancers', 82 | 'pivots', 83 | 'planes', 84 | 'pluginShapes', 85 | 'polymeshes', 86 | 'rendererName', 87 | 'selectionHiliteDisplay', 88 | 'shadingModel', 89 | 'shadows', 90 | 'smallObjectCulling', 91 | 'smallObjectThreshold', 92 | 'smoothWireframe', 93 | 'sortTransparent', 94 | 'strokes', 95 | 'subdivSurfaces', 96 | 'textureAnisotropic', 97 | 'textureCompression', 98 | 'textureDisplay', 99 | 'textureHilight', 100 | 'textureMaxSize', 101 | 'textures', 102 | 'textureSampling', 103 | 'transparencyAlgorithm', 104 | 'transpInShadows', 105 | 'twoSidedLighting', 106 | 'useBaseRenderer', 107 | 'useDefaultMaterial', 108 | 'useInteractiveMode', 109 | 'useReducedRenderer', 110 | 'viewSelected', 111 | 'wireframeOnShaded', 112 | 'xray', 113 | ] 114 | 115 | CAMERA_PROPERTIES = [ 116 | 'displayFilmGate', 117 | 'displayResolution', 118 | 'displayGateMask', 119 | 'displayFieldChart', 120 | 'displaySafeAction', 121 | 'displaySafeTitle', 122 | 'displayFilmPivot', 123 | 'displayFilmOrigin', 124 | 'overscan', 125 | 'displayGateMaskOpacity', 126 | 'displayGateMaskColor' 127 | ] 128 | 129 | 130 | def deferred_close(view): 131 | panel = view.panel 132 | wait(0.1) 133 | utils.executeDeferred(cmds.deleteUI, panel, panel=True) 134 | 135 | 136 | def playblast(camera=None, state=None, **kwargs): 137 | '''Playblast the active viewport. 138 | 139 | Arguments: 140 | :param camera: Camera to playblast 141 | :param state: Viewport state 142 | 143 | Playblast Arguments: 144 | :param width: Resolution width 145 | :param height: Resolution height 146 | :param format: Render format like qt or image 147 | :param compression: Render compression 148 | :param viewer: Launch the default viewer afterwards 149 | ''' 150 | 151 | playblast_kwargs = { 152 | 'offScreen': False, 153 | 'percent': 100, 154 | 'quality': 100, 155 | 'viewer': False, 156 | 'width': 960, 157 | 'height': 540, 158 | 'framePadding': 4, 159 | 'format': 'qt', 160 | 'compression': 'H.264', 161 | 'forceOverwrite': True, 162 | } 163 | playblast_kwargs.update(kwargs) 164 | 165 | active = Viewport.active() 166 | state = state or active.get_state() 167 | 168 | if camera: 169 | state['camera'] = camera 170 | 171 | with viewport_state(active, state): 172 | file = cmds.playblast(**playblast_kwargs) 173 | 174 | return file 175 | 176 | class EditorProperty(object): 177 | 178 | def __init__(self, name): 179 | self.name = name 180 | 181 | def __get__(self, inst, typ=None): 182 | '''Gets a model editor property.''' 183 | 184 | if not inst: 185 | return self 186 | 187 | try: 188 | val = cmds.modelEditor( 189 | inst.panel, 190 | **{'query': True, self.name: True} 191 | ) 192 | except TypeError: 193 | val = cmds.modelEditor( 194 | inst.panel, 195 | **{'query': True, 'qpo': self.name} 196 | ) 197 | 198 | if self.name == 'smallObjectThreshold': 199 | val = val[0] 200 | 201 | return val 202 | 203 | def __set__(self, inst, value): 204 | '''Sets a model editor property.''' 205 | 206 | try: 207 | cmds.modelEditor( 208 | inst.panel, 209 | **{'edit': True, self.name: value} 210 | ) 211 | except TypeError: 212 | cmds.modelEditor( 213 | inst.panel, 214 | **{'edit': True, 'po': [self.name, value]} 215 | ) 216 | 217 | 218 | class CameraProperty(object): 219 | 220 | def __init__(self, name): 221 | self.name = name 222 | 223 | def __get__(self, inst, typ=None): 224 | '''Gets a model panels camera property''' 225 | 226 | if not inst: 227 | return self 228 | 229 | attr = inst.camera + '.' + self.name 230 | value = cmds.getAttr(attr) 231 | if isinstance(value, list): 232 | if len(value) == 1 and isinstance(value[0], (list, tuple)): 233 | value = value[0] 234 | return value 235 | 236 | def __set__(self, inst, value): 237 | '''Sets a model panels camera property''' 238 | 239 | attr = inst.camera + '.' + self.name 240 | 241 | locked = cmds.getAttr(attr, lock=True) 242 | if locked: 243 | return 244 | 245 | has_connections = cmds.listConnections(attr, s=True, d=False) 246 | if has_connections: 247 | return 248 | 249 | try: 250 | if isinstance(value, (int, float)): 251 | cmds.setAttr(attr, value) 252 | elif isinstance(value, basestring): 253 | cmds.setAttr(attr, value, type='string') 254 | elif isinstance(value, (list, tuple)): 255 | cmds.setAttr(attr, *value) 256 | else: 257 | cmds.setAttr(attr, value) 258 | except Except as e: 259 | print('Failed to set state: %s %s' % (attr, value)) 260 | print(e) 261 | 262 | 263 | class Viewport(object): 264 | '''A convenient api for manipulating Maya 3D Viewports. While you can 265 | manually construct a Viewport from an OpenMayaUI.M3dView instance, it is 266 | much easier to use the convenience methods Viewport.iter, 267 | Viewport.active and Viewport.get:: 268 | 269 | # Get the active view 270 | v = Viewport.active() 271 | assert v.focus == True 272 | 273 | # Assuming we have a second modelPanel available 274 | # Get an inactive view and make it the active view 275 | v2 = Viewport.get(1) 276 | v2.focus = True 277 | 278 | assert v.focus == False 279 | assert v2.focus == True 280 | 281 | Viewport provides standard attribute lookup to all modelEditor properties:: 282 | 283 | # Hide nurbsCurves and show polymeshes in the viewport 284 | v.nurbsCurves = False 285 | v.polymeshes = True 286 | 287 | :param m3dview: OpenMayaUI.M3dView instance. 288 | ''' 289 | 290 | for p in EDITOR_PROPERTIES: 291 | locals()[p] = EditorProperty(p) 292 | 293 | for p in CAMERA_PROPERTIES: 294 | locals()[p] = CameraProperty(p) 295 | 296 | def __init__(self, m3dview): 297 | self._m3dview = m3dview 298 | self.highlight = self._highlight 299 | self.identify = self._highlight 300 | 301 | def __hash__(self): 302 | return hash(self._m3dview) 303 | 304 | def __eq__(self, other): 305 | if hasattr(other, '_m3dview'): 306 | return self._m3dview == other._m3dview 307 | return self.panel == other 308 | 309 | def copy(self): 310 | '''Tear off a copy of the viewport. 311 | 312 | :returns: A new torn off copy of Viewport''' 313 | 314 | panel = cmds.modelPanel(tearOffCopy=self.panel) 315 | view = self.from_panel(panel) 316 | view.focus = True 317 | return view 318 | 319 | __copy__ = copy 320 | __deepcopy__ = copy 321 | 322 | def float(self): 323 | '''Tear off the panel.''' 324 | copied_view = self.copy() 325 | deferred_close(self) 326 | self._m3dview = copied_view._m3dview 327 | 328 | @classmethod 329 | def new(cls): 330 | panel = cmds.modelPanel() 331 | view = cls.from_panel(panel) 332 | view.float() 333 | view.focus = True 334 | return view 335 | 336 | def close(self): 337 | '''Close this viewport''' 338 | deferred_close(self) 339 | 340 | @property 341 | def properties(self): 342 | '''A list including all editor property names.''' 343 | 344 | return EDITOR_PROPERTIES + CAMERA_PROPERTIES 345 | 346 | def get_state(self): 347 | '''Get a state dictionary of all modelEditor properties.''' 348 | 349 | active_state = {} 350 | active_state['RenderGlobals'] = RenderGlobals.get_state() 351 | for ep in EDITOR_PROPERTIES + CAMERA_PROPERTIES: 352 | active_state[ep] = getattr(self, ep) 353 | 354 | return active_state 355 | 356 | def set_state(self, state): 357 | '''Sets a dictionary of properties all at once. 358 | 359 | :param state: Dictionary including property, value pairs''' 360 | 361 | cstate = state.copy() 362 | 363 | renderglobals_state = cstate.pop('RenderGlobals', None) 364 | if renderglobals_state: 365 | RenderGlobals.set_state(renderglobals_state) 366 | 367 | for k, v in cstate.items(): 368 | setattr(self, k, v) 369 | 370 | def playblast(self, camera=None, state=None, **kwargs): 371 | '''Playblasting with reasonable default arguments. Automatically sets 372 | this viewport to the active view, ensuring that we playblast the 373 | correct view. 374 | 375 | :param kwargs: Same kwargs as :func:`maya.cmds.playblast`''' 376 | 377 | if not self.focus: 378 | self.focus = True 379 | 380 | playblast(camera=None, state=None, **kwargs) 381 | 382 | @property 383 | def screen_geometry(self): 384 | qapp = QtWidgets.QApplication.instance() 385 | desktop = qapp.desktop() 386 | screen = desktop.screenNumber(self.widget) 387 | return desktop.screenGeometry(screen) 388 | 389 | def center(self): 390 | screen_center = self.screen_geometry.center() 391 | window_center = self.window.rect().center() 392 | pnt = screen_center - window_center 393 | self.window.move(pnt) 394 | 395 | @property 396 | def size(self): 397 | return self._m3dview.portWidth(), self._m3dview.portHeight() 398 | 399 | @size.setter 400 | def size(self, wh): 401 | w1, h1 = self.size 402 | win_size = self.window.size() 403 | w2, h2 = win_size.width(), win_size.height() 404 | w_offset = w2 - w1 405 | h_offset = h2 - h1 406 | self.window.resize(wh[0] + w_offset, wh[1] + h_offset) 407 | 408 | @property 409 | def widget(self): 410 | '''Returns a QWidget object for the viewport.''' 411 | 412 | try: 413 | from shiboken import wrapInstance 414 | except ImportError: # PySide2 compat 415 | from shiboken2 import wrapInstance 416 | w = wrapInstance(long(self._m3dview.widget()), QtWidgets.QWidget) 417 | return w 418 | 419 | @property 420 | def window(self): 421 | '''Returns a QWidget object for the viewports parent window''' 422 | 423 | return self.widget.window() 424 | 425 | @property 426 | def panel(self): 427 | '''Returns a panel name for the Viewport.''' 428 | panel = OpenMayaUI.MQtUtil.fullName(long(self._m3dview.widget())) 429 | return panel.split('|')[-2] 430 | 431 | @property 432 | def index(self): 433 | '''Returns the index of the viewport''' 434 | 435 | i = 0 436 | for i, view in Viewport.iter(): 437 | if self == view: 438 | return i 439 | i += 1 440 | raise IndexError('Can not find index') 441 | 442 | @property 443 | def focus(self): 444 | '''Check if current Viewport is the active Viewport.''' 445 | return self == self.active() 446 | 447 | @focus.setter 448 | def focus(self, value): 449 | '''Set focus to Viewport instance.''' 450 | 451 | if not value: 452 | try: 453 | Viewport.get(1).focus = True 454 | except: 455 | pass 456 | return 457 | 458 | cmds.modelEditor(self.panel, edit=True, activeView=True) 459 | 460 | @property 461 | def camera(self): 462 | '''Get the short name of the active camera.''' 463 | 464 | camera = OpenMaya.MDagPath() 465 | self._m3dview.getCamera(camera) 466 | camera.pop() 467 | return camera.partialPathName() 468 | 469 | @camera.setter 470 | def camera(self, camera_path): 471 | '''Set the active camera for the Viewport.''' 472 | 473 | sel = OpenMaya.MSelectionList() 474 | sel.add(camera_path) 475 | camera = OpenMaya.MDagPath() 476 | sel.getDagPath(0, camera) 477 | 478 | util = OpenMaya.MScriptUtil(0) 479 | int_ptr = util.asUintPtr() 480 | camera.numberOfShapesDirectlyBelow(int_ptr) 481 | num_shapes = util.getUint(int_ptr) 482 | if num_shapes: 483 | camera.extendToShape() 484 | 485 | self._m3dview.setCamera(camera) 486 | self._m3dview.refresh(False, False) 487 | 488 | @property 489 | def background(self): 490 | '''Get the background color of the Viewport''' 491 | 492 | color = self._m3dview.backgroundColor() 493 | return color[0], color[1], color[2] 494 | 495 | @background.setter 496 | def background(self, values): 497 | '''Set the background color of the Viewport. 498 | 499 | :param values: RGB value''' 500 | 501 | cmds.displayRGBColor('background', *values) 502 | 503 | def _highlight(self, msec=2000): 504 | '''Draws an identifier in a Viewport.''' 505 | 506 | highlight = Highlight(self) 507 | highlight.display(msec) 508 | 509 | @classmethod 510 | def identify(cls, delay=2000): 511 | '''Shows identifiers in all Viewports:: 512 | 513 | Viewport.identify() 514 | 515 | :param delay: Length of time in ms to leave up identifier 516 | ''' 517 | 518 | cls.highlight() 519 | 520 | @classmethod 521 | def highlight(cls, msec=2000): 522 | '''Draws QLabels indexing each Viewport. These indices can be used to 523 | with :method:`get` to return a corresponding Viewport object.''' 524 | 525 | for viewport in cls.iter(): 526 | if viewport.widget.isVisible(): 527 | viewport.highlight(msec) 528 | 529 | @staticmethod 530 | def count(): 531 | '''The number of 3D Viewports.''' 532 | 533 | return OpenMayaUI.M3dView.numberOf3dViews() 534 | 535 | @classmethod 536 | def from_panel(cls, panel): 537 | m3dview = OpenMayaUI.M3dView() 538 | OpenMayaUI.M3dView.getM3dViewFromModelPanel(panel, m3dview) 539 | return cls(m3dview) 540 | 541 | @classmethod 542 | def get(cls, index): 543 | '''Get the Viewport at index.''' 544 | 545 | m3dview = OpenMayaUI.M3dView() 546 | OpenMayaUI.M3dView.get3dView(index, m3dview) 547 | return cls(m3dview) 548 | 549 | @classmethod 550 | def active(cls): 551 | '''Get the active Viewport.''' 552 | 553 | m3dview = OpenMayaUI.M3dView.active3dView() 554 | return cls(m3dview) 555 | 556 | @classmethod 557 | def iter(cls): 558 | '''Yield all Viewport objects. 559 | 560 | usage:: 561 | 562 | for view in Viewport.iter(): 563 | print(v.panel) 564 | ''' 565 | 566 | for index in range(cls.count()): 567 | m3dview = OpenMayaUI.M3dView() 568 | OpenMayaUI.M3dView.get3dView(index, m3dview) 569 | yield cls(m3dview) 570 | 571 | 572 | class Highlight(QtWidgets.QDialog): 573 | '''Outline a viewport panel and show the panel name.''' 574 | 575 | def __init__(self, view): 576 | super(Highlight, self).__init__(parent=get_maya_window()) 577 | self.view = view 578 | self.widget = self.view.widget 579 | 580 | self.setWindowFlags( 581 | self.windowFlags() 582 | | QtCore.Qt.FramelessWindowHint 583 | ) 584 | self.setAttribute(QtCore.Qt.WA_TranslucentBackground) 585 | self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) 586 | wrect = self.widget.geometry() 587 | rect = QtCore.QRect( 588 | self.widget.mapToGlobal( 589 | wrect.topLeft(), 590 | ), 591 | wrect.size(), 592 | ) 593 | self.setGeometry( 594 | rect 595 | ) 596 | 597 | def display(self, msec): 598 | w = QtWidgets.QApplication.instance().activeWindow() 599 | self.show() 600 | w.raise_() 601 | QtCore.QTimer.singleShot(msec, self.accept) 602 | 603 | def paintEvent(self, event): 604 | 605 | painter = QtGui.QPainter(self) 606 | 607 | pen = QtGui.QPen(QtCore.Qt.red) 608 | pen.setWidth(8) 609 | font = QtGui.QFont() 610 | font.setPointSize(48) 611 | painter.setFont(font) 612 | painter.setPen(pen) 613 | painter.setBrush(QtCore.Qt.transparent) 614 | 615 | painter.drawRect(self.rect()) 616 | painter.drawText(self.rect(), QtCore.Qt.AlignCenter, self.view.panel) 617 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "mvp" 3 | version = "0.9.3" 4 | description = "Maya Viewport API and playblasting tools" 5 | authors = ["Dan Bradham "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^2.7 || ^3.6" 10 | 11 | [tool.poetry.dev-dependencies] 12 | 13 | [build-system] 14 | requires = ["poetry>=0.12"] 15 | build-backend = "poetry.masonry.api" 16 | --------------------------------------------------------------------------------