Repository: fredrikaverpil/pyvfx-boilerplate Branch: master Commit: bbfc07541807 Files: 21 Total size: 45.3 KB Directory structure: gitextract_ynxoaokt/ ├── .github/ │ └── workflows/ │ ├── pypi.yml │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── setup.cfg ├── setup.py └── src/ └── pyvfx_boilerplate/ ├── __init__.py ├── boilerplate_ui.py ├── cli.py ├── init.py ├── mayapalette.py ├── menu.py ├── resources/ │ ├── main_window.ui │ ├── module.ui │ ├── qpalette_maya2015.json │ └── qpalette_maya2016.json └── utils/ ├── __init__.py ├── get_set_palette_data_qt4.py └── get_set_palette_data_qt5.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/pypi.yml ================================================ # https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions # https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ name: PyPI on: release: types: [created] jobs: build-and-publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup Python uses: actions/setup-python@v1 with: python-version: '3.x' architecture: x64 - name: Build wheel run: | pip install --upgrade wheel setuptools python setup.py bdist_wheel - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.pypi_password }} ================================================ FILE: .github/workflows/tests.yml ================================================ # https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: ['2.x', '3.x'] name: Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v2 - name: Setup Python uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} architecture: x64 - name: Build wheel run: | pip install --upgrade wheel setuptools python setup.py bdist_wheel - run: pip install isort && isort --recursive --diff . - run: pip install black && black --check . if: matrix.python-version == '3.x' - run: pip install flake8 && flake8 ================================================ FILE: .gitignore ================================================ # Python *.pyc # Folders .eggs *.egg-info/ build dist venv ================================================ FILE: CHANGELOG.md ================================================ # Changelog ### Version 3.3.0 - adding Blender, 3DS Max, Houdini, and Unreal Engine support - see below for installing PySide2 into Blender Ultra Easy Guide To install PySide2 into Blender on macOS: 1) install blender 2) open blender at least once, and then close blender 3) open terminal and run the commands below ```bash curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py /Applications/Blender.app/Contents/Resources/2.81/python/bin/python3.7m get-pip.py /Applications/Blender.app/Contents/Resources/2.81/python/bin/pip install PySide2, pyvfx-boilerplate ``` Ultra Easy Guide To install PySide into Unreal on Windows: this currently fails: (testing on 4.24.2) 1) install Unreal 2) open a windows cmd or powershell and run the commands below ```bash cd C:/Program Files/Unreal/UE_4.24/Engine/Binaries/ThirdParty/Python/Win64/ python.exe -m pip install --upgrade pip python.exe -m pip install --no-warn-script-location PySide pyvfx-boilerplate ``` ### Version 3.2.1 - rearrangement of namespace package location - updated to setuptools_scm to handle version numbering ### Version 3.2.0 - uses kwargs to pass more arguments to the gui show - auto docks into attribute editor panel in maya if dockable=True in kwargs ### Version 3.1.0 #### Changes from 2.0 (nzanepro fork) - Updated to work with latest Qt.py (1.2.0b2) - Tested with maya (2018.4), nuke (11.2v4), and PySide2 (5.11.2) - Install via pip and you will get Qt.py installed as a dependency (see below) - Now includes MayaQWidgetDockableMixin in maya - Better Maya menuing via python instead of pymel, pyvfx now has a root menu, and other modules can be added to the menu. - Example new app via inheritance of Boilerplate (includes extension of MayaQWidgetDockableMixin): https://github.com/nzanepro/pyvfx.boilerplateinherited #### Changes from 1.0 - Complete rewrite of the boilerplate. - Requires (and bundles) the [`Qt.py`](https://github.com/mottosso/Qt.py) - Tested with Python 2.7.11 and 3.5.1. - Uses `PySide.QUiTools` instead of `pysideuic`, which was used in v1.0. - No longer uses the complex "wrap instance" approach in favor for simpler code. Because of this, UIs are no longer loaded into `self`. - Maya palette styling in standalone mode. - Git repo name change: all lowercase. - Pretty much PEP8 compliant. - Properly parented window in Maya - Writing of .pyc (bytecode) files disabled to prevent issues between Python 2 and 3. - Can be run in multiple ways (see examples). ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Fredrik Averpil Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # pyvfx-boilerplate ![Tests](https://github.com/fredrikaverpil/pyvfx-boilerplate/workflows/Tests/badge.svg) ![PyPI](https://github.com/fredrikaverpil/pyvfx-boilerplate/workflows/PyPI/badge.svg) A boilerplate for creating PyQt4/PySide and PyQt5/PySide2 applications running in Maya, Nuke, Blender, 3DS Max, Houdini, Unreal Engine or completely standalone. ## Documentation ### Version 3.x - The entire boilerplate was re-written so it could be packaged and distributed with PyPi. - Adding Blender, 3DS Max, Houdini, and Unreal Engine support. For details, see [CHANGELOG.md](CHANGELOG.md). ### Noteworthy known issues - Does not work with Nuke 10.0v1 on OS X: [#7](https://github.com/fredrikaverpil/pyvfx-boilerplate/issues/7) - Maya palette glitchy in standalone mode with PySide/PyQt4 on OS X (disabled by default): [#9](https://github.com/fredrikaverpil/pyvfx-boilerplate/issues/9) - Window will not stay on top of Nuke (OS X) without Qt.Tool or Qt.WindowStaysOnTopHint: [#12](https://github.com/fredrikaverpil/pyvfx-boilerplate/issues/12) ### Installation Easy way: ```bash pip install pyvfx-boilerplate ``` Long way: ```bash git clone https://github.com/fredrikaverpil/pyvfx-boilerplate.git cd pyvfx-boilerplate python setup.py sdist bdist_wheel pip install dist/* ``` ### Example usage Pip installs a program named `pyvfx-boilerplate` as an example Run as standalone: (you may need to additionally install PyQt4, PyQt5, PySide or PySide2 for standalone to work depending on your system configuration) ```bash pyvfx-boilerplate ``` Run in script editor of Maya or Nuke: ```python import sys sys.path.append('/path/to/pyvfx-boilerplate') from pyvfx_boilerplate import boilerplate_ui bpr = boilerplate_ui.BoilerplateRunner() bpr.run_main() ``` ### Modifying the boilerplate - See inheritance example above ### Development guidelines Since the boilerplate relies on [`Qt.py`](https://github.com/mottosso/Qt.py), you should design your application as if you were designing it for PyQt5/PySide2. This means creating widgets using `QtWidgets` rather than `QtGui`. The `Qt.py` module takes care of the remapping and makes for compatibility with PyQt4/PySide. Read more over at the [`Qt.py` repository](https://github.com/mottosso/Qt.py). Tip: when you cannot rely on `Qt.py`, create an issue (probably over at [`Qt.py`](https://github.com/mottosso/Qt.py)) and/or detect which binding is being used and write some custom code: ```python from Qt import QtCompat if QtCompat.__binding__ in ('PyQt4', 'PySide'): # Do something if PyQt4 or PySide is used if QtCompat__binding.startswith('PySide'): # Do something if PySide or PySide2 is used if QtCompat__binding == 'PySide2': # Do something if PySide2 is used ``` ### Issues Something wrong, have a question or wish to file a feature request? Open up an issue [here](https://github.com/fredrikaverpil/pyvfx-boilerplate/issues)! ### Contribute If you wish to contribute, pull requests are more than welcome! ================================================ FILE: setup.cfg ================================================ [bdist_wheel] universal=1 ================================================ FILE: setup.py ================================================ # https://docs.python.org/3/distutils/setupscript.html#additional-meta-data import setuptools with open("README.md", "r") as fh: long_description = fh.read() name = "pyvfx-boilerplate" url = "https://github.com/fredrikaverpil/pyvfx-boilerplate" description = "A boilerplate Qt Py* app that runs in dcc apps, py2, or py3." package_dir = "src" cli_modules = [ "pyvfx-boilerplate=pyvfx_boilerplate.cli:main", ] setuptools.setup( setup_requires=["setuptools_scm"], use_scm_version={"local_scheme": "node-and-timestamp"}, name=name, description=description, long_description=long_description, long_description_content_type="text/markdown", url=url, packages=setuptools.find_packages(package_dir), package_dir={"": package_dir}, entry_points={"console_scripts": cli_modules}, include_package_data=True, install_requires=["Qt.py"], classifiers=[ "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.7", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], ) ================================================ FILE: src/pyvfx_boilerplate/__init__.py ================================================ __path__ = __import__("pkgutil").extend_path(__path__, __name__) ================================================ FILE: src/pyvfx_boilerplate/boilerplate_ui.py ================================================ """This uses a Qt binding of "any" kind, thanks to the Qt.py module, to produce an UI. First, one .ui file is loaded and then attaches another .ui file onto the first. Think of it as creating a modular UI. More on Qt.py: https://github.com/mottosso/Qt.py """ import atexit import os import platform import sys import Qt from Qt import QtCompat, QtCore, QtWidgets # pylint: disable=E0611 from . import mayapalette try: import maya.cmds as cmds from maya.app.general.mayaMixin import MayaQWidgetDockableMixin MAYA = True except ImportError: MAYA = False try: import nuke import nukescripts NUKE = True except ImportError: NUKE = False try: import hou HOUDINI = True except ImportError: HOUDINI = False try: import MaxPlus THREEDSMAX = True except ImportError: THREEDSMAX = False try: import bpy BLENDER = True except ImportError: BLENDER = False try: import unreal UNREAL = True except ImportError: UNREAL = False # ---------------------------------------------------------------------- # Configuration # ---------------------------------------------------------------------- # Full path to where .ui files are stored UI_PATH = os.path.join(os.path.dirname(__file__), "resources") # Palette filepath PALETTE_FILEPATH = os.path.join(UI_PATH, "qpalette_maya2016.json") WTITLE = "Boilerplate" WOBJ = "boilerPlate" # ---------------------------------------------------------------------- # Main script # ---------------------------------------------------------------------- class _Boilerplate(QtWidgets.QMainWindow): """ Don't subclass this directly, subclass Boilerplate without the '_' """ def __init__(self, parent=None, win_title=WTITLE, win_object=WOBJ): super(_Boilerplate, self).__init__(parent) # Set object name and window title self.setObjectName(win_object) self.setWindowTitle(win_title) # Window type self.setWindowFlags(QtCore.Qt.Window) if MAYA: # Makes Maya perform magic which makes the window stay # on top in OS X and Linux. As an added bonus, it'll # make Maya remember the window position self.setProperty("saveWindowPref", True) self.setupUi() def setupUi(self): # Filepaths main_window_file = os.path.join(UI_PATH, "main_window.ui") module_file = os.path.join(UI_PATH, "module.ui") # Load UIs self.main_widget = QtCompat.load_ui(main_window_file) # Main window UI self.module_widget = QtCompat.load_ui(module_file) # Module UI # Attach module to main window UI's boilerVerticalLayout layout self.main_widget.boilerVerticalLayout.addWidget(self.module_widget) # Edit widget which resides in module UI self.module_widget.boilerLabel.setText("Push the button!") # Edit widget which reside in main window UI self.main_widget.boilerPushButton.setText("Push me!") # Set the main widget self.setCentralWidget(self.main_widget) # Define minimum size of UI self.setMinimumSize(200, 200) # Signals # The "pushButton" widget resides in the main window UI self.main_widget.boilerPushButton.clicked.connect(self.say_hello) def say_hello(self): """Set the label text. The "label" widget resides in the module """ self.module_widget.boilerLabel.setText("Hello world!") self.module_widget.boilerLabel.update() if MAYA: class Boilerplate(MayaQWidgetDockableMixin, _Boilerplate): """Example showing how UI files can be loaded using the same script when taking advantage of the Qt.py module and build-in methods from PySide/PySide2/PyQt4/PyQt5.""" def __init__(self, parent=None, win_title=WTITLE, win_object=WOBJ): super(Boilerplate, self).__init__( parent, win_title=win_title, win_object=win_object ) # elif THREEDSMAX: # # https://forums.autodesk.com/t5/3ds-max-programming/3ds-max-2019-qt-dock-widget/td-p/8164550 # noqa # # https://help.autodesk.com/view/3DSMAX/2019/ENU/?guid=__py_ref_demo_py_side_tool_bar_q_widget_8py_example_html # noqa # pass else: class Boilerplate(_Boilerplate): """Example showing how UI files can be loaded using the same script when taking advantage of the Qt.py module and build-in methods from PySide/PySide2/PyQt4/PyQt5.""" def __init__(self, parent=None, win_title=WTITLE, win_object=WOBJ): super(Boilerplate, self).__init__( parent, win_title=win_title, win_object=win_object ) # ---------------------------------------------------------------------- # DCC application helper functions # ---------------------------------------------------------------------- def _maya_delete_ui(window_title, window_object): """Delete existing UI in Maya""" if cmds.window(window_object, q=True, exists=True): cmds.deleteUI(window_object) # Delete window if cmds.dockControl("MayaWindow|" + window_title, q=True, ex=True): cmds.deleteUI("MayaWindow|" + window_title) # Delete docked window def _maya_delete_workspace(window_object): """Delete existing workspace in Maya""" control = window_object + "WorkspaceControl" if cmds.workspaceControl(control, q=True, exists=True): cmds.workspaceControl(control, e=True, close=True) cmds.deleteUI(control, control=True) def _maya_update_workspace(window_object): """Updates existing workspace in Maya""" control = window_object + "WorkspaceControl" # TODO make this argument controllable if cmds.workspaceControl(control, q=True, exists=True): cmds.workspaceControl( control, e=True, restore=True, retain=True, # # options below # dockToMainWindow=("left", -1), # tabToControl=("ChannelBoxLayerEditor", -1), # tabToControl=("Outliner", -1), tabToControl=("AttributeEditor", -1), ) def _maya_main_window(): """Return Maya's main window""" for obj in QtWidgets.QApplication.topLevelWidgets(): if obj.objectName() == "MayaWindow": return obj raise RuntimeError("Could not find MayaWindow instance") def _nuke_delete_ui(window_object): """Delete existing UI in Nuke""" for obj in QtWidgets.QApplication.allWidgets(): if obj.objectName() == window_object: obj.deleteLater() def _nuke_main_window(): """Returns Nuke's main window""" for obj in QtWidgets.QApplication.topLevelWidgets(): if ( obj.inherits("QMainWindow") and obj.metaObject().className() == "Foundry::UI::DockMainWindow" ): return obj else: raise RuntimeError("Could not find DockMainWindow instance") def _nuke_set_zero_margins(widget_object): """Remove Nuke margins when docked UI .. _More info: https://gist.github.com/maty974/4739917 """ parentApp = QtWidgets.QApplication.allWidgets() parentWidgetList = [] for parent in parentApp: for child in parent.children(): if widget_object.__class__.__name__ == child.__class__.__name__: parentWidgetList.append(parent.parentWidget()) try: twoup = parent.parentWidget().parentWidget() parentWidgetList.append(twoup) threeup = twoup.parentWidget() parentWidgetList.append(threeup) except AttributeError: pass for sub in parentWidgetList: if sub is not None: for tinychild in sub.children(): try: tinychild.setContentsMargins(0, 0, 0, 0) except AttributeError: pass def _houdini_main_window(): """Return Houdini's main window""" return hou.ui.mainQtWindow() def _3dsmax_main_window(): """Return 3dsmax's main window""" return MaxPlus.GetQMaxMainWindow() # ---------------------------------------------------------------------- # Run functions # ---------------------------------------------------------------------- class BoilerplateRunner: def __init__(self, guiClass=Boilerplate, win_title=WTITLE, win_object=WOBJ): # noqa self.guiClass = guiClass self.window_title = win_title self.window_object = win_object self.boil = None def run_maya(self, **kwargs): """Run in Maya""" # Delete any existing existing UI _maya_delete_ui(self.window_title, self.window_object) # Delete any existing existing workspace _maya_delete_workspace(self.window_object) self.boil = self.guiClass( _maya_main_window(), self.window_title, self.window_object ) # Makes Maya perform magic which makes the window stay # on top in OS X and Linux. As an added bonus, it'll # make Maya remember the window position self.boil.setProperty("saveWindowPref", True) if "dockable" in kwargs and kwargs["dockable"]: kwargs["allowed_areas"] = ["right", "left"] self.boil.show(**kwargs) if "dockable" in kwargs and kwargs["dockable"]: _maya_update_workspace(self.window_object) def run_nuke(self, **kwargs): """Run in Nuke Note: If you want the UI to always stay on top, replace: `boil.ui.setWindowFlags(QtCore.Qt.Tool)` with: `boil.ui.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)` If you want the UI to be modal: `boil.ui.setWindowModality(QtCore.Qt.WindowModal)` """ _nuke_delete_ui(self.window_object) # Delete any alrady existing UI if "dockable" in kwargs and kwargs["dockable"]: widgetname = ("{}.{}").format( self.guiClass.__module__, self.guiClass.__name__ ) panel = nukescripts.panels.registerWidgetAsPanel( widget=widgetname, # module_name.Class_name name=self.window_title, id="uk.co.thefoundry." + self.window_title, create=True, ) pane = nuke.getPaneFor("Properties.1") panel.addToPane(pane) self.boil = panel.customKnob.getObject().widget _nuke_set_zero_margins(self.boil) else: self.boil = self.guiClass( _nuke_main_window(), self.window_title, self.window_object ) self.boil.setWindowFlags(QtCore.Qt.Tool) self.boil.show() # Show the UI def run_houdini(self, **kwargs): """Run in Houdini""" self.boil = self.guiClass( _houdini_main_window(), self.window_title, self.window_object ) self.boil.show() def run_3dsmax(self, **kwargs): """Run in 3dsmax""" # https://gist.github.com/mrabito/0f9d1f177a3bea94d33d35b476c88731 # dockable? # https://help.autodesk.com/view/3DSMAX/2019/ENU/?guid=__py_ref_demo_py_side_tool_bar_q_widget_8py_example_html self.boil = self.guiClass( _3dsmax_main_window(), self.window_title, self.window_object ) self.boil.show() def on_exit_unreal(self): app = QtWidgets.QApplication.instance() if app: app.store_window_geometry() app.quit() def run_unreal(self, **kwargs): # https://github.com/20tab/UnrealEnginePython # https://forums.unrealengine.com/development-discussion/python-scripting/1674204-dock-qtwidget-to-unreal # https://github.com/AlexQuevillon/UnrealPythonLibrary/blob/master/UnrealProject/UnrealPythonLibrary/Plugins/UnrealPythonLibraryPlugin/Content/Python/PythonLibraries/QtFunctions.py # https://forums.unrealengine.com/unreal-engine/unreal-studio/1526501-how-to-get-the-main-window-of-the-editor-to-parent-qt-or-pyside-application-to-it app = QtWidgets.QApplication.instance() if not app: # create the first instance app = QtWidgets.QApplication(sys.argv) app.aboutToQuit.connect(self.on_exit_unreal) atexit.register(self.on_exit_unreal) self.boil = None self.event_loop = QtCore.QEventLoop() self.boil = self.guiClass(None, self.window_title, self.window_object) mayapalette.set_maya_palette_with_tweaks(PALETTE_FILEPATH) unreal.parent_external_window_to_slate(self.boil.winId()) self.boil.show() def on_exit_blender(self): """ Close BlenderApplication instance on exit Returns: None """ app = QtWidgets.QApplication.instance() if app: app.store_window_geometry() app.quit() def on_update_blender(self): # https://github.com/techartorg/bqt app = QtWidgets.QApplication.instance() if app.should_close: win = bpy.context.window_manager.windows[0] bpy.ops.wm.quit_blender({"window": win}, "INVOKE_DEFAULT") def run_blender(self, **kwargs): """Run in Blender""" # mix of code from # https://github.com/friedererdmann/blender_pyside2_example # and # https://github.com/techartorg/bqt # TODO add dockable? # https://github.com/cube-creative/guibedos/blob/master/guibedos/blender.py app = QtWidgets.QApplication.instance() if not app: # create the first instance app = QtWidgets.QApplication(sys.argv) # modal bpy.app.timers.register(self.on_update_blender, persistent=True) atexit.register(self.on_exit_blender) self.event_loop = QtCore.QEventLoop() self.boil = self.guiClass(None, self.window_title, self.window_object) mayapalette.set_maya_palette_with_tweaks(PALETTE_FILEPATH) self.boil.show() # non-modal: # app.exec_() def run_standalone(self, **kwargs): """Run standalone Note: Styling the UI with the Maya palette on OS X when using the PySide/PyQt4 bindings result in various issues, which is why it is disabled by default when you're running this combo. .. _Issue #9: https://github.com/fredrikaverpil/pyvfx-boilerplate/issues/9 """ extrawin = True app = QtWidgets.QApplication.instance() if not app: app = QtWidgets.QApplication(sys.argv) extrawin = False self.boil = self.guiClass( win_title=self.window_title, win_object=self.window_object ) if not (platform.system() == "Darwin" and (Qt.IsPySide or Qt.IsPyQt4)): mayapalette.set_maya_palette_with_tweaks(PALETTE_FILEPATH) self.boil.show() # Show the UI if not extrawin: sys.exit(app.exec_()) def run_main(self, **kwargs): """Run appropriate gui""" if MAYA: self.run_maya(**kwargs) elif NUKE: self.run_nuke(**kwargs) elif HOUDINI: self.run_houdini(**kwargs) elif THREEDSMAX: self.run_3dsmax(**kwargs) elif BLENDER: self.run_blender(**kwargs) elif UNREAL: self.run_unreal(**kwargs) else: self.run_standalone(**kwargs) ================================================ FILE: src/pyvfx_boilerplate/cli.py ================================================ #!/usr/bin/env python import os import sys # this is required to keep pyside2 seperate from maya and nuke's pyside2 # make sure this is added to any inherited program to work from cli if "QT_PREFERRED_PATH" in os.environ: sys.path.append(os.environ["QT_PREFERRED_PATH"]) from pyvfx_boilerplate import menu # isort:skip def main(): menu.activate() if __name__ == "__main__": main() ================================================ FILE: src/pyvfx_boilerplate/init.py ================================================ ================================================ FILE: src/pyvfx_boilerplate/mayapalette.py ================================================ import json from Qt import QtGui, QtWidgets def set_palette_from_dict(dct): """Set palette to current QApplication based on given dictionary""" groups = ["Disabled", "Active", "Inactive", "Normal"] roles = [ "AlternateBase", "Background", "Base", "Button", "ButtonText", "BrightText", "Dark", "Foreground", "Highlight", "HighlightedText", "Light", "Link", "LinkVisited", "Mid", "Midlight", "Shadow", "ToolTipBase", "ToolTipText", "Text", "Window", "WindowText", ] palette = QtGui.QPalette() for role in roles: try: for group in groups: color = QtGui.QColor(dct["%s:%s" % (role, group)]) qGrp = getattr(QtGui.QPalette, group) qRl = getattr(QtGui.QPalette, role) palette.setColor(qGrp, qRl, color) except: # noqa print("Could not use: " + str(palette)) try: QtWidgets.QApplication.setPalette(palette) except: # noqa print("Could not set palette: " + str(palette)) def set_style(): """Set style""" available_styles = QtWidgets.QStyleFactory.keys() if "Fusion" in available_styles: QtWidgets.QApplication.setStyle("Fusion") elif "Plastique" in available_styles: QtWidgets.QApplication.setStyle("Plastique") def set_maya_tweaks(): """Apply Maya-specific styling""" base_palette = QtWidgets.QApplication.palette() # Set custom colors LIGHT_COLOR = QtGui.QColor(100, 100, 100) MID_COLOR = QtGui.QColor(68, 68, 68) # Create a new palette tab_palette = QtGui.QPalette(base_palette) tab_palette.setBrush(QtGui.QPalette.Window, QtGui.QBrush(LIGHT_COLOR)) tab_palette.setBrush(QtGui.QPalette.Button, QtGui.QBrush(MID_COLOR)) # Define the widgets that needs tweaking widget_palettes = {} widget_palettes["QTabBar"] = tab_palette widget_palettes["QTabWidget"] = tab_palette # Set the new tweaked palette for name, palette in widget_palettes.items(): QtWidgets.QApplication.setPalette(palette, name) def read_json(filepath): """Read given JSON filepath into dictionary""" with open(filepath, "r") as data_file: data = json.load(data_file) return data def set_maya_palette_with_tweaks(palette_filepath): """Apply styling to current QApplication""" data = read_json(palette_filepath) set_palette_from_dict(data) set_style() set_maya_tweaks() ================================================ FILE: src/pyvfx_boilerplate/menu.py ================================================ import pyvfx_boilerplate.boilerplate_ui as bpui try: import maya.cmds as cmds import maya.mel as mel MAYA = True except ImportError: MAYA = False try: import nuke NUKE = True except ImportError: NUKE = False try: import hou # noqa HOUDINI = True except ImportError: HOUDINI = False try: import MaxPlus THREEDSMAX = True except ImportError: THREEDSMAX = False try: import bpy BLENDER = True except ImportError: BLENDER = False try: import unreal # noqa UNREAL = True except ImportError: UNREAL = False rootMenuName = "pyvfx" def activate(dockable=False): bpr = bpui.BoilerplateRunner(bpui.Boilerplate) kwargs = {} kwargs["dockable"] = dockable bpr.run_main(**kwargs) mcmd = "import pyvfx_boilerplate.menu\npyvfx_boilerplate.menu.activate({})" if NUKE: m = nuke.menu("Nuke") menuname = "{}/boilerplate UI".format(rootMenuName) m.addCommand("{}/boilerplate UI".format(rootMenuName), mcmd.format("")) m.addCommand("{} dockable".format(menuname), mcmd.format("True")) elif MAYA: MainMayaWindow = mel.eval("$temp = $gMainWindow") if not cmds.menu("pyvfxMenuItemRoot", exists=True): cmds.menu( "pyvfxMenuItemRoot", label=rootMenuName, parent=MainMayaWindow, tearOff=True, allowOptionBoxes=True, ) cmds.menuItem( label="boilerplate UI", parent="pyvfxMenuItemRoot", ec=True, command=mcmd.format(""), ) cmds.menuItem( label="boilerplate UI dockable", parent="pyvfxMenuItemRoot", ec=True, command=mcmd.format("True"), ) elif HOUDINI: print("add menu code here for Houdini") activate() elif UNREAL: # http://golaem.com/content/doc/golaem-crowd-documentation/rendering-unreal-engine # https://github.com/AlexQuevillon/UnrealPythonLibrary/tree/master/UnrealProject/UnrealPythonLibrary print("add menu code here for Unreal") activate() elif THREEDSMAX: def activate_dockable(): activate(True) MaxPlus.MenuManager.UnregisterMenu(rootMenuName) mb = MaxPlus.MenuBuilder(rootMenuName) menuitem = "boilerplate UI" menuitemD = "{} dockable".format(menuitem) menulist = [ (menuitem, menuitem, activate), (menuitemD, menuitemD, activate_dockable), ] for item in menulist: action = MaxPlus.ActionFactory.Create(item[0], item[1], item[2]) mb.AddItem(action) menu = mb.Create(MaxPlus.MenuManager.GetMainMenu()) elif BLENDER: # a little of This # https://blender.stackexchange.com/questions/156652/topbar-ht-upper-bar-append-add-menus-in-two-places # and a little of this # https://blenderartists.org/t/creating-a-custom-menu-option/627316/4 class PyvfxBoilerplateActivateOperator(bpy.types.Operator): """start the pyvfx_boilerplate ui""" bl_idname = "pyvfx_boilerplate_activate" bl_label = "boilerplate UI" def execute(self, context): activate() return {"FINISHED"} class TOPBAR_MT_pyvfx_menu(bpy.types.Menu): """create the pyvfx top menu and pyvfx menu item""" bl_label = rootMenuName def draw(self, context): """create the pyvfx menu item""" layout = self.layout layout.operator("pyvfx_boilerplate_activate") def menu_draw(self, context): """create the pyvfx top menu""" self.layout.menu("TOPBAR_MT_pyvfx_menu") def register(): bpy.utils.register_class(PyvfxBoilerplateActivateOperator) bpy.utils.register_class(TOPBAR_MT_pyvfx_menu) bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR_MT_pyvfx_menu.menu_draw) def unregister(): bpy.utils.unregister_class(PyvfxBoilerplateActivateOperator) bpy.types.TOPBAR_MT_editor_menus.remove(TOPBAR_MT_pyvfx_menu.menu_draw) bpy.utils.unregister_class(TOPBAR_MT_pyvfx_menu) register() else: activate() ================================================ FILE: src/pyvfx_boilerplate/resources/main_window.ui ================================================ MainWindow 0 0 405 348 MainWindow PushButton ================================================ FILE: src/pyvfx_boilerplate/resources/module.ui ================================================ Form 0 0 400 300 Form QFrame::StyledPanel QFrame::Raised TextLabel ================================================ FILE: src/pyvfx_boilerplate/resources/qpalette_maya2015.json ================================================ { "Foreground:Active": 4291348680, "HighlightedText:Active": 4294967295, "Mid:Active": 4281150765, "WindowText:Normal": 4291348680, "Window:Disabled": 4282664004, "AlternateBase:Inactive": 4281216558, "Midlight:Normal": 4282071867, "HighlightedText:Inactive": 4294967295, "Text:Disabled": 4285098345, "Midlight:Disabled": 4282071867, "Midlight:Inactive": 4282071867, "Highlight:Disabled": 4283856276, "Base:Disabled": 4280953386, "Window:Active": 4282664004, "Background:Inactive": 4282664004, "AlternateBase:Disabled": 4281216558, "Button:Inactive": 4284769380, "AlternateBase:Active": 4281216558, "LinkVisited:Active": 4283570315, "ToolTipBase:Inactive": 4294967260, "Dark:Inactive": 4280624421, "LinkVisited:Inactive": 4283570315, "Dark:Disabled": 4280624421, "Light:Inactive": 4284572001, "ButtonText:Inactive": 4291348680, "Dark:Active": 4280624421, "Light:Disabled": 4284572001, "Button:Disabled": 4283453520, "Shadow:Inactive": 4278190080, "ToolTipBase:Disabled": 4294967260, "BrightText:Inactive": 4280624421, "ToolTipBase:Active": 4294967260, "Highlight:Active": 4284976562, "BrightText:Disabled": 4294967295, "Highlight:Inactive": 4284976562, "ToolTipText:Active": 4278190080, "Light:Active": 4284572001, "Button:Active": 4284769380, "ButtonText:Disabled": 4286611584, "HighlightedText:Normal": 4294967295, "Base:Inactive": 4280953386, "BrightText:Active": 4280624421, "Highlight:Normal": 4284976562, "AlternateBase:Normal": 4281216558, "Dark:Normal": 4280624421, "Window:Inactive": 4282664004, "LinkVisited:Normal": 4283570315, "Link:Normal": 4278190318, "Base:Active": 4280953386, "Mid:Inactive": 4281150765, "Foreground:Disabled": 4286611584, "Text:Normal": 4291348680, "Link:Inactive": 4278190318, "ButtonText:Normal": 4291348680, "WindowText:Inactive": 4291348680, "ToolTipBase:Normal": 4294967260, "ToolTipText:Inactive": 4278190080, "WindowText:Disabled": 4286611584, "ToolTipText:Normal": 4278190080, "Midlight:Active": 4282071867, "ButtonText:Active": 4291348680, "BrightText:Normal": 4280624421, "Text:Active": 4291348680, "Mid:Normal": 4281150765, "WindowText:Active": 4291348680, "Base:Normal": 4280953386, "Background:Normal": 4282664004, "Mid:Disabled": 4281150765, "Background:Disabled": 4282664004, "Link:Disabled": 4278190318, "Window:Normal": 4282664004, "Shadow:Active": 4278190080, "Text:Inactive": 4291348680, "Button:Normal": 4284769380, "Light:Normal": 4284572001, "Link:Active": 4278190318, "Background:Active": 4282664004, "HighlightedText:Disabled": 4294967295, "Shadow:Disabled": 4278190080, "ToolTipText:Disabled": 4278190080, "Foreground:Normal": 4291348680, "LinkVisited:Disabled": 4283570315, "Shadow:Normal": 4278190080, "Foreground:Inactive": 4291348680 } ================================================ FILE: src/pyvfx_boilerplate/resources/qpalette_maya2016.json ================================================ { "Foreground:Active": 4290493371, "HighlightedText:Active": 4294967295, "Mid:Active": 4281150765, "WindowText:Normal": 4290493371, "Window:Disabled": 4282664004, "AlternateBase:Inactive": 4281216558, "Midlight:Normal": 4281808695, "HighlightedText:Inactive": 4294967295, "Text:Disabled": 4285098345, "Midlight:Disabled": 4281808695, "Midlight:Inactive": 4281808695, "Highlight:Disabled": 4283590260, "Base:Disabled": 4281019179, "Window:Active": 4282664004, "Background:Inactive": 4282664004, "AlternateBase:Disabled": 4281216558, "Button:Inactive": 4284308829, "AlternateBase:Active": 4281216558, "LinkVisited:Active": 4283570315, "ToolTipBase:Inactive": 4294967260, "Dark:Inactive": 4280624421, "LinkVisited:Inactive": 4283570315, "Dark:Disabled": 4280624421, "Light:Inactive": 4284572001, "ButtonText:Inactive": 4293848814, "Dark:Active": 4280624421, "Light:Disabled": 4284572001, "Button:Disabled": 4283124555, "Shadow:Inactive": 4278190080, "ToolTipBase:Disabled": 4294967260, "BrightText:Inactive": 4280624421, "ToolTipBase:Active": 4294967260, "Highlight:Active": 4283598246, "BrightText:Disabled": 4294967295, "Highlight:Inactive": 4283598246, "ToolTipText:Active": 4278190080, "Light:Active": 4284572001, "Button:Active": 4284308829, "ButtonText:Disabled": 4286611584, "HighlightedText:Normal": 4294967295, "Base:Inactive": 4281019179, "BrightText:Active": 4280624421, "Highlight:Normal": 4283598246, "AlternateBase:Normal": 4281216558, "Dark:Normal": 4280624421, "Window:Inactive": 4282664004, "LinkVisited:Normal": 4283570315, "Link:Normal": 4278190318, "Base:Active": 4281019179, "Mid:Inactive": 4281150765, "Foreground:Disabled": 4286611584, "Text:Normal": 4291348680, "Link:Inactive": 4278190318, "ButtonText:Normal": 4293848814, "WindowText:Inactive": 4290493371, "ToolTipBase:Normal": 4294967260, "ToolTipText:Inactive": 4278190080, "WindowText:Disabled": 4286611584, "ToolTipText:Normal": 4278190080, "Midlight:Active": 4281808695, "ButtonText:Active": 4293848814, "BrightText:Normal": 4280624421, "Text:Active": 4291348680, "Mid:Normal": 4281150765, "WindowText:Active": 4290493371, "Base:Normal": 4281019179, "Background:Normal": 4282664004, "Mid:Disabled": 4281150765, "Background:Disabled": 4282664004, "Link:Disabled": 4278190318, "Window:Normal": 4282664004, "Shadow:Active": 4278190080, "Text:Inactive": 4291348680, "Button:Normal": 4284308829, "Light:Normal": 4284572001, "Link:Active": 4278190318, "Background:Active": 4282664004, "HighlightedText:Disabled": 4294967295, "Shadow:Disabled": 4278190080, "ToolTipText:Disabled": 4278190080, "Foreground:Normal": 4290493371, "LinkVisited:Disabled": 4283570315, "Shadow:Normal": 4278190080, "Foreground:Inactive": 4290493371 } ================================================ FILE: src/pyvfx_boilerplate/utils/__init__.py ================================================ ================================================ FILE: src/pyvfx_boilerplate/utils/get_set_palette_data_qt4.py ================================================ """Set and get QPalette data from Maya # Example: fetch palette data from Maya data = getPaletteInfo() print data write_json(data) # Example: read palette JSON file and set palette data = read_json() print data setPaletteFromDict(data) setStylePlastique() setMayaTweaks() """ import json from PySide import QtGui STYLE = "plastique" GROUPS = ["Disabled", "Active", "Inactive", "Normal"] ROLES = [ "AlternateBase", "Background", "Base", "Button", "ButtonText", "BrightText", "Dark", "Foreground", "Highlight", "HighlightedText", "Light", "Link", "LinkVisited", "Mid", "Midlight", "Shadow", "ToolTipBase", "ToolTipText", "Text", "Window", "WindowText", ] def getPaletteInfo(): palette = QtGui.QApplication.palette() # ColorGroups groups = [] for name in dir(QtGui.QPalette): curr_pallet = getattr(QtGui.QPalette, name) if isinstance(curr_pallet, QtGui.QPalette.ColorGroup): if name != "All" and name != "NColorGroups" and name != "Current": print("ColorGroup: {}".format(name)) groups.append(name) # ColorRoles roles = [] for name in dir(QtGui.QPalette): if isinstance(getattr(QtGui.QPalette, name), QtGui.QPalette.ColorRole): if name != "NColorRoles" and name != "NoRole": print("ColorGroup: {}".format(name)) roles.append(name) # build a dict with all the colors result = {} for role in ROLES: for group in GROUPS: qGrp = getattr(QtGui.QPalette, group) qRl = getattr(QtGui.QPalette, role) result["%s:%s" % (role, group)] = palette.color(qGrp, qRl).rgba() return result def setPaletteFromDict(dct): palette = QtGui.QPalette() for role in ROLES: for group in GROUPS: color = QtGui.QColor(dct["%s:%s" % (role, group)]) qGrp = getattr(QtGui.QPalette, group) qRl = getattr(QtGui.QPalette, role) palette.setColor(qGrp, qRl, color) QtGui.QApplication.setPalette(palette) def setStylePlastique(): QtGui.QApplication.setStyle(STYLE) def setMayaTweaks(): base_palette = QtGui.QApplication.palette() # Set custom colors LIGHT_COLOR = QtGui.QColor(100, 100, 100) MID_COLOR = QtGui.QColor(68, 68, 68) # Create a new palette tab_palette = QtGui.QPalette(base_palette) tab_palette.setBrush(QtGui.QPalette.Window, QtGui.QBrush(LIGHT_COLOR)) tab_palette.setBrush(QtGui.QPalette.Button, QtGui.QBrush(MID_COLOR)) # Define the widgets that needs tweaking widget_palettes = {} widget_palettes["QTabBar"] = tab_palette widget_palettes["QTabWidget"] = tab_palette # Set the new tweaked palette for name, palette in widget_palettes.items(): QtGui.QApplication.setPalette(palette, name) def write_json(data): with open("/Users/fredrik/Desktop/qpalette.json", "w") as outfile: json.dump(data, outfile) def read_json(): # read with open("/Users/fredrik/Desktop/qpalette.json", "r") as handle: data = json.load(handle) return data ================================================ FILE: src/pyvfx_boilerplate/utils/get_set_palette_data_qt5.py ================================================ """Set and get QPalette data from Maya # Example: fetch palette data from Maya data = getPaletteInfo() print data write_json(data) # Example: read palette JSON file and set palette data = read_json() print data setPaletteFromDict(data) setStylePlastique() setMayaTweaks() """ import json from PySide2 import QtGui STYLE = "plastique" GROUPS = ["Disabled", "Active", "Inactive", "Normal"] ROLES = [ "AlternateBase", "Background", "Base", "Button", "ButtonText", "BrightText", "Dark", "Foreground", "Highlight", "HighlightedText", "Light", "Link", "LinkVisited", "Mid", "Midlight", "Shadow", "ToolTipBase", "ToolTipText", "Text", "Window", "WindowText", ] def getPaletteInfo(): palette = QtGui.QGuiApplication.palette() # ColorGroups groups = [] for name in dir(QtGui.QPalette): curr_pallet = getattr(QtGui.QPalette, name) if isinstance(curr_pallet, QtGui.QPalette.ColorGroup): if name != "All" and name != "NColorGroups" and name != "Current": print("ColorGroup: {}".format(name)) groups.append(name) # ColorRoles roles = [] for name in dir(QtGui.QPalette): if isinstance(getattr(QtGui.QPalette, name), QtGui.QPalette.ColorRole): if name != "NColorRoles" and name != "NoRole": print("ColorGroup: {}".format(name)) roles.append(name) # build a dict with all the colors result = {} for role in ROLES: for group in GROUPS: qGrp = getattr(QtGui.QPalette, group) qRl = getattr(QtGui.QPalette, role) result["%s:%s" % (role, group)] = palette.color(qGrp, qRl).rgba() return result def setPaletteFromDict(dct): palette = QtGui.QPalette() for role in ROLES: for group in GROUPS: color = QtGui.QColor(dct["%s:%s" % (role, group)]) qGrp = getattr(QtGui.QPalette, group) qRl = getattr(QtGui.QPalette, role) palette.setColor(qGrp, qRl, color) QtGui.QApplication.setPalette(palette) def setStylePlastique(): QtGui.QApplication.setStyle(STYLE) def setMayaTweaks(): base_palette = QtGui.QApplication.palette() # Set custom colors LIGHT_COLOR = QtGui.QColor(100, 100, 100) MID_COLOR = QtGui.QColor(68, 68, 68) # Create a new palette tab_palette = QtGui.QPalette(base_palette) tab_palette.setBrush(QtGui.QPalette.Window, QtGui.QBrush(LIGHT_COLOR)) tab_palette.setBrush(QtGui.QPalette.Button, QtGui.QBrush(MID_COLOR)) # Define the widgets that needs tweaking widget_palettes = {} widget_palettes["QTabBar"] = tab_palette widget_palettes["QTabWidget"] = tab_palette # Set the new tweaked palette for name, palette in widget_palettes.items(): QtGui.QApplication.setPalette(palette, name) def write_json(data): with open("/Users/fredrik/Desktop/qpalette.json", "w") as outfile: json.dump(data, outfile) def read_json(): # read with open("/Users/fredrik/Desktop/qpalette.json", "r") as handle: data = json.load(handle) return data