diff --git a/CHANGELOG.md b/CHANGELOG.md index a5c928273d..e9405ff759 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,26 +1,48 @@ # Changelog -## [3.7.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.7.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.4...HEAD) +### 📖 Documentation + +- docs\[website\]: Add Ellipse Studio \(logo\) as an OpenPype contributor [\#2324](https://github.com/pypeclub/OpenPype/pull/2324) + +**🆕 New features** + +- Store typed version dependencies for workfiles [\#2192](https://github.com/pypeclub/OpenPype/pull/2192) + **🚀 Enhancements** +- Hiero: Add experimental tools action [\#2323](https://github.com/pypeclub/OpenPype/pull/2323) +- Input links: Cleanup and unification of differences [\#2322](https://github.com/pypeclub/OpenPype/pull/2322) +- General: Run process log stderr as info log level [\#2309](https://github.com/pypeclub/OpenPype/pull/2309) +- Tools: Cleanup of unused classes [\#2304](https://github.com/pypeclub/OpenPype/pull/2304) +- Project Manager: Added ability to delete project [\#2298](https://github.com/pypeclub/OpenPype/pull/2298) - Ftrack: Synchronize input links [\#2287](https://github.com/pypeclub/OpenPype/pull/2287) - StandalonePublisher: Remove unused plugin ExtractHarmonyZip [\#2277](https://github.com/pypeclub/OpenPype/pull/2277) - Ftrack: Support multiple reviews [\#2271](https://github.com/pypeclub/OpenPype/pull/2271) - Ftrack: Remove unused clean component plugin [\#2269](https://github.com/pypeclub/OpenPype/pull/2269) -- Royal Render: Support for rr channels in separate dirs [\#2268](https://github.com/pypeclub/OpenPype/pull/2268) - Houdini: Add experimental tools action [\#2267](https://github.com/pypeclub/OpenPype/pull/2267) +- Tools: Assets widget [\#2265](https://github.com/pypeclub/OpenPype/pull/2265) +- Nuke: extract baked review videos presets [\#2248](https://github.com/pypeclub/OpenPype/pull/2248) +- TVPaint: Workers rendering [\#2209](https://github.com/pypeclub/OpenPype/pull/2209) **🐛 Bug fixes** +- Fix - provider icons are pulled from a folder [\#2326](https://github.com/pypeclub/OpenPype/pull/2326) +- InputLinks: Typo in "inputLinks" key [\#2314](https://github.com/pypeclub/OpenPype/pull/2314) +- Deadline timeout and logging [\#2312](https://github.com/pypeclub/OpenPype/pull/2312) +- nuke: do not multiply representation on class method [\#2311](https://github.com/pypeclub/OpenPype/pull/2311) +- Workfiles tool: Fix task formatting [\#2306](https://github.com/pypeclub/OpenPype/pull/2306) +- Delivery: Fix delivery paths created on windows [\#2302](https://github.com/pypeclub/OpenPype/pull/2302) - Maya: Deadline - fix limit groups [\#2295](https://github.com/pypeclub/OpenPype/pull/2295) - New Publisher: Fix mapping of indexes [\#2285](https://github.com/pypeclub/OpenPype/pull/2285) - Alternate site for site sync doesnt work for sequences [\#2284](https://github.com/pypeclub/OpenPype/pull/2284) - FFmpeg: Execute ffprobe using list of arguments instead of string command [\#2281](https://github.com/pypeclub/OpenPype/pull/2281) - Nuke: Anatomy fill data use task as dictionary [\#2278](https://github.com/pypeclub/OpenPype/pull/2278) - Bug: fix variable name \_asset\_id in workfiles application [\#2274](https://github.com/pypeclub/OpenPype/pull/2274) +- Version handling fixes [\#2272](https://github.com/pypeclub/OpenPype/pull/2272) ## [3.6.4](https://github.com/pypeclub/OpenPype/tree/3.6.4) (2021-11-23) @@ -44,23 +66,22 @@ **🚀 Enhancements** -- Tools: Assets widget [\#2265](https://github.com/pypeclub/OpenPype/pull/2265) +- Royal Render: Support for rr channels in separate dirs [\#2268](https://github.com/pypeclub/OpenPype/pull/2268) - SceneInventory: Choose loader in asset switcher [\#2262](https://github.com/pypeclub/OpenPype/pull/2262) - Style: New fonts in OpenPype style [\#2256](https://github.com/pypeclub/OpenPype/pull/2256) - Tools: SceneInventory in OpenPype [\#2255](https://github.com/pypeclub/OpenPype/pull/2255) - Tools: Tasks widget [\#2251](https://github.com/pypeclub/OpenPype/pull/2251) -- Tools: Creator in OpenPype [\#2244](https://github.com/pypeclub/OpenPype/pull/2244) - Added endpoint for configured extensions [\#2221](https://github.com/pypeclub/OpenPype/pull/2221) **🐛 Bug fixes** -- Version handling fixes [\#2272](https://github.com/pypeclub/OpenPype/pull/2272) - Tools: Parenting of tools in Nuke and Hiero [\#2266](https://github.com/pypeclub/OpenPype/pull/2266) - limiting validator to specific editorial hosts [\#2264](https://github.com/pypeclub/OpenPype/pull/2264) - Tools: Select Context dialog attribute fix [\#2261](https://github.com/pypeclub/OpenPype/pull/2261) - Maya: Render publishing fails on linux [\#2260](https://github.com/pypeclub/OpenPype/pull/2260) - LookAssigner: Fix tool reopen [\#2259](https://github.com/pypeclub/OpenPype/pull/2259) - Standalone: editorial not publishing thumbnails on all subsets [\#2258](https://github.com/pypeclub/OpenPype/pull/2258) +- Loader doesn't allow changing of version before loading [\#2254](https://github.com/pypeclub/OpenPype/pull/2254) - Burnins: Support mxf metadata [\#2247](https://github.com/pypeclub/OpenPype/pull/2247) - Maya: Support for configurable AOV separator characters [\#2197](https://github.com/pypeclub/OpenPype/pull/2197) - Maya: texture colorspace modes in looks [\#2195](https://github.com/pypeclub/OpenPype/pull/2195) @@ -69,10 +90,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.1-nightly.1...3.6.1) -**🐛 Bug fixes** - -- Loader doesn't allow changing of version before loading [\#2254](https://github.com/pypeclub/OpenPype/pull/2254) - ## [3.6.0](https://github.com/pypeclub/OpenPype/tree/3.6.0) (2021-11-15) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.0-nightly.6...3.6.0) @@ -82,14 +99,9 @@ - Add alternative sites for Site Sync [\#2206](https://github.com/pypeclub/OpenPype/pull/2206) - Add command line way of running site sync server [\#2188](https://github.com/pypeclub/OpenPype/pull/2188) -**🆕 New features** - -- Add validate active site button to sync queue on a project [\#2176](https://github.com/pypeclub/OpenPype/pull/2176) -- Maya : Colorspace configuration [\#2170](https://github.com/pypeclub/OpenPype/pull/2170) -- Blender: Added support for audio [\#2168](https://github.com/pypeclub/OpenPype/pull/2168) - **🚀 Enhancements** +- Tools: Creator in OpenPype [\#2244](https://github.com/pypeclub/OpenPype/pull/2244) - Tools: Subset manager in OpenPype [\#2243](https://github.com/pypeclub/OpenPype/pull/2243) - General: Skip module directories without init file [\#2239](https://github.com/pypeclub/OpenPype/pull/2239) - General: Static interfaces [\#2238](https://github.com/pypeclub/OpenPype/pull/2238) @@ -109,8 +121,6 @@ - Delivery: Check 'frame' key in template for sequence delivery [\#2196](https://github.com/pypeclub/OpenPype/pull/2196) - Settings: Site sync project settings improvement [\#2193](https://github.com/pypeclub/OpenPype/pull/2193) - Usage of tools code [\#2185](https://github.com/pypeclub/OpenPype/pull/2185) -- Settings: Dictionary based on project roots [\#2184](https://github.com/pypeclub/OpenPype/pull/2184) -- Subset name: Be able to pass asset document to get subset name [\#2179](https://github.com/pypeclub/OpenPype/pull/2179) **🐛 Bug fixes** @@ -126,9 +136,6 @@ - Tools: Workfiles tool don't use avalon widgets [\#2205](https://github.com/pypeclub/OpenPype/pull/2205) - Ftrack: Fill missing ftrack id on mongo project [\#2203](https://github.com/pypeclub/OpenPype/pull/2203) - Project Manager: Fix copying of tasks [\#2191](https://github.com/pypeclub/OpenPype/pull/2191) -- Blender: Fix trying to pack an image when the shader node has no texture [\#2183](https://github.com/pypeclub/OpenPype/pull/2183) -- Maya: review viewport settings [\#2177](https://github.com/pypeclub/OpenPype/pull/2177) -- Maya: Aspect ratio [\#2174](https://github.com/pypeclub/OpenPype/pull/2174) ## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17) diff --git a/igniter/tools.py b/igniter/tools.py index 04d7451335..3e862f5803 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -59,7 +59,7 @@ def validate_mongo_connection(cnx: str) -> (bool, str): return False, "Not mongodb schema" kwargs = { - "serverSelectionTimeoutMS": 2000 + "serverSelectionTimeoutMS": os.environ.get("AVALON_TIMEOUT", 2000) } # Add certificate path if should be required if should_add_certificate_path_to_mongo_url(cnx): diff --git a/openpype/hosts/aftereffects/api/__init__.py b/openpype/hosts/aftereffects/api/__init__.py index 5f6a64a6d0..b1edb91a5c 100644 --- a/openpype/hosts/aftereffects/api/__init__.py +++ b/openpype/hosts/aftereffects/api/__init__.py @@ -4,7 +4,7 @@ import logging from avalon import io from avalon import api as avalon -from avalon.vendor import Qt +from Qt import QtWidgets from openpype import lib, api import pyblish.api as pyblish import openpype.hosts.aftereffects @@ -41,10 +41,10 @@ def check_inventory(): # Warn about outdated containers. print("Starting new QApplication..") - app = Qt.QtWidgets.QApplication(sys.argv) + app = QtWidgets.QApplication(sys.argv) - message_box = Qt.QtWidgets.QMessageBox() - message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) + message_box = QtWidgets.QMessageBox() + message_box.setIcon(QtWidgets.QMessageBox.Warning) msg = "There are outdated containers in the scene." message_box.setText(msg) message_box.exec_() diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 4234ee0f0c..b796e9eaac 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -1,5 +1,5 @@ import openpype.api -from avalon.vendor import Qt +from Qt import QtWidgets from avalon import aftereffects import logging @@ -56,7 +56,7 @@ class CreateRender(openpype.api.Creator): stub.rename_item(item.id, stub.PUBLISH_ICON + self.data["subset"]) def _show_msg(self, txt): - msg = Qt.QtWidgets.QMessageBox() - msg.setIcon(Qt.QtWidgets.QMessageBox.Warning) + msg = QtWidgets.QMessageBox() + msg.setIcon(QtWidgets.QMessageBox.Warning) msg.setText(txt) msg.exec_() diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 77866fde9d..7afcdd82ea 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -1,6 +1,6 @@ import sys -from avalon.vendor.Qt import QtGui +from Qt import QtGui import avalon.fusion from avalon import io diff --git a/openpype/hosts/fusion/plugins/inventory/set_tool_color.py b/openpype/hosts/fusion/plugins/inventory/set_tool_color.py index 940a0e9941..9fc7012db7 100644 --- a/openpype/hosts/fusion/plugins/inventory/set_tool_color.py +++ b/openpype/hosts/fusion/plugins/inventory/set_tool_color.py @@ -1,5 +1,5 @@ from avalon import api, style -from avalon.vendor.Qt import QtGui, QtWidgets +from Qt import QtGui, QtWidgets import avalon.fusion diff --git a/openpype/hosts/fusion/scripts/set_rendermode.py b/openpype/hosts/fusion/scripts/set_rendermode.py index cb104445a8..73eec528a2 100644 --- a/openpype/hosts/fusion/scripts/set_rendermode.py +++ b/openpype/hosts/fusion/scripts/set_rendermode.py @@ -1,4 +1,4 @@ -from avalon.vendor.Qt import QtWidgets +from Qt import QtWidgets from avalon.vendor import qtawesome import avalon.fusion as avalon diff --git a/openpype/hosts/fusion/utility_scripts/switch_ui.py b/openpype/hosts/fusion/utility_scripts/switch_ui.py index e0b6b3f882..2be91af32a 100644 --- a/openpype/hosts/fusion/utility_scripts/switch_ui.py +++ b/openpype/hosts/fusion/utility_scripts/switch_ui.py @@ -2,12 +2,13 @@ import os import glob import logging +from Qt import QtWidgets, QtCore + import avalon.io as io import avalon.api as api import avalon.pipeline as pipeline import avalon.fusion import avalon.style as style -from avalon.vendor.Qt import QtWidgets, QtCore from avalon.vendor import qtawesome as qta diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index af58f5b73e..21b65e5c96 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -5,13 +5,13 @@ import os import re import sys import ast +import shutil import hiero +from Qt import QtWidgets import avalon.api as avalon import avalon.io -from avalon.vendor.Qt import QtWidgets from openpype.api import (Logger, Anatomy, get_anatomy_settings) from . import tags -import shutil from compiler.ast import flatten try: diff --git a/openpype/hosts/hiero/api/menu.py b/openpype/hosts/hiero/api/menu.py index 61b515d719..5aaab7a2e5 100644 --- a/openpype/hosts/hiero/api/menu.py +++ b/openpype/hosts/hiero/api/menu.py @@ -105,9 +105,9 @@ def menu_install(): sceneinventory_action.triggered.connect( lambda: host_tools.show_scene_inventory(parent=main_window) ) - menu.addSeparator() if os.getenv("OPENPYPE_DEVELOP"): + menu.addSeparator() reload_action = menu.addAction("Reload pipeline") reload_action.setIcon(QtGui.QIcon("icons:ColorAdd.png")) reload_action.triggered.connect(reload_config) @@ -120,3 +120,10 @@ def menu_install(): apply_colorspace_c_action = menu.addAction("Apply Colorspace Clips") apply_colorspace_c_action.setIcon(QtGui.QIcon("icons:ColorAdd.png")) apply_colorspace_c_action.triggered.connect(apply_colorspace_clips) + + menu.addSeparator() + + exeprimental_action = menu.addAction("Experimental tools...") + exeprimental_action.triggered.connect( + lambda: host_tools.show_experimental_tools_dialog(parent=main_window) + ) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index c46ef9abfa..75d1c1b18f 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -6,6 +6,7 @@ from avalon.vendor import qargparse import avalon.api as avalon import openpype.api as openpype from . import lib +from copy import deepcopy log = openpype.Logger().get_logger(__name__) @@ -799,7 +800,8 @@ class PublishClip: # increasing steps by index of rename iteration self.count_steps *= self.rename_index - hierarchy_formating_data = dict() + hierarchy_formating_data = {} + hierarchy_data = deepcopy(self.hierarchy_data) _data = self.track_item_default_data.copy() if self.ui_inputs: # adding tag metadata from ui @@ -824,19 +826,19 @@ class PublishClip: _data.update({"shot": self.shot_num}) # solve # in test to pythonic expression - for _k, _v in self.hierarchy_data.items(): + for _k, _v in hierarchy_data.items(): if "#" not in _v["value"]: continue - self.hierarchy_data[ + hierarchy_data[ _k]["value"] = self._replace_hash_to_expression( _k, _v["value"]) # fill up pythonic expresisons in hierarchy data - for k, _v in self.hierarchy_data.items(): + for k, _v in hierarchy_data.items(): hierarchy_formating_data[k] = _v["value"].format(**_data) else: # if no gui mode then just pass default data - hierarchy_formating_data = self.hierarchy_data + hierarchy_formating_data = hierarchy_data tag_hierarchy_data = self._solve_tag_hierarchy_data( hierarchy_formating_data @@ -886,30 +888,38 @@ class PublishClip: "families": [self.data["family"]] } - def _convert_to_entity(self, key): + def _convert_to_entity(self, type, template): """ Converting input key to key with type. """ # convert to entity type - entity_type = self.types.get(key, None) + entity_type = self.types.get(type, None) assert entity_type, "Missing entity type for `{}`".format( - key + type ) + # first collect formating data to use for formating template + formating_data = {} + for _k, _v in self.hierarchy_data.items(): + value = _v["value"].format( + **self.track_item_default_data) + formating_data[_k] = value + return { "entity_type": entity_type, - "entity_name": self.hierarchy_data[key]["value"].format( - **self.track_item_default_data + "entity_name": template.format( + **formating_data ) } def _create_parents(self): """ Create parents and return it in list. """ - self.parents = list() + self.parents = [] patern = re.compile(self.parents_search_patern) - par_split = [patern.findall(t).pop() + + par_split = [(patern.findall(t).pop(), t) for t in self.hierarchy.split("/")] - for key in par_split: - parent = self._convert_to_entity(key) + for type, template in par_split: + parent = self._convert_to_entity(type, template) self.parents.append(parent) diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index e330904abf..b25fd44217 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -138,7 +138,7 @@ def on_save(_): def on_open(_): """On scene open let's assume the containers have changed.""" - from avalon.vendor.Qt import QtWidgets + from Qt import QtWidgets from openpype.widgets import popup cmds.evalDeferred( diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 4074aa7fa8..52ebcaff64 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -6,19 +6,19 @@ import platform import uuid import math -import bson import json import logging import itertools import contextlib from collections import OrderedDict, defaultdict from math import ceil +from six import string_types +import bson from maya import cmds, mel import maya.api.OpenMaya as om from avalon import api, maya, io, pipeline -from avalon.vendor.six import string_types import avalon.maya.lib import avalon.maya.interactive @@ -1936,7 +1936,7 @@ def validate_fps(): if current_fps != fps: - from avalon.vendor.Qt import QtWidgets + from Qt import QtWidgets from ...widgets import popup # Find maya main window @@ -2694,7 +2694,7 @@ def update_content_on_context_change(): def show_message(title, msg): - from avalon.vendor.Qt import QtWidgets + from Qt import QtWidgets from openpype.widgets import message_window # Find maya main window diff --git a/openpype/hosts/maya/plugins/load/actions.py b/openpype/hosts/maya/plugins/load/actions.py index d4525511f4..1a9adf6142 100644 --- a/openpype/hosts/maya/plugins/load/actions.py +++ b/openpype/hosts/maya/plugins/load/actions.py @@ -133,7 +133,7 @@ class ImportMayaLoader(api.Loader): """ - from avalon.vendor.Qt import QtWidgets + from Qt import QtWidgets accept = QtWidgets.QMessageBox.Ok buttons = accept | QtWidgets.QMessageBox.Cancel diff --git a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py index 0d240b1a32..029432223b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py @@ -275,7 +275,7 @@ class CollectYetiRig(pyblish.api.InstancePlugin): list: file sequence. """ - from avalon.vendor import clique + import clique escaped = re.escape(filepath) re_pattern = escaped.replace(pattern, "-?[0-9]+") diff --git a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py index 207cf56cfe..ac3de4114c 100644 --- a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py +++ b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py @@ -1,13 +1,14 @@ import os import json import getpass -import appdirs import platform +import appdirs +import requests + from maya import cmds from avalon import api -from avalon.vendor import requests import pyblish.api from openpype.hosts.maya.api import lib diff --git a/openpype/hosts/maya/plugins/publish/validate_assembly_transforms.py b/openpype/hosts/maya/plugins/publish/validate_assembly_transforms.py index 00600a6f62..dca59b147b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_assembly_transforms.py +++ b/openpype/hosts/maya/plugins/publish/validate_assembly_transforms.py @@ -89,8 +89,8 @@ class ValidateAssemblyModelTransforms(pyblish.api.InstancePlugin): """ + from Qt import QtWidgets from openpype.hosts.maya.api import lib - from avalon.vendor.Qt import QtWidgets # Store namespace in variable, cosmetics thingy messagebox = QtWidgets.QMessageBox diff --git a/openpype/hosts/maya/plugins/publish/validate_muster_connection.py b/openpype/hosts/maya/plugins/publish/validate_muster_connection.py index 1a7ee11230..af32c82f97 100644 --- a/openpype/hosts/maya/plugins/publish/validate_muster_connection.py +++ b/openpype/hosts/maya/plugins/publish/validate_muster_connection.py @@ -1,9 +1,10 @@ import os import json + import appdirs +import requests import pyblish.api -from avalon.vendor import requests from openpype.plugin import contextplugin_should_run import openpype.hosts.maya.api.action diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index e6dab5cfc9..e684b48fa3 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -70,7 +70,8 @@ def install(): family_states = [ "write", "review", - "nukenodes" + "nukenodes", + "model", "gizmo" ] diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index e53b97e297..82299dd354 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -27,7 +27,7 @@ class PypeCreator(PypeCreatorMixin, avalon.nuke.pipeline.Creator): self.data["subset"]): msg = ("The subset name `{0}` is already used on a node in" "this workfile.".format(self.data["subset"])) - self.log.error(msg + '\n\nPlease use other subset name!') + self.log.error(msg + "\n\nPlease use other subset name!") raise NameError("`{0}: {1}".format(__name__, msg)) return @@ -53,7 +53,7 @@ class NukeLoader(api.Loader): container_id = None def reset_container_id(self): - self.container_id = ''.join(random.choice( + self.container_id = "".join(random.choice( string.ascii_uppercase + string.digits) for _ in range(10)) def get_container_id(self, node): @@ -61,7 +61,7 @@ class NukeLoader(api.Loader): return id_knob.value() if id_knob else None def get_members(self, source): - """Return nodes that has same 'containerId' as `source`""" + """Return nodes that has same "containerId" as `source`""" source_id = self.get_container_id(source) return [node for node in nuke.allNodes(recurseGroups=True) if self.get_container_id(node) == source_id @@ -116,11 +116,13 @@ class ExporterReview(object): def __init__(self, klass, - instance + instance, + multiple_presets=True ): self.log = klass.log self.instance = instance + self.multiple_presets = multiple_presets self.path_in = self.instance.data.get("path", None) self.staging_dir = self.instance.data["stagingDir"] self.collection = self.instance.data.get("collection", None) @@ -152,12 +154,10 @@ class ExporterReview(object): def get_representation_data(self, tags=None, range=False): add_tags = tags or [] - repre = { - 'outputName': self.name, - 'name': self.name, - 'ext': self.ext, - 'files': self.file, + "name": self.name, + "ext": self.ext, + "files": self.file, "stagingDir": self.staging_dir, "tags": [self.name.replace("_", "-")] + add_tags } @@ -168,6 +168,9 @@ class ExporterReview(object): "frameEnd": self.last_frame, }) + if self.multiple_presets: + repre["outputName"] = self.name + self.data["representations"].append(repre) def get_view_input_process_node(self): @@ -183,19 +186,19 @@ class ExporterReview(object): anlib.reset_selection() ipn_orig = None for v in nuke.allNodes(filter="Viewer"): - ip = v['input_process'].getValue() - ipn = v['input_process_node'].getValue() + ip = v["input_process"].getValue() + ipn = v["input_process_node"].getValue() if "VIEWER_INPUT" not in ipn and ip: ipn_orig = nuke.toNode(ipn) ipn_orig.setSelected(True) if ipn_orig: # copy selected to clipboard - nuke.nodeCopy('%clipboard%') + nuke.nodeCopy("%clipboard%") # reset selection anlib.reset_selection() # paste node and selection is on it only - nuke.nodePaste('%clipboard%') + nuke.nodePaste("%clipboard%") # assign to variable ipn = nuke.selectedNode() @@ -234,9 +237,11 @@ class ExporterReviewLut(ExporterReview): ext=None, cube_size=None, lut_size=None, - lut_style=None): + lut_style=None, + multiple_presets=True): # initialize parent class - super(ExporterReviewLut, self).__init__(klass, instance) + super(ExporterReviewLut, self).__init__( + klass, instance, multiple_presets) # deal with now lut defined in viewer lut if hasattr(klass, "viewer_lut_raw"): @@ -349,9 +354,11 @@ class ExporterReviewMov(ExporterReview): instance, name=None, ext=None, + multiple_presets=True ): # initialize parent class - super(ExporterReviewMov, self).__init__(klass, instance) + super(ExporterReviewMov, self).__init__( + klass, instance, multiple_presets) # passing presets for nodes to self self.nodes = klass.nodes if hasattr(klass, "nodes") else {} diff --git a/openpype/hosts/nuke/plugins/create/create_model.py b/openpype/hosts/nuke/plugins/create/create_model.py new file mode 100644 index 0000000000..4e30860e05 --- /dev/null +++ b/openpype/hosts/nuke/plugins/create/create_model.py @@ -0,0 +1,85 @@ +from avalon.nuke import lib as anlib +from openpype.hosts.nuke.api import plugin +import nuke + + +class CreateModel(plugin.PypeCreator): + """Add Publishable Model Geometry""" + + name = "model" + label = "Create 3d Model" + family = "model" + icon = "cube" + defaults = ["Main"] + + def __init__(self, *args, **kwargs): + super(CreateModel, self).__init__(*args, **kwargs) + self.nodes = nuke.selectedNodes() + self.node_color = "0xff3200ff" + return + + def process(self): + nodes = list() + if (self.options or {}).get("useSelection"): + nodes = self.nodes + for n in nodes: + n['selected'].setValue(0) + end_nodes = list() + + # get the latest nodes in tree for selecion + for n in nodes: + x = n + end = 0 + while end == 0: + try: + x = x.dependent()[0] + except: + end_node = x + end = 1 + end_nodes.append(end_node) + + # set end_nodes + end_nodes = list(set(end_nodes)) + + # check if nodes is 3d nodes + for n in end_nodes: + n['selected'].setValue(1) + sn = nuke.createNode("Scene") + if not sn.input(0): + end_nodes.remove(n) + nuke.delete(sn) + + # loop over end nodes + for n in end_nodes: + n['selected'].setValue(1) + + self.nodes = nuke.selectedNodes() + nodes = self.nodes + if len(nodes) >= 1: + # loop selected nodes + for n in nodes: + data = self.data.copy() + if len(nodes) > 1: + # rename subset name only if more + # then one node are selected + subset = self.family + n["name"].value().capitalize() + data["subset"] = subset + + # change node color + n["tile_color"].setValue(int(self.node_color, 16)) + # add avalon knobs + anlib.set_avalon_knob_data(n, data) + return True + else: + msg = str("Please select nodes you " + "wish to add to a container") + self.log.error(msg) + nuke.message(msg) + return + else: + # if selected is off then create one node + model_node = nuke.createNode("WriteGeo") + model_node["tile_color"].setValue(int(self.node_color, 16)) + # add avalon knobs + instance = anlib.set_avalon_knob_data(model_node, self.data) + return instance diff --git a/openpype/hosts/nuke/plugins/load/load_model.py b/openpype/hosts/nuke/plugins/load/load_model.py new file mode 100644 index 0000000000..15fa4fa35c --- /dev/null +++ b/openpype/hosts/nuke/plugins/load/load_model.py @@ -0,0 +1,187 @@ +from avalon import api, io +from avalon.nuke import lib as anlib +from avalon.nuke import containerise, update_container +import nuke + + +class AlembicModelLoader(api.Loader): + """ + This will load alembic model into script. + """ + + families = ["model"] + representations = ["abc"] + + label = "Load Alembic Model" + icon = "cube" + color = "orange" + node_color = "0x4ecd91ff" + + def load(self, context, name, namespace, data): + # get main variables + version = context['version'] + version_data = version.get("data", {}) + vname = version.get("name", None) + first = version_data.get("frameStart", None) + last = version_data.get("frameEnd", None) + fps = version_data.get("fps") or nuke.root()["fps"].getValue() + namespace = namespace or context['asset']['name'] + object_name = "{}_{}".format(name, namespace) + + # prepare data for imprinting + # add additional metadata from the version to imprint to Avalon knob + add_keys = ["source", "author", "fps"] + + data_imprint = {"frameStart": first, + "frameEnd": last, + "version": vname, + "objectName": object_name} + + for k in add_keys: + data_imprint.update({k: version_data[k]}) + + # getting file path + file = self.fname.replace("\\", "/") + + with anlib.maintained_selection(): + model_node = nuke.createNode( + "ReadGeo2", + "name {} file {} ".format( + object_name, file), + inpanel=False + ) + model_node.forceValidate() + model_node["frame_rate"].setValue(float(fps)) + + # workaround because nuke's bug is not adding + # animation keys properly + xpos = model_node.xpos() + ypos = model_node.ypos() + nuke.nodeCopy("%clipboard%") + nuke.delete(model_node) + nuke.nodePaste("%clipboard%") + model_node = nuke.toNode(object_name) + model_node.setXYpos(xpos, ypos) + + # color node by correct color by actual version + self.node_version_color(version, model_node) + + return containerise( + node=model_node, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + data=data_imprint) + + def update(self, container, representation): + """ + Called by Scene Inventory when look should be updated to current + version. + If any reference edits cannot be applied, eg. shader renamed and + material not present, reference is unloaded and cleaned. + All failed edits are highlighted to the user via message box. + + Args: + container: object that has look to be updated + representation: (dict): relationship data to get proper + representation from DB and persisted + data in .json + Returns: + None + """ + # Get version from io + version = io.find_one({ + "type": "version", + "_id": representation["parent"] + }) + object_name = container['objectName'] + # get corresponding node + model_node = nuke.toNode(object_name) + + # get main variables + version_data = version.get("data", {}) + vname = version.get("name", None) + first = version_data.get("frameStart", None) + last = version_data.get("frameEnd", None) + fps = version_data.get("fps") or nuke.root()["fps"].getValue() + + # prepare data for imprinting + # add additional metadata from the version to imprint to Avalon knob + add_keys = ["source", "author", "fps"] + + data_imprint = {"representation": str(representation["_id"]), + "frameStart": first, + "frameEnd": last, + "version": vname, + "objectName": object_name} + + for k in add_keys: + data_imprint.update({k: version_data[k]}) + + # getting file path + file = api.get_representation_path(representation).replace("\\", "/") + + with anlib.maintained_selection(): + model_node = nuke.toNode(object_name) + model_node['selected'].setValue(True) + + # collect input output dependencies + dependencies = model_node.dependencies() + dependent = model_node.dependent() + + model_node["frame_rate"].setValue(float(fps)) + model_node["file"].setValue(file) + + # workaround because nuke's bug is + # not adding animation keys properly + xpos = model_node.xpos() + ypos = model_node.ypos() + nuke.nodeCopy("%clipboard%") + nuke.delete(model_node) + nuke.nodePaste("%clipboard%") + model_node = nuke.toNode(object_name) + model_node.setXYpos(xpos, ypos) + + # link to original input nodes + for i, input in enumerate(dependencies): + model_node.setInput(i, input) + # link to original output nodes + for d in dependent: + index = next((i for i, dpcy in enumerate( + d.dependencies()) + if model_node is dpcy), 0) + d.setInput(index, model_node) + + # color node by correct color by actual version + self.node_version_color(version, model_node) + + self.log.info("udated to version: {}".format(version.get("name"))) + + return update_container(model_node, data_imprint) + + def node_version_color(self, version, node): + """ Coloring a node by correct color by actual version + """ + # get all versions in list + versions = io.find({ + "type": "version", + "parent": version["parent"] + }).distinct('name') + + max_version = max(versions) + + # change color of node + if version.get("name") not in [max_version]: + node["tile_color"].setValue(int("0xd88467ff", 16)) + else: + node["tile_color"].setValue(int(self.node_color, 16)) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + from avalon.nuke import viewer_update_and_undo_stop + node = nuke.toNode(container['objectName']) + with viewer_update_and_undo_stop(): + nuke.delete(node) diff --git a/openpype/hosts/nuke/plugins/publish/collect_model.py b/openpype/hosts/nuke/plugins/publish/collect_model.py new file mode 100644 index 0000000000..5fca240553 --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/collect_model.py @@ -0,0 +1,49 @@ +import pyblish.api +import nuke + + +@pyblish.api.log +class CollectModel(pyblish.api.InstancePlugin): + """Collect Model node instance and its content + """ + + order = pyblish.api.CollectorOrder + 0.22 + label = "Collect Model" + hosts = ["nuke"] + families = ["model"] + + def process(self, instance): + + grpn = instance[0] + + # add family to familiess + instance.data["families"].insert(0, instance.data["family"]) + # make label nicer + instance.data["label"] = grpn.name() + + # Get frame range + handle_start = instance.context.data["handleStart"] + handle_end = instance.context.data["handleEnd"] + first_frame = int(nuke.root()["first_frame"].getValue()) + last_frame = int(nuke.root()["last_frame"].getValue()) + + # Add version data to instance + version_data = { + "handles": handle_start, + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStart": first_frame + handle_start, + "frameEnd": last_frame - handle_end, + "colorspace": nuke.root().knob('workingSpaceLUT').value(), + "families": [instance.data["family"]] + instance.data["families"], + "subset": instance.data["subset"], + "fps": instance.context.data["fps"] + } + + instance.data.update({ + "versionData": version_data, + "frameStart": first_frame, + "frameEnd": last_frame + }) + self.log.info("Model content collected: `{}`".format(instance[:])) + self.log.info("Model instance collected: `{}`".format(instance)) diff --git a/openpype/hosts/nuke/plugins/publish/extract_model.py b/openpype/hosts/nuke/plugins/publish/extract_model.py new file mode 100644 index 0000000000..43214bf3e9 --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/extract_model.py @@ -0,0 +1,103 @@ +import nuke +import os +import pyblish.api +import openpype.api +from avalon.nuke import lib as anlib +from pprint import pformat + + +class ExtractModel(openpype.api.Extractor): + """ 3D model exctractor + """ + label = 'Exctract Model' + order = pyblish.api.ExtractorOrder + families = ["model"] + hosts = ["nuke"] + + # presets + write_geo_knobs = [ + ("file_type", "abc"), + ("storageFormat", "Ogawa"), + ("writeGeometries", True), + ("writePointClouds", False), + ("writeAxes", False) + ] + + def process(self, instance): + handle_start = instance.context.data["handleStart"] + handle_end = instance.context.data["handleEnd"] + first_frame = int(nuke.root()["first_frame"].getValue()) + last_frame = int(nuke.root()["last_frame"].getValue()) + + self.log.info("instance.data: `{}`".format( + pformat(instance.data))) + + rm_nodes = list() + model_node = instance[0] + self.log.info("Crating additional nodes") + subset = instance.data["subset"] + staging_dir = self.staging_dir(instance) + + extension = next((k[1] for k in self.write_geo_knobs + if k[0] == "file_type"), None) + if not extension: + raise RuntimeError( + "Bad config for extension in presets. " + "Talk to your supervisor or pipeline admin") + + # create file name and path + filename = subset + ".{}".format(extension) + file_path = os.path.join(staging_dir, filename).replace("\\", "/") + + with anlib.maintained_selection(): + # select model node + anlib.select_nodes([model_node]) + + # create write geo node + wg_n = nuke.createNode("WriteGeo") + wg_n["file"].setValue(file_path) + # add path to write to + for k, v in self.write_geo_knobs: + wg_n[k].setValue(v) + rm_nodes.append(wg_n) + + # write out model + nuke.execute( + wg_n, + int(first_frame), + int(last_frame) + ) + # erase additional nodes + for n in rm_nodes: + nuke.delete(n) + + self.log.info(file_path) + + # create representation data + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': extension, + 'ext': extension, + 'files': filename, + "stagingDir": staging_dir, + "frameStart": first_frame, + "frameEnd": last_frame + } + instance.data["representations"].append(representation) + + instance.data.update({ + "path": file_path, + "outputDir": staging_dir, + "ext": extension, + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStart": first_frame + handle_start, + "frameEnd": last_frame - handle_end, + "frameStartHandle": first_frame, + "frameEndHandle": last_frame, + }) + + self.log.info("Extracted instance '{0}' to: {1}".format( + instance.name, file_path)) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py index b5890b5c51..261fca6583 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py @@ -31,7 +31,7 @@ class ExtractReviewDataMov(openpype.api.Extractor): instance.data["representations"] = [] staging_dir = os.path.normpath( - os.path.dirname(instance.data['path'])) + os.path.dirname(instance.data["path"])) instance.data["stagingDir"] = staging_dir @@ -83,9 +83,15 @@ class ExtractReviewDataMov(openpype.api.Extractor): "Baking output `{}` with settings: {}".format( o_name, o_data)) + # check if settings have more then one preset + # so we dont need to add outputName to representation + # in case there is only one preset + multiple_presets = bool(len(self.outputs.keys()) > 1) + # create exporter instance exporter = plugin.ExporterReviewMov( - self, instance, o_name, o_data["extension"]) + self, instance, o_name, o_data["extension"], + multiple_presets) if "render.farm" in families: if "review" in instance.data["families"]: diff --git a/openpype/hosts/photoshop/api/__init__.py b/openpype/hosts/photoshop/api/__init__.py index 81942c3b2a..d978d6ecc1 100644 --- a/openpype/hosts/photoshop/api/__init__.py +++ b/openpype/hosts/photoshop/api/__init__.py @@ -2,9 +2,10 @@ import os import sys import logging +from Qt import QtWidgets + from avalon import io from avalon import api as avalon -from avalon.vendor import Qt from openpype import lib from pyblish import api as pyblish import openpype.hosts.photoshop @@ -38,10 +39,10 @@ def check_inventory(): # Warn about outdated containers. print("Starting new QApplication..") - app = Qt.QtWidgets.QApplication(sys.argv) + app = QtWidgets.QApplication(sys.argv) - message_box = Qt.QtWidgets.QMessageBox() - message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) + message_box = QtWidgets.QMessageBox() + message_box.setIcon(QtWidgets.QMessageBox.Warning) msg = "There are outdated containers in the scene." message_box.setText(msg) message_box.exec_() diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 967a704ccf..657d41aa93 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -1,5 +1,5 @@ +from Qt import QtWidgets import openpype.api -from avalon.vendor import Qt from avalon import photoshop @@ -26,21 +26,21 @@ class CreateImage(openpype.api.Creator): if len(selection) > 1: # Ask user whether to create one image or image per selected # item. - msg_box = Qt.QtWidgets.QMessageBox() - msg_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) + msg_box = QtWidgets.QMessageBox() + msg_box.setIcon(QtWidgets.QMessageBox.Warning) msg_box.setText( "Multiple layers selected." "\nDo you want to make one image per layer?" ) msg_box.setStandardButtons( - Qt.QtWidgets.QMessageBox.Yes | - Qt.QtWidgets.QMessageBox.No | - Qt.QtWidgets.QMessageBox.Cancel + QtWidgets.QMessageBox.Yes | + QtWidgets.QMessageBox.No | + QtWidgets.QMessageBox.Cancel ) ret = msg_box.exec_() - if ret == Qt.QtWidgets.QMessageBox.Yes: + if ret == QtWidgets.QMessageBox.Yes: multiple_instances = True - elif ret == Qt.QtWidgets.QMessageBox.Cancel: + elif ret == QtWidgets.QMessageBox.Cancel: return if multiple_instances: diff --git a/openpype/hosts/resolve/api/menu.py b/openpype/hosts/resolve/api/menu.py index 262ce739dd..0d5930d275 100644 --- a/openpype/hosts/resolve/api/menu.py +++ b/openpype/hosts/resolve/api/menu.py @@ -61,6 +61,9 @@ class OpenPypeMenu(QtWidgets.QWidget): inventory_btn = QtWidgets.QPushButton("Inventory ...", self) subsetm_btn = QtWidgets.QPushButton("Subset Manager ...", self) libload_btn = QtWidgets.QPushButton("Library ...", self) + experimental_btn = QtWidgets.QPushButton( + "Experimental tools ...", self + ) # rename_btn = QtWidgets.QPushButton("Rename", self) # set_colorspace_btn = QtWidgets.QPushButton( # "Set colorspace from presets", self @@ -91,6 +94,8 @@ class OpenPypeMenu(QtWidgets.QWidget): # layout.addWidget(set_colorspace_btn) # layout.addWidget(reset_resolution_btn) + layout.addWidget(Spacer(15, self)) + layout.addWidget(experimental_btn) self.setLayout(layout) @@ -104,6 +109,7 @@ class OpenPypeMenu(QtWidgets.QWidget): # rename_btn.clicked.connect(self.on_rename_clicked) # set_colorspace_btn.clicked.connect(self.on_set_colorspace_clicked) # reset_resolution_btn.clicked.connect(self.on_reset_resolution_clicked) + experimental_btn.clicked.connect(self.on_experimental_clicked) def on_workfile_clicked(self): print("Clicked Workfile") @@ -142,6 +148,9 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_reset_resolution_clicked(self): print("Clicked Reset Resolution") + def on_experimental_clicked(self): + host_tools.show_experimental_tools_dialog() + def launch_pype_menu(): app = QtWidgets.QApplication(sys.argv) diff --git a/openpype/lib/anatomy.py b/openpype/lib/anatomy.py index aaf10479fd..66ecbd66d1 100644 --- a/openpype/lib/anatomy.py +++ b/openpype/lib/anatomy.py @@ -89,8 +89,10 @@ class Anatomy: self.project_name = project_name - self._data = get_anatomy_settings(project_name, site_name) - + self._data = self._prepare_anatomy_data( + get_anatomy_settings(project_name, site_name) + ) + self._site_name = site_name self._templates_obj = Templates(self) self._roots_obj = Roots(self) @@ -121,9 +123,36 @@ class Anatomy: """ return get_default_anatomy_settings(clear_metadata=False) + @staticmethod + def _prepare_anatomy_data(anatomy_data): + """Prepare anatomy data for futher processing. + + Method added to replace `{task}` with `{task[name]}` in templates. + """ + templates_data = anatomy_data.get("templates") + if templates_data: + # Replace `{task}` with `{task[name]}` in templates + value_queue = collections.deque() + value_queue.append(templates_data) + while value_queue: + item = value_queue.popleft() + if not isinstance(item, dict): + continue + + for key in tuple(item.keys()): + value = item[key] + if isinstance(value, dict): + value_queue.append(value) + + elif isinstance(value, StringType): + item[key] = value.replace("{task}", "{task[name]}") + return anatomy_data + def reset(self): """Reset values of cached data in templates and roots objects.""" - self._data = get_anatomy_settings(self.project_name) + self._data = self._prepare_anatomy_data( + get_anatomy_settings(self.project_name, self._site_name) + ) self.templates_obj.reset() self.roots_obj.reset() @@ -981,6 +1010,14 @@ class Templates: TemplateResult: Filled or partially filled template containing all data needed or missing for filling template. """ + task_data = data.get("task") + if ( + isinstance(task_data, StringType) + and "{task[name]}" in orig_template + ): + # Change task to dictionary if template expect dictionary + data["task"] = {"name": task_data} + template, missing_optional, invalid_optional = ( self._filter_optional(orig_template, data) ) @@ -990,13 +1027,6 @@ class Templates: missing_required = [] replace_keys = [] - task_data = data.get("task") - if ( - isinstance(task_data, StringType) - and "{task[name]}" in orig_template - ): - data["task"] = {"name": task_data} - for group in self.key_pattern.findall(template): orig_key = group[1:-1] key = str(orig_key) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 3e0e0c6ea6..a8340d7d09 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -271,9 +271,17 @@ def get_linked_asset_ids(asset_doc): if not asset_doc: return output - input_links = asset_doc["data"].get("inputsLinks") or [] + input_links = asset_doc["data"].get("inputLinks") or [] if input_links: - output = [item["_id"] for item in input_links] + for item in input_links: + # Backwards compatibility for "_id" key which was replaced with + # "id" + if "_id" in item: + link_id = item["_id"] + else: + link_id = item["id"] + output.append(link_id) + return output diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py index c89e2e7ae0..01fcc907ed 100644 --- a/openpype/lib/delivery.py +++ b/openpype/lib/delivery.py @@ -60,12 +60,13 @@ def path_from_representation(representation, anatomy): path = pipeline.format_template_with_optional_keys( context, template ) + path = os.path.normpath(path.replace("/", "\\")) except KeyError: # Template references unavailable data return None - return os.path.normpath(path) + return path def copy_file(src_path, dst_path): @@ -179,9 +180,11 @@ def process_single_file( Returns: (collections.defaultdict , int) """ + # Make sure path is valid for all platforms + src_path = os.path.normpath(src_path.replace("\\", "/")) + if not os.path.exists(src_path): - msg = "{} doesn't exist for {}".format(src_path, - repre["_id"]) + msg = "{} doesn't exist for {}".format(src_path, repre["_id"]) report_items["Source file was not found"].append(msg) return report_items, 0 @@ -192,8 +195,10 @@ def process_single_file( else: delivery_path = anatomy_filled["delivery"][template_name] - # context.representation could be .psd + # Backwards compatibility when extension contained `.` delivery_path = delivery_path.replace("..", ".") + # Make sure path is valid for all platforms + delivery_path = os.path.normpath(delivery_path.replace("\\", "/")) delivery_folder = os.path.dirname(delivery_path) if not os.path.exists(delivery_folder): @@ -230,14 +235,14 @@ def process_sequence( Returns: (collections.defaultdict , int) """ + src_path = os.path.normpath(src_path.replace("\\", "/")) def hash_path_exist(myPath): res = myPath.replace('#', '*') glob_search_results = glob.glob(res) if len(glob_search_results) > 0: return True - else: - return False + return False if not hash_path_exist(src_path): msg = "{} doesn't exist for {}".format(src_path, @@ -307,6 +312,7 @@ def process_sequence( else: delivery_path = anatomy_filled["delivery"][template_name] + delivery_path = os.path.normpath(delivery_path.replace("\\", "/")) delivery_folder = os.path.dirname(delivery_path) dst_head, dst_tail = delivery_path.split(frame_indicator) dst_padding = src_collection.padding diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index a1111fba29..ad77b2f899 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -124,7 +124,7 @@ def run_subprocess(*args, **kwargs): if full_output: full_output += "\n" full_output += _stderr - logger.warning(_stderr) + logger.info(_stderr) if proc.returncode != 0: exc_msg = "Executing arguments was not successful: \"{}\"".format(args) diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_remote_publish.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_remote_publish.py index 9ada437716..c3228bfe52 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_remote_publish.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_remote_publish.py @@ -1,10 +1,10 @@ import os import json +import requests import hou from avalon import api, io -from avalon.vendor import requests import pyblish.api diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py index f471d788b6..fa146c0d30 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -2,8 +2,8 @@ import os import json import getpass +import requests from avalon import api -from avalon.vendor import requests import pyblish.api diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_nuke_deadline.py index a064a0aa86..ae9cd985eb 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -1,10 +1,11 @@ import os +import re import json import getpass +import requests + from avalon import api -from avalon.vendor import requests -import re import pyblish.api import nuke diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py index 7f7cedf4e7..1e158bda9b 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py @@ -5,10 +5,11 @@ import os import json import re from copy import copy, deepcopy +import requests +import clique import openpype.api from avalon import api, io -from avalon.vendor import requests, clique import pyblish.api diff --git a/openpype/modules/default_modules/deadline/plugins/publish/validate_deadline_connection.py b/openpype/modules/default_modules/deadline/plugins/publish/validate_deadline_connection.py index ff664d9f83..d5016a4d82 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/validate_deadline_connection.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/validate_deadline_connection.py @@ -1,7 +1,7 @@ -import pyblish.api - -from avalon.vendor import requests import os +import requests + +import pyblish.api class ValidateDeadlineConnection(pyblish.api.InstancePlugin): diff --git a/openpype/modules/default_modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/openpype/modules/default_modules/deadline/plugins/publish/validate_expected_and_rendered_files.py index addd4a2e80..719c7dfe3e 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/validate_expected_and_rendered_files.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/validate_expected_and_rendered_files.py @@ -1,8 +1,8 @@ import os import json -import pyblish.api +import requests -from avalon.vendor import requests +import pyblish.api from openpype.lib.abstract_submit_deadline import requests_get from openpype.lib.delivery import collect_frames diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_links.py b/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_links.py index 8c3d858a96..83132acd85 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_links.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_links.py @@ -113,7 +113,7 @@ class SyncLinksToAvalon(BaseEvent): continue links.append({ - "_id": ObjectId(link_mongo_id), + "id": ObjectId(link_mongo_id), "linkedBy": "ftrack", "type": "breakdown" }) diff --git a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py b/openpype/modules/default_modules/ftrack/lib/avalon_sync.py index 9e22f80b1c..3ba874281a 100644 --- a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/default_modules/ftrack/lib/avalon_sync.py @@ -1479,7 +1479,7 @@ class SyncEntitiesFactory: mongo_id = self.ftrack_avalon_mapper.get(ftrack_link_id) if mongo_id is not None: input_links.append({ - "_id": ObjectId(mongo_id), + "id": ObjectId(mongo_id), "linkedBy": "ftrack", "type": "breakdown" }) diff --git a/openpype/modules/default_modules/royal_render/plugins/publish/collect_sequences_from_job.py b/openpype/modules/default_modules/royal_render/plugins/publish/collect_sequences_from_job.py index d2754d1f92..2505d671af 100644 --- a/openpype/modules/default_modules/royal_render/plugins/publish/collect_sequences_from_job.py +++ b/openpype/modules/default_modules/royal_render/plugins/publish/collect_sequences_from_job.py @@ -17,7 +17,7 @@ def collect(root, frame_end=None): """Collect sequence collections in root""" - from avalon.vendor import clique + import clique files = [] for filename in os.listdir(root): diff --git a/openpype/modules/default_modules/sync_server/README.md b/openpype/modules/default_modules/sync_server/README.md index d7d7f3718b..e283b3bb66 100644 --- a/openpype/modules/default_modules/sync_server/README.md +++ b/openpype/modules/default_modules/sync_server/README.md @@ -56,6 +56,13 @@ representation.files.sites: `db.getCollection('MY_PROJECT').update({type:"representation"}, {$set:{"files.$[].sites.MY_CONFIGURED_REMOTE_SITE" : {}}}, true, true)` +I want to create new custom provider: +----------------------------------- +- take `providers\abstract_provider.py` as a base class +- create provider class in `providers` with a name according to a provider (eg. 'gdrive.py' for gdrive provider etc.) +- upload provider icon in png format, 24x24, into `providers\resources`, its name must follow name of provider (eg. 'gdrive.png' for gdrive provider) +- register new provider into `providers.lib.py`, test how many files could be manipulated at same time, check provider's API for limits + Needed configuration: -------------------- `pype/settings/defaults/project_settings/global.json`.`sync_server`: diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 1aeccbb958..0f165ff0ac 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -95,8 +95,10 @@ class TimersManager(OpenPypeModule, ITrayService): message_time = int(timers_settings["message_time"] * 60) auto_stop = timers_settings["auto_stop"] + platform_name = platform.system().lower() # Turn of auto stop on MacOs because pynput requires root permissions - if platform.system().lower() == "darwin" or full_time <= 0: + # and on linux can cause thread locks on application close + if full_time <= 0 or platform_name in ("darwin", "linux"): auto_stop = False self.auto_stop = auto_stop diff --git a/openpype/plugins/load/copy_file.py b/openpype/plugins/load/copy_file.py index 1acacf6b27..eaf5853035 100644 --- a/openpype/plugins/load/copy_file.py +++ b/openpype/plugins/load/copy_file.py @@ -18,7 +18,7 @@ class CopyFile(api.Loader): @staticmethod def copy_file_to_clipboard(path): - from avalon.vendor.Qt import QtCore, QtWidgets + from Qt import QtCore, QtWidgets clipboard = QtWidgets.QApplication.clipboard() assert clipboard, "Must have running QApplication instance" diff --git a/openpype/plugins/load/copy_file_path.py b/openpype/plugins/load/copy_file_path.py index f64f3e76d8..2041c79f6d 100644 --- a/openpype/plugins/load/copy_file_path.py +++ b/openpype/plugins/load/copy_file_path.py @@ -19,7 +19,7 @@ class CopyFilePath(api.Loader): @staticmethod def copy_path_to_clipboard(path): - from avalon.vendor.Qt import QtWidgets + from Qt import QtWidgets clipboard = QtWidgets.QApplication.clipboard() assert clipboard, "Must have running QApplication instance" diff --git a/openpype/plugins/load/delete_old_versions.py b/openpype/plugins/load/delete_old_versions.py index 263c534b64..b2f2c88975 100644 --- a/openpype/plugins/load/delete_old_versions.py +++ b/openpype/plugins/load/delete_old_versions.py @@ -5,9 +5,9 @@ import uuid import clique from pymongo import UpdateOne import ftrack_api +from Qt import QtWidgets, QtCore from avalon import api, style -from avalon.vendor.Qt import QtWidgets, QtCore from avalon.vendor import qargparse from avalon.api import AvalonMongoDB import avalon.pipeline diff --git a/openpype/plugins/load/open_djv.py b/openpype/plugins/load/open_djv.py index 5b49bb58d0..4b0e8411c8 100644 --- a/openpype/plugins/load/open_djv.py +++ b/openpype/plugins/load/open_djv.py @@ -32,7 +32,7 @@ class OpenInDJV(api.Loader): def load(self, context, name, namespace, data): directory = os.path.dirname(self.fname) - from avalon.vendor import clique + import clique pattern = clique.PATTERNS["frames"] files = os.listdir(directory) diff --git a/openpype/plugins/load/open_file.py b/openpype/plugins/load/open_file.py index b496311e0c..4133a64eb3 100644 --- a/openpype/plugins/load/open_file.py +++ b/openpype/plugins/load/open_file.py @@ -27,7 +27,7 @@ class Openfile(api.Loader): color = "orange" def load(self, context, name, namespace, data): - from avalon.vendor import clique + import clique directory = os.path.dirname(self.fname) pattern = clique.PATTERNS["frames"] diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index e7d5451631..b6c2e49385 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -1062,10 +1062,11 @@ class ExtractReview(pyblish.api.InstancePlugin): streams = ffprobe_streams( full_input_path_single_file, self.log ) - except Exception: + except Exception as exc: raise AssertionError(( - "FFprobe couldn't read information about input file: \"{}\"" - ).format(full_input_path_single_file)) + "FFprobe couldn't read information about input file: \"{}\"." + " Error message: {}" + ).format(full_input_path_single_file, str(exc))) # Try to find first stream with defined 'width' and 'height' # - this is to avoid order of streams where audio can be as first diff --git a/openpype/plugins/publish/integrate_inputlinks.py b/openpype/plugins/publish/integrate_inputlinks.py index e8a8b2296c..f973dfc963 100644 --- a/openpype/plugins/publish/integrate_inputlinks.py +++ b/openpype/plugins/publish/integrate_inputlinks.py @@ -81,7 +81,8 @@ class IntegrateInputLinks(pyblish.api.ContextPlugin): version_doc=instance.data["versionEntity"], ) - publishing.append(workfile) + if workfile is not None: + publishing.append(workfile) self.write_links_to_database(publishing) def add_link(self, link_type, input_id, version_doc): @@ -103,7 +104,7 @@ class IntegrateInputLinks(pyblish.api.ContextPlugin): # future. link = OrderedDict() link["type"] = link_type - link["input"] = io.ObjectId(input_id) + link["id"] = io.ObjectId(input_id) link["linkedBy"] = "publish" if "inputLinks" not in version_doc["data"]: diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index d6fc30c315..39390f355a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -24,6 +24,9 @@ { "nukenodes": "nukenodes" }, + { + "model": "model" + }, { "camera": "camera" }, diff --git a/openpype/style/data.json b/openpype/style/data.json index 977de50be2..026eaf4264 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -59,7 +59,12 @@ "color-selected": "#F0F2F5", "color-hover": "#F0F2F5" }, - + "nice-checkbox": { + "bg-checked": "#56a06f", + "bg-unchecked": "#434b56", + "bg-checker": "#D3D8DE", + "bg-checker-hover": "#F0F2F5" + }, "loader": { "asset-view": { "selected": "rgba(168, 175, 189, 0.6)", @@ -79,6 +84,34 @@ "bg-expander-hover": "#2d6c9f", "bg-expander-selected-hover": "#3784c5" } + }, + "settings": { + "invalid-light": "#C93636", + "invalid-dark": "#AD2E2E", + + "modified-light": "#46b1f3", + "modified-mid": "#189AEA", + "modified-dark": "#106AA2", + + "studio-light": "#73C990", + "studio-dark": "#56a06f", + "studio-label-hover": "#FFFFFF", + + "project-light": "#FFA64D", + "project-mid": "#FF8C1A", + "project-dark": "#E67300", + + "label-fg": "#969b9e", + "label-fg-hover": "#b8c1c5", + + "breadcrumbs-btn-bg": "rgba(127, 127, 127, 60)", + "breadcrumbs-btn-bg-hover": "rgba(127, 127, 127, 90)", + + "content-hightlighted": "rgba(19, 26, 32, 15)", + "focus-border": "#839caf", + "image-btn": "#bfccd6", + "image-btn-hover": "#189aea", + "image-btn-disabled": "#bfccd6" } } } diff --git a/openpype/style/style.css b/openpype/style/style.css index fa5b41cd07..a60c3592d7 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -660,15 +660,6 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; } -/* Globally used names */ -#Separator { - background: {color:bg-menu-separator}; -} - -#IconButton { - padding: 4px 4px 4px 4px; -} - /* Password dialog*/ #PasswordBtn { border: none; @@ -971,7 +962,235 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: transparent; } +/* Settings - NOT USED YET +- we need to define font family for settings UI */ + +#SettingsMainWidget { + background: #141a1f; +} +/* Change focus borders. */ +#SettingsMainWidget QAbstractSpinBox:focus, #SettingsMainWidget QLineEdit:focus, #SettingsMainWidget QPlainTextEdit:focus, #SettingsMainWidget QTextEdit:focus { + border-color: {color:settings:focus-border}; +} +/* Modify tab widget for settings */ +#SettingsMainWidget QTabWidget::pane { + border-top-style: none; +} + +#SettingsMainWidget QTabBar { + background: transparent; +} + +#SettingsMainWidget QTabBar::tab { + border: none; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + padding: 5px; +} + +#SettingsMainWidget QTabBar::tab:selected { + background: {color:bg}; + border-color: #9B9B9B; + border-bottom-color: #C2C7CB; +} + +#SettingsMainWidget QTabBar::tab:!selected { + margin-top: 2px; + background: #21252B; +} + +#SettingsMainWidget QTabBar::tab:!selected:hover { + background: #333840; +} + +#SettingsMainWidget QTabBar::tab:first:selected { + margin-left: 0; +} + +#SettingsMainWidget QTabBar::tab:last:selected { + margin-right: 0; +} + +#SettingsMainWidget QTabBar::tab:only-one { + margin: 0; +} + +#SettingsToolIconBtn { + border: 0px solid #bfccd6; + background-color: transparent; +} + +#SettingsToolBtn { + border: 1px solid #bfccd6; + border-radius: 10px; + background-color: transparent; +} + +#SettingsToolBtn:hover { + border-color: #189aea; + color: {color:settings:modified-light}; + background-color: transparent; +} +#SettingsToolBtn:disabled { + background-color: #464b54; +} + +#ExpandToggleBtn { + background: transparent; +} + +#SettingsLabel { + background: transparent; + color: {color:settings:label-fg}; +} +#SettingsLabel:hover {color: {color:settings:label-fg-hover};} +#SettingsLabel[state="studio"] {color: {color:settings:studio-light};} +#SettingsLabel[state="studio"]:hover {color: {color:settings:studio-label-hover};} +#SettingsLabel[state="modified"] {color: {color:settings:modified-mid};} +#SettingsLabel[state="modified"]:hover {color: {color:settings:modified-light};} +#SettingsLabel[state="overriden-modified"] {color: {color:settings:modified-mid};} +#SettingsLabel[state="overriden-modified"]:hover {color: {color:settings:modified-light};} +#SettingsLabel[state="overriden"] {color: {color:settings:project-mid};} +#SettingsLabel[state="overriden"]:hover {color: {color:settings:project-light};} +#SettingsLabel[state="invalid"] {color:{color:settings:invalid-dark};} +#SettingsLabel[state="invalid"]:hover {color: {color:settings:invalid-dark};} + +/* TODO Replace these with explicit widget types if possible */ +#SettingsMainWidget QWidget[input-state="modified"] { + border-color: {color:settings:modified-mid}; +} +#SettingsMainWidget QWidget[input-state="overriden-modified"] { + border-color: {color:settings:modified-mid}; +} +#SettingsMainWidget QWidget[input-state="overriden"] { + border-color: {color:settings:project-mid}; +} +#SettingsMainWidget QWidget[input-state="invalid"] { + border-color: {color:settings:invalid-dark}; +} + +#GroupWidget { + border-bottom: 1px solid #21252B; +} + +#ProjectListWidget QLabel { + background: transparent; + font-weight: bold; +} + +#MultiSelectionComboBox { + font-size: 12px; +} + +#DictKey[state="modified"] {border-color: {color:settings:modified-mid};} +#DictKey[state="invalid"] {border-color: {color:settings:invalid-dark};} + +#ExpandLabel { + font-weight: bold; + color: {color:settings:label-fg}; +} +#ExpandLabel:hover { + color: {color:settings:label-fg-hover}; +} + +#ContentWidget { + background-color: transparent; +} +#ContentWidget[content_state="hightlighted"] { + background-color: {color:settings:content-hightlighted}; +} + +#SideLineWidget { + background-color: #333942; + border-style: solid; + border-color: #4e5254; + border-left-width: 3px; + border-bottom-width: 0px; + border-right-width: 0px; + border-top-width: 0px; +} + +#SideLineWidget:hover { + border-color: #7d8386; +} + +#SideLineWidget[state="child-studio"] {border-color: {color:settings:studio-dark};} +#SideLineWidget[state="child-studio"]:hover {border-color: {color:settings:studio-light};} + +#SideLineWidget[state="child-modified"] {border-color: {color:settings:modified-dark};} +#SideLineWidget[state="child-modified"]:hover {border-color: {color:settings:modified-mid};} + +#SideLineWidget[state="child-invalid"] {border-color: {color:settings:invalid-dark};} +#SideLineWidget[state="child-invalid"]:hover {border-color: {color:settings:invalid-light};} + +#SideLineWidget[state="child-overriden"] {border-color: {color:settings:project-dark};} +#SideLineWidget[state="child-overriden"]:hover {border-color: {color:settings:project-mid};} + +#SideLineWidget[state="child-overriden-modified"] {border-color: {color:settings:modified-dark};} +#SideLineWidget[state="child-overriden-modified"]:hover {border-color: {color:settings:modified-mid};} + +#DictAsWidgetBody { + background: transparent; +} +#DictAsWidgetBody[show_borders="1"] { + border: 1px solid #4e5254; + border-radius: 5px; +} + +#ShadowWidget { + font-size: 36pt; +} + +#BreadcrumbsPathInput { + padding: 2px; + font-size: 9pt; +} + +#BreadcrumbsButton { + padding-right: 12px; + font-size: 9pt; + background: transparent; +} + +#BreadcrumbsButton[empty="1"] { + padding-right: 0px; +} + +#BreadcrumbsButton::menu-button { + border: none; + width: 12px; + background: {color:settings:breadcrumbs-btn-bg}; +} +#BreadcrumbsButton::menu-button:hover { + background: {color:settings:breadcrumbs-btn-bg-hover}; +} + +#BreadcrumbsPanel { + border: 1px solid #4e5254; + border-radius: 5px; + background: #21252B; +} + +/* Globally used names */ +#Separator { + background: {color:bg-menu-separator}; +} + +#IconButton { + padding: 4px 4px 4px 4px; +} + #NiceCheckbox { /* Default size hint of NiceCheckbox is defined by font size. */ font-size: 7pt; } + +#ImageButton { + padding: 0; + background: transparent; + font-size: 11pt; +} + +#ImageButton:disabled { + background: {color:bg-buttons-disabled}; +} diff --git a/openpype/tools/assetcreator/model.py b/openpype/tools/assetcreator/model.py index 3af1d77127..f84541ca2a 100644 --- a/openpype/tools/assetcreator/model.py +++ b/openpype/tools/assetcreator/model.py @@ -1,8 +1,7 @@ import re import logging -import collections -from avalon.vendor.Qt import QtCore, QtWidgets +from Qt import QtCore, QtWidgets from avalon.vendor import qtawesome from avalon import io from avalon import style diff --git a/openpype/tools/assetlinks/widgets.py b/openpype/tools/assetlinks/widgets.py index 9a136462b0..22e8848a60 100644 --- a/openpype/tools/assetlinks/widgets.py +++ b/openpype/tools/assetlinks/widgets.py @@ -37,8 +37,13 @@ class SimpleLinkView(QtWidgets.QWidget): # inputs # for link in version_doc["data"].get("inputLinks", []): + # Backwards compatibility for "input" key used as "id" + if "id" not in link: + link_id = link["input"] + else: + link_id = link["id"] version = self.dbcon.find_one( - {"_id": link["input"], "type": "version"}, + {"_id": link_id, "type": "version"}, projection={"name": 1, "parent": 1} ) if not version: diff --git a/openpype/tools/settings/__init__.py b/openpype/tools/settings/__init__.py index a156228dc1..3e77a8348a 100644 --- a/openpype/tools/settings/__init__.py +++ b/openpype/tools/settings/__init__.py @@ -1,12 +1,13 @@ import sys from Qt import QtWidgets, QtGui + +from openpype import style from .lib import ( BTN_FIXED_SIZE, CHILD_OFFSET ) from .local_settings import LocalSettingsWindow from .settings import ( - style, MainWidget, ProjectListWidget ) @@ -36,8 +37,6 @@ __all__ = ( "BTN_FIXED_SIZE", "CHILD_OFFSET", - "style", - "MainWidget", "ProjectListWidget", "LocalSettingsWindow", diff --git a/openpype/tools/settings/local_settings/apps_widget.py b/openpype/tools/settings/local_settings/apps_widget.py index e6a4132955..850e009937 100644 --- a/openpype/tools/settings/local_settings/apps_widget.py +++ b/openpype/tools/settings/local_settings/apps_widget.py @@ -172,7 +172,9 @@ class LocalApplicationsWidgets(QtWidgets.QWidget): def _reset_app_widgets(self): while self.content_layout.count() > 0: item = self.content_layout.itemAt(0) - item.widget().hide() + widget = item.widget() + if widget is not None: + widget.setVisible(False) self.content_layout.removeItem(item) self.widgets_by_group_name.clear() diff --git a/openpype/tools/settings/local_settings/projects_widget.py b/openpype/tools/settings/local_settings/projects_widget.py index 9cd3b9a38e..7e2ad661a0 100644 --- a/openpype/tools/settings/local_settings/projects_widget.py +++ b/openpype/tools/settings/local_settings/projects_widget.py @@ -6,10 +6,7 @@ from openpype.settings.constants import ( PROJECT_ANATOMY_KEY, DEFAULT_PROJECT_KEY ) -from .widgets import ( - SpacerWidget, - ProxyLabelWidget -) +from .widgets import ProxyLabelWidget from .constants import ( LABEL_REMOVE_DEFAULT, LABEL_ADD_DEFAULT, @@ -238,9 +235,9 @@ class SitesWidget(QtWidgets.QWidget): comboboxes_layout = QtWidgets.QHBoxLayout(comboboxes_widget) comboboxes_layout.setContentsMargins(0, 0, 0, 0) - comboboxes_layout.addWidget(active_site_widget) - comboboxes_layout.addWidget(remote_site_widget) - comboboxes_layout.addWidget(SpacerWidget(comboboxes_widget), 1) + comboboxes_layout.addWidget(active_site_widget, 0) + comboboxes_layout.addWidget(remote_site_widget, 0) + comboboxes_layout.addStretch(1) content_widget = QtWidgets.QWidget(self) content_layout = QtWidgets.QVBoxLayout(content_widget) @@ -259,7 +256,9 @@ class SitesWidget(QtWidgets.QWidget): def _clear_widgets(self): while self.content_layout.count(): item = self.content_layout.itemAt(0) - item.widget().hide() + widget = item.widget() + if widget is not None: + widget.setVisible(False) self.content_layout.removeItem(item) self.input_objects = {} @@ -383,7 +382,7 @@ class SitesWidget(QtWidgets.QWidget): self.input_objects[site_name] = site_input_objects # Add spacer so other widgets are squeezed to top - self.content_layout.addWidget(SpacerWidget(self), 1) + self.content_layout.addStretch(1) def _on_input_value_change(self, site_name, key): if ( @@ -456,6 +455,8 @@ class _SiteCombobox(QtWidgets.QWidget): self ) combobox_input = QtWidgets.QComboBox(self) + combobox_delegate = QtWidgets.QStyledItemDelegate() + combobox_input.setItemDelegate(combobox_delegate) main_layout = QtWidgets.QHBoxLayout(self) main_layout.addWidget(label_widget) @@ -464,6 +465,7 @@ class _SiteCombobox(QtWidgets.QWidget): combobox_input.currentIndexChanged.connect(self._on_index_change) self.label_widget = label_widget self.combobox_input = combobox_input + self._combobox_delegate = combobox_delegate def _set_current_text(self, text): index = None @@ -777,7 +779,7 @@ class RootSiteWidget(QtWidgets.QWidget): main_layout = QtWidgets.QVBoxLayout(self) main_layout.addWidget(sites_widget) - main_layout.addWidget(SpacerWidget(self), 1) + main_layout.addStretch(1) self.sites_widget = sites_widget diff --git a/openpype/tools/settings/local_settings/widgets.py b/openpype/tools/settings/local_settings/widgets.py index b164f1b407..2733aef187 100644 --- a/openpype/tools/settings/local_settings/widgets.py +++ b/openpype/tools/settings/local_settings/widgets.py @@ -1,7 +1,6 @@ from Qt import QtWidgets, QtCore from openpype.tools.settings.settings.widgets import ( - ExpandingWidget, - SpacerWidget + ExpandingWidget ) @@ -56,7 +55,5 @@ class ProxyLabelWidget(QtWidgets.QWidget): __all__ = ( "ExpandingWidget", - "SpacerWidget", "Separator", - "SpacerWidget" ) diff --git a/openpype/tools/settings/local_settings/window.py b/openpype/tools/settings/local_settings/window.py index f22e397323..a00bc232f4 100644 --- a/openpype/tools/settings/local_settings/window.py +++ b/openpype/tools/settings/local_settings/window.py @@ -1,7 +1,7 @@ import logging from Qt import QtWidgets, QtGui -from ..settings import style +from openpype import style from openpype.settings.lib import ( get_local_settings, @@ -15,7 +15,6 @@ from openpype.api import ( from openpype.modules import ModulesManager from .widgets import ( - SpacerWidget, ExpandingWidget ) from .mongo_widget import OpenPypeMongoWidget @@ -58,8 +57,7 @@ class LocalSettingsWidget(QtWidgets.QWidget): self._create_app_ui() self._create_project_ui() - # Add spacer to main layout - self.main_layout.addWidget(SpacerWidget(self), 1) + self.main_layout.addStretch(1) def _create_pype_mongo_ui(self): pype_mongo_expand_widget = ExpandingWidget("OpenPype Mongo URL", self) @@ -210,7 +208,7 @@ class LocalSettingsWindow(QtWidgets.QWidget): footer_layout = QtWidgets.QHBoxLayout(footer) footer_layout.addWidget(reset_btn, 0) - footer_layout.addWidget(SpacerWidget(footer), 1) + footer_layout.addStretch(1) footer_layout.addWidget(save_btn, 0) main_layout = QtWidgets.QVBoxLayout(self) diff --git a/openpype/tools/settings/settings/__init__.py b/openpype/tools/settings/settings/__init__.py index 6b4cf94357..9eadd456b7 100644 --- a/openpype/tools/settings/settings/__init__.py +++ b/openpype/tools/settings/settings/__init__.py @@ -1,10 +1,8 @@ -from . import style from .window import MainWidget from .widgets import ProjectListWidget __all__ = ( - "style", "MainWidget", "ProjectListWidget" ) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 92fffe6f9c..f8378ed18c 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -470,10 +470,9 @@ class GUIWidget(BaseWidget): self.entity_widget.add_widget_to_layout(self) def _create_label_ui(self): - self.setObjectName("LabelWidget") - label = self.entity["label"] label_widget = QtWidgets.QLabel(label, self) + label_widget.setObjectName("SettingsLabel") layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 5, 0, 5) @@ -481,7 +480,7 @@ class GUIWidget(BaseWidget): def _create_separator_ui(self): splitter_item = QtWidgets.QWidget(self) - splitter_item.setObjectName("SplitterItem") + splitter_item.setObjectName("Separator") splitter_item.setMinimumHeight(self.separator_height) splitter_item.setMaximumHeight(self.separator_height) @@ -513,10 +512,9 @@ class MockUpWidget(BaseWidget): child_invalid = False def create_ui(self): - self.setObjectName("LabelWidget") - label = "Mockup widget for entity {}".format(self.entity.path) label_widget = QtWidgets.QLabel(label, self) + label_widget.setObjectName("SettingsLabel") layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 5, 0, 5) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 5f9051344d..a6e4154b2b 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -391,7 +391,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget): while self.content_layout.count() != 0: widget = self.content_layout.itemAt(0).widget() - widget.hide() + if widget is not None: + widget.setVisible(False) + self.content_layout.removeWidget(widget) widget.deleteLater() diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 3e3270cac9..2e1617f505 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -164,6 +164,7 @@ class DictConditionalWidget(BaseWidget): content_widget.setProperty("show_borders", show_borders) label_widget = QtWidgets.QLabel(self.entity.label) + label_widget.setObjectName("SettingsLabel") content_layout = QtWidgets.QGridLayout(content_widget) content_layout.setContentsMargins(5, 5, 5, 5) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index 9afce7259e..294711b38a 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -3,7 +3,12 @@ from uuid import uuid4 from Qt import QtWidgets, QtCore, QtGui from .base import BaseWidget -from .lib import create_deffered_value_change_timer +from .lib import ( + create_deffered_value_change_timer, + create_add_btn, + create_remove_btn, + create_confirm_btn +) from .widgets import ( ExpandingWidget, IconButton @@ -21,92 +26,6 @@ KEY_INPUT_TOOLTIP = ( ) -class PaintHelper: - cached_icons = {} - - @classmethod - def _draw_image(cls, width, height, brush): - image = QtGui.QPixmap(width, height) - image.fill(QtCore.Qt.transparent) - - icon_path_stroker = QtGui.QPainterPathStroker() - icon_path_stroker.setCapStyle(QtCore.Qt.RoundCap) - icon_path_stroker.setJoinStyle(QtCore.Qt.RoundJoin) - icon_path_stroker.setWidth(height / 5) - - painter = QtGui.QPainter(image) - painter.setPen(QtCore.Qt.transparent) - painter.setBrush(brush) - rect = QtCore.QRect(0, 0, image.width(), image.height()) - fifteenth = rect.height() / 15 - # Left point - p1 = QtCore.QPoint( - rect.x() + (5 * fifteenth), - rect.y() + (9 * fifteenth) - ) - # Middle bottom point - p2 = QtCore.QPoint( - rect.center().x(), - rect.y() + (11 * fifteenth) - ) - # Top right point - p3 = QtCore.QPoint( - rect.x() + (10 * fifteenth), - rect.y() + (5 * fifteenth) - ) - - path = QtGui.QPainterPath(p1) - path.lineTo(p2) - path.lineTo(p3) - - stroked_path = icon_path_stroker.createStroke(path) - painter.drawPath(stroked_path) - - painter.end() - - return image - - @classmethod - def get_confirm_icon(cls, width, height): - key = "{}x{}-confirm_image".format(width, height) - icon = cls.cached_icons.get(key) - - if icon is None: - image = cls._draw_image(width, height, QtCore.Qt.white) - icon = QtGui.QIcon(image) - cls.cached_icons[key] = icon - return icon - - -def create_add_btn(parent): - add_btn = QtWidgets.QPushButton("+", parent) - add_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - add_btn.setProperty("btn-type", "tool-item") - add_btn.setFixedSize(BTN_FIXED_SIZE, BTN_FIXED_SIZE) - return add_btn - - -def create_remove_btn(parent): - remove_btn = QtWidgets.QPushButton("-", parent) - remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - remove_btn.setProperty("btn-type", "tool-item") - remove_btn.setFixedSize(BTN_FIXED_SIZE, BTN_FIXED_SIZE) - return remove_btn - - -def create_confirm_btn(parent): - confirm_btn = QtWidgets.QPushButton(parent) - - icon = PaintHelper.get_confirm_icon( - BTN_FIXED_SIZE, BTN_FIXED_SIZE - ) - confirm_btn.setIcon(icon) - confirm_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - confirm_btn.setProperty("btn-type", "tool-item") - confirm_btn.setFixedSize(BTN_FIXED_SIZE, BTN_FIXED_SIZE) - return confirm_btn - - class ModifiableDictEmptyItem(QtWidgets.QWidget): def __init__(self, entity_widget, store_as_list, parent): super(ModifiableDictEmptyItem, self).__init__(parent) @@ -375,7 +294,7 @@ class ModifiableDictItem(QtWidgets.QWidget): "fa.edit", QtCore.Qt.lightGray, QtCore.Qt.white ) edit_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - edit_btn.setProperty("btn-type", "tool-item-icon") + edit_btn.setObjectName("SettingsToolIconBtn") edit_btn.setFixedHeight(BTN_FIXED_SIZE) confirm_btn = create_confirm_btn(self) diff --git a/openpype/tools/settings/settings/images/__init__.py b/openpype/tools/settings/settings/images/__init__.py new file mode 100644 index 0000000000..3ad65e114a --- /dev/null +++ b/openpype/tools/settings/settings/images/__init__.py @@ -0,0 +1,19 @@ +import os +from Qt import QtGui + + +def get_image_path(image_filename): + return os.path.join( + os.path.dirname(os.path.abspath(__file__)), + image_filename + ) + + +def get_image(image_filename): + image_path = get_image_path(image_filename) + return QtGui.QImage(image_path) + + +def get_pixmap(image_filename): + image_path = get_image_path(image_filename) + return QtGui.QPixmap(image_path) diff --git a/openpype/tools/settings/settings/images/add.png b/openpype/tools/settings/settings/images/add.png new file mode 100644 index 0000000000..91ef720d32 Binary files /dev/null and b/openpype/tools/settings/settings/images/add.png differ diff --git a/openpype/tools/settings/settings/images/confirm.png b/openpype/tools/settings/settings/images/confirm.png new file mode 100644 index 0000000000..a0fdc66d3e Binary files /dev/null and b/openpype/tools/settings/settings/images/confirm.png differ diff --git a/openpype/tools/settings/settings/images/down.png b/openpype/tools/settings/settings/images/down.png new file mode 100644 index 0000000000..f78622922f Binary files /dev/null and b/openpype/tools/settings/settings/images/down.png differ diff --git a/openpype/tools/settings/settings/images/mask.png b/openpype/tools/settings/settings/images/mask.png new file mode 100644 index 0000000000..f10f00be2c Binary files /dev/null and b/openpype/tools/settings/settings/images/mask.png differ diff --git a/openpype/tools/settings/settings/images/remove.png b/openpype/tools/settings/settings/images/remove.png new file mode 100644 index 0000000000..79ea6eb973 Binary files /dev/null and b/openpype/tools/settings/settings/images/remove.png differ diff --git a/openpype/tools/settings/settings/images/up.png b/openpype/tools/settings/settings/images/up.png new file mode 100644 index 0000000000..4fccb08fe1 Binary files /dev/null and b/openpype/tools/settings/settings/images/up.png differ diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index a28bee8d36..2e00967a60 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -7,8 +7,8 @@ from .widgets import ( NumberSpinBox, GridLabelWidget, SettingsComboBox, - NiceCheckbox, SettingsPlainTextEdit, + SettingsNiceCheckbox, SettingsLineEdit ) from .multiselection_combobox import MultiSelectionComboBox @@ -21,6 +21,7 @@ from .base import ( BaseWidget, InputWidget ) + from openpype.widgets.sliders import NiceSlider from openpype.tools.settings import CHILD_OFFSET @@ -129,6 +130,7 @@ class DictImmutableKeysWidget(BaseWidget): content_widget.setProperty("show_borders", show_borders) label_widget = QtWidgets.QLabel(self.entity.label) + label_widget.setObjectName("SettingsLabel") content_layout = QtWidgets.QGridLayout(content_widget) content_layout.setContentsMargins(5, 5, 5, 5) @@ -324,12 +326,7 @@ class DictImmutableKeysWidget(BaseWidget): class BoolWidget(InputWidget): def _add_inputs_to_layout(self): - checkbox_height = self.style().pixelMetric( - QtWidgets.QStyle.PM_IndicatorHeight - ) - self.input_field = NiceCheckbox( - height=checkbox_height, parent=self.content_widget - ) + self.input_field = SettingsNiceCheckbox(parent=self.content_widget) self.content_layout.addWidget(self.input_field, 0) self.content_layout.addStretch(1) @@ -352,6 +349,9 @@ class BoolWidget(InputWidget): def _on_value_change(self): if self.ignore_input_changes: return + self.start_value_timer() + + def _on_value_change_timer(self): self.entity.set(self.input_field.isChecked()) diff --git a/openpype/tools/settings/settings/lib.py b/openpype/tools/settings/settings/lib.py index 577aaa5671..d12a14259a 100644 --- a/openpype/tools/settings/settings/lib.py +++ b/openpype/tools/settings/settings/lib.py @@ -1,5 +1,7 @@ from Qt import QtCore +from .widgets import SettingsToolBtn + # Offset of value change trigger in ms VALUE_CHANGE_OFFSET_MS = 300 @@ -16,3 +18,33 @@ def create_deffered_value_change_timer(callback): timer.setInterval(VALUE_CHANGE_OFFSET_MS) timer.timeout.connect(callback) return timer + + +def create_add_btn(parent): + add_btn = SettingsToolBtn("add", parent) + add_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + return add_btn + + +def create_remove_btn(parent): + remove_btn = SettingsToolBtn("remove", parent) + remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + return remove_btn + + +def create_up_btn(parent): + remove_btn = SettingsToolBtn("up", parent) + remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + return remove_btn + + +def create_down_btn(parent): + add_btn = SettingsToolBtn("down", parent) + add_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + return add_btn + + +def create_confirm_btn(parent): + remove_btn = SettingsToolBtn("confirm", parent) + remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + return remove_btn diff --git a/openpype/tools/settings/settings/list_item_widget.py b/openpype/tools/settings/settings/list_item_widget.py index 128af92631..cd1fd912ae 100644 --- a/openpype/tools/settings/settings/list_item_widget.py +++ b/openpype/tools/settings/settings/list_item_widget.py @@ -1,13 +1,17 @@ from Qt import QtWidgets, QtCore -from .base import InputWidget -from .widgets import ExpandingWidget from openpype.tools.settings import ( - BTN_FIXED_SIZE, CHILD_OFFSET ) -from avalon.vendor import qtawesome +from .base import InputWidget +from .widgets import ExpandingWidget +from .lib import ( + create_add_btn, + create_remove_btn, + create_up_btn, + create_down_btn +) class EmptyListItem(QtWidgets.QWidget): @@ -16,18 +20,11 @@ class EmptyListItem(QtWidgets.QWidget): self.entity_widget = entity_widget - add_btn = QtWidgets.QPushButton("+", self) - remove_btn = QtWidgets.QPushButton("-", self) + add_btn = create_add_btn(self) + remove_btn = create_remove_btn(self) - add_btn.setFocusPolicy(QtCore.Qt.ClickFocus) remove_btn.setEnabled(False) - add_btn.setFixedSize(BTN_FIXED_SIZE, BTN_FIXED_SIZE) - remove_btn.setFixedSize(BTN_FIXED_SIZE, BTN_FIXED_SIZE) - - add_btn.setProperty("btn-type", "tool-item") - remove_btn.setProperty("btn-type", "tool-item") - layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(3) @@ -52,32 +49,10 @@ class ListItem(QtWidgets.QWidget): self.ignore_input_changes = entity_widget.ignore_input_changes - char_up = qtawesome.charmap("fa.angle-up") - char_down = qtawesome.charmap("fa.angle-down") - - add_btn = QtWidgets.QPushButton("+") - remove_btn = QtWidgets.QPushButton("-") - up_btn = QtWidgets.QPushButton(char_up) - down_btn = QtWidgets.QPushButton(char_down) - - font_up_down = qtawesome.font("fa", 13) - up_btn.setFont(font_up_down) - down_btn.setFont(font_up_down) - - add_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - up_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - down_btn.setFocusPolicy(QtCore.Qt.ClickFocus) - - add_btn.setFixedSize(BTN_FIXED_SIZE, BTN_FIXED_SIZE) - remove_btn.setFixedSize(BTN_FIXED_SIZE, BTN_FIXED_SIZE) - up_btn.setFixedSize(BTN_FIXED_SIZE, BTN_FIXED_SIZE) - down_btn.setFixedSize(BTN_FIXED_SIZE, BTN_FIXED_SIZE) - - add_btn.setProperty("btn-type", "tool-item") - remove_btn.setProperty("btn-type", "tool-item") - up_btn.setProperty("btn-type", "tool-item") - down_btn.setProperty("btn-type", "tool-item") + add_btn = create_add_btn(self) + remove_btn = create_remove_btn(self) + up_btn = create_up_btn(self) + down_btn = create_down_btn(self) add_btn.clicked.connect(self._on_add_clicked) remove_btn.clicked.connect(self._on_remove_clicked) diff --git a/openpype/tools/settings/settings/style/__init__.py b/openpype/tools/settings/settings/style/__init__.py deleted file mode 100644 index f1d9829a04..0000000000 --- a/openpype/tools/settings/settings/style/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -import os -from openpype import resources - - -def load_stylesheet(): - style_path = os.path.join(os.path.dirname(__file__), "style.css") - with open(style_path, "r") as style_file: - stylesheet = style_file.read() - return stylesheet - - -def app_icon_path(): - return resources.get_openpype_icon_filepath() diff --git a/openpype/tools/settings/settings/style/style.css b/openpype/tools/settings/settings/style/style.css deleted file mode 100644 index b77b575204..0000000000 --- a/openpype/tools/settings/settings/style/style.css +++ /dev/null @@ -1,453 +0,0 @@ -/* :root { - --border-color-: #464b54; -} - */ - - -QWidget { - color: #bfccd6; - background-color: #282C34; - font-size: 12px; - border-radius: 0px; -} - -QMenu { - border: 1px solid #555555; - background-color: #21252B; -} - -QMenu::item { - padding: 5px 10px 5px 10px; - border-left: 5px solid #313131; -} - -QMenu::item:selected { - border-left-color: #61839e; - background-color: #222d37; -} -QCheckBox { - spacing: 0px; -} -QCheckBox::indicator {} -QCheckBox::indicator:focus {} - -QLineEdit, QSpinBox, QDoubleSpinBox, QPlainTextEdit, QTextEdit { - border: 1px solid #464b54; - border-radius: 3px; - background-color: #21252B; -} - -QLineEdit:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled, QPlainTextEdit:disabled, QTextEdit:disabled, QPushButton:disabled { - background-color: #464b54; -} - -QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QPlainTextEdit:focus, QTextEdit:focus { - border: 1px solid #839caf; -} - -QComboBox { - border: 1px solid #464b54; - border-radius: 3px; - padding: 2px 2px 4px 4px; - background: #21252B; -} - -QComboBox QAbstractItemView::item { - padding: 3px; -} - -QToolButton { - background: transparent; -} - -QLabel { - background: transparent; - color: #969b9e; -} -QLabel:hover {color: #b8c1c5;} - -QLabel[state="studio"] {color: #73C990;} -QLabel[state="studio"]:hover {color: #ffffff;} -QLabel[state="modified"] {color: #189aea;} -QLabel[state="modified"]:hover {color: #46b1f3;} -QLabel[state="overriden-modified"] {color: #189aea;} -QLabel[state="overriden-modified"]:hover {color: #46b1f3;} -QLabel[state="overriden"] {color: #ff8c1a;} -QLabel[state="overriden"]:hover {color: #ffa64d;} -QLabel[state="invalid"] {color: #ad2e2e;} -QLabel[state="invalid"]:hover {color: #ad2e2e;} - - -QWidget[input-state="studio"] {border-color: #858a94;} -QWidget[input-state="modified"] {border-color: #189aea;} -QWidget[input-state="overriden-modified"] {border-color: #189aea;} -QWidget[input-state="overriden"] {border-color: #ff8c1a;} -QWidget[input-state="invalid"] {border-color: #ad2e2e;} - -QPushButton { - border: 1px solid #aaaaaa; - border-radius: 3px; - padding: 5px; -} -QPushButton:hover { - background-color: #333840; - border: 1px solid #fff; - color: #fff; -} -QPushButton[btn-type="tool-item"] { - border: 1px solid #bfccd6; - border-radius: 10px; -} - -QPushButton[btn-type="tool-item"]:hover { - border-color: #189aea; - color: #46b1f3; - background-color: transparent; -} - -QPushButton[btn-type="tool-item-icon"] { - border: 0px solid #bfccd6; - background-color: transparent; -} - -QPushButton[btn-type="expand-toggle"] { - background: #21252B; -} - -/* SLider */ -QSlider::groove { - border: 1px solid #464b54; - border-radius: 0.3em; -} -QSlider::groove:horizontal { - height: 8px; -} -QSlider::groove:vertical { - width: 8px; -} -QSlider::handle { - width: 10px; - height: 10px; - - border-radius: 5px; -} -QSlider::handle:horizontal { - margin: -2px 0; -} -QSlider::handle:vertical { - margin: 0 -2px; -} - -#GroupWidget { - border-bottom: 1px solid #21252B; -} - -#ProjectListWidget QListView { - border: 1px solid #464b54; - background: #21252B; -} - -#ProjectListWidget QListView:disabled { - background: #282C34; -} - -#ProjectListWidget QListView::item:disabled { - color: #4e5254; -} - -#ProjectListWidget QLabel { - background: transparent; - font-weight: bold; -} - -#MultiSelectionComboBox { - font-size: 12px; -} - -#DictKey[state="studio"] {border-color: #464b54;} -#DictKey[state="modified"] {border-color: #189aea;} -#DictKey[state="overriden"] {border-color: #00f;} -#DictKey[state="overriden-modified"] {border-color: #0f0;} -#DictKey[state="invalid"] {border-color: #ad2e2e;} - -#DictLabel { - font-weight: bold; -} - -#ContentWidget { - background-color: transparent; -} -#ContentWidget[content_state="hightlighted"] { - background-color: rgba(19, 26, 32, 15%); -} - -#SideLineWidget { - background-color: #333942; - border-style: solid; - border-color: #4e5254; - border-left-width: 3px; - border-bottom-width: 0px; - border-right-width: 0px; - border-top-width: 0px; -} - -#SideLineWidget:hover { - border-color: #7d8386; -} - -#SideLineWidget[state="child-studio"] {border-color: #56a06f;} -#SideLineWidget[state="child-studio"]:hover {border-color: #73C990;} - -#SideLineWidget[state="child-modified"] {border-color: #106aa2;} -#SideLineWidget[state="child-modified"]:hover {border-color: #189aea;} - -#SideLineWidget[state="child-invalid"] {border-color: #ad2e2e;} -#SideLineWidget[state="child-invalid"]:hover {border-color: #c93636;} - -#SideLineWidget[state="child-overriden"] {border-color: #e67300;} -#SideLineWidget[state="child-overriden"]:hover {border-color: #ff8c1a;} - -#SideLineWidget[state="child-overriden-modified"] {border-color: #106aa2;} -#SideLineWidget[state="child-overriden-modified"]:hover {border-color: #189aea;} - -#MainWidget { - background: #141a1f; -} - -#DictAsWidgetBody { - background: transparent; -} -#DictAsWidgetBody[show_borders="1"] { - border: 1px solid #4e5254; - border-radius: 5px; -} - -#SplitterItem { - background-color: #21252B; -} - -#ShadowWidget { - font-size: 36pt; -} -QTabWidget::pane { - border-top-style: none; -} - -QTabBar { - background: transparent; -} - -QTabBar::tab { - border-top-left-radius: 4px; - border-top-right-radius: 4px; - padding: 5px; -} - -QTabBar::tab:selected { - background: #282C34; - border-color: #9B9B9B; - border-bottom-color: #C2C7CB; -} - -QTabBar::tab:!selected { - margin-top: 2px; - background: #21252B; -} - -QTabBar::tab:!selected:hover { - background: #333840; -} - -QTabBar::tab:first:selected { - margin-left: 0; -} - -QTabBar::tab:last:selected { - margin-right: 0; -} - -QTabBar::tab:only-one { - margin: 0; -} - -QScrollBar:horizontal { - height: 15px; - margin: 3px 15px 3px 15px; - border: 1px transparent #21252B; - border-radius: 4px; - background-color: #21252B; -} - -QScrollBar::handle:horizontal { - background-color: #4B5362; - min-width: 5px; - border-radius: 4px; -} - -QScrollBar::add-line:horizontal { - margin: 0px 3px 0px 3px; - border-image: url(:/qss_icons/rc/right_arrow_disabled.png); - width: 10px; - height: 10px; - subcontrol-position: right; - subcontrol-origin: margin; -} - -QScrollBar::sub-line:horizontal { - margin: 0px 3px 0px 3px; - border-image: url(:/qss_icons/rc/left_arrow_disabled.png); - height: 10px; - width: 10px; - subcontrol-position: left; - subcontrol-origin: margin; -} - -QScrollBar::add-line:horizontal:hover,QScrollBar::add-line:horizontal:on { - border-image: url(:/qss_icons/rc/right_arrow.png); - height: 10px; - width: 10px; - subcontrol-position: right; - subcontrol-origin: margin; -} - -QScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on { - border-image: url(:/qss_icons/rc/left_arrow.png); - height: 10px; - width: 10px; - subcontrol-position: left; - subcontrol-origin: margin; -} - -QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal { - background: none; -} - -QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { - background: none; -} - -QScrollBar:vertical { - background-color: #21252B; - width: 15px; - margin: 15px 3px 15px 3px; - border: 1px transparent #21252B; - border-radius: 4px; -} - -QScrollBar::handle:vertical { - background-color: #4B5362; - min-height: 5px; - border-radius: 4px; -} - -QScrollBar::sub-line:vertical { - margin: 3px 0px 3px 0px; - border-image: url(:/qss_icons/rc/up_arrow_disabled.png); - height: 10px; - width: 10px; - subcontrol-position: top; - subcontrol-origin: margin; -} - -QScrollBar::add-line:vertical { - margin: 3px 0px 3px 0px; - border-image: url(:/qss_icons/rc/down_arrow_disabled.png); - height: 10px; - width: 10px; - subcontrol-position: bottom; - subcontrol-origin: margin; -} - -QScrollBar::sub-line:vertical:hover,QScrollBar::sub-line:vertical:on { - - border-image: url(:/qss_icons/rc/up_arrow.png); - height: 10px; - width: 10px; - subcontrol-position: top; - subcontrol-origin: margin; -} - - -QScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on { - border-image: url(:/qss_icons/rc/down_arrow.png); - height: 10px; - width: 10px; - subcontrol-position: bottom; - subcontrol-origin: margin; -} - -QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { - background: none; -} - - -QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { - background: none; -} - -QTableView -{ - border: 1px solid #444; - gridline-color: #6c6c6c; - background-color: #201F1F; - alternate-background-color:#21252B; -} - -QHeaderView -{ - border: 1px transparent; - border-radius: 2px; - margin: 0px; - padding: 0px; -} - -QHeaderView::section { - background-color: #21252B; - /*color: silver;*/ - padding: 4px; - border: 1px solid #6c6c6c; - border-radius: 0px; - text-align: center; - color: #969b9e; - font-weight: bold; -} - -QAbstractItemView::item:pressed { - background: #78879b; - color: #FFFFFF; -} - -QAbstractItemView::item:selected:active { - background: #3d8ec9; -} -QAbstractItemView::item:selected:!active { - background: #3d8ec9; -} - -#BreadcrumbsPathInput { - padding: 2px; - font-size: 9pt; -} - -#BreadcrumbsButton { - padding-right: 12px; - font-size: 9pt; -} - -#BreadcrumbsButton[empty="1"] { - padding-right: 0px; -} - -#BreadcrumbsButton::menu-button { - width: 12px; - background: rgba(127, 127, 127, 60); -} -#BreadcrumbsButton::menu-button:hover { - background: rgba(127, 127, 127, 90); -} - -#BreadcrumbsPanel { - border: 1px solid #4e5254; - border-radius: 5px; - background: #21252B;; -} diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 710884e9e5..7a7213fa66 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -6,7 +6,16 @@ from avalon.mongodb import ( AvalonMongoDB ) +from openpype.style import get_objected_colors +from openpype.tools.utils.widgets import ImageButton +from openpype.tools.utils.lib import paint_image_with_color + +from openpype.widgets.nice_checkbox import NiceCheckbox from openpype.settings.lib import get_system_settings +from .images import ( + get_pixmap, + get_image +) from .constants import ( DEFAULT_PROJECT_LABEL, PROJECT_NAME_ROLE, @@ -31,6 +40,78 @@ class SettingsPlainTextEdit(QtWidgets.QPlainTextEdit): self.focused_in.emit() +class SettingsToolBtn(ImageButton): + _mask_pixmap = None + _cached_icons = {} + + def __init__(self, btn_type, parent): + super(SettingsToolBtn, self).__init__(parent) + + icon, hover_icon = self._get_icon_type(btn_type) + + self.setIcon(icon) + + self._icon = icon + self._hover_icon = hover_icon + + @classmethod + def _get_icon_type(cls, btn_type): + if btn_type not in cls._cached_icons: + settings_colors = get_objected_colors()["settings"] + normal_color = settings_colors["image-btn"].get_qcolor() + hover_color = settings_colors["image-btn-hover"].get_qcolor() + disabled_color = settings_colors["image-btn-disabled"].get_qcolor() + + image = get_image("{}.png".format(btn_type)) + + pixmap = paint_image_with_color(image, normal_color) + hover_pixmap = paint_image_with_color(image, hover_color) + disabled_pixmap = paint_image_with_color(image, disabled_color) + + icon = QtGui.QIcon(pixmap) + hover_icon = QtGui.QIcon(hover_pixmap) + icon.addPixmap( + disabled_pixmap, QtGui.QIcon.Disabled, QtGui.QIcon.On + ) + icon.addPixmap( + disabled_pixmap, QtGui.QIcon.Disabled, QtGui.QIcon.Off + ) + hover_icon.addPixmap( + disabled_pixmap, QtGui.QIcon.Disabled, QtGui.QIcon.On + ) + hover_icon.addPixmap( + disabled_pixmap, QtGui.QIcon.Disabled, QtGui.QIcon.Off + ) + cls._cached_icons[btn_type] = icon, hover_icon + return cls._cached_icons[btn_type] + + def enterEvent(self, event): + self.setIcon(self._hover_icon) + super(SettingsToolBtn, self).enterEvent(event) + + def leaveEvent(self, event): + self.setIcon(self._icon) + super(SettingsToolBtn, self).leaveEvent(event) + + @classmethod + def _get_mask_pixmap(cls): + if cls._mask_pixmap is None: + mask_pixmap = get_pixmap("mask.png") + cls._mask_pixmap = mask_pixmap + return cls._mask_pixmap + + def _change_size(self): + super(SettingsToolBtn, self)._change_size() + size = self.iconSize() + scaled = self._get_mask_pixmap().scaled( + size.width(), + size.height(), + QtCore.Qt.IgnoreAspectRatio, + QtCore.Qt.SmoothTransformation + ) + self.setMask(scaled.mask()) + + class ShadowWidget(QtWidgets.QWidget): def __init__(self, message, parent): super(ShadowWidget, self).__init__(parent) @@ -132,9 +213,14 @@ class SettingsComboBox(QtWidgets.QComboBox): def __init__(self, *args, **kwargs): super(SettingsComboBox, self).__init__(*args, **kwargs) + delegate = QtWidgets.QStyledItemDelegate() + self.setItemDelegate(delegate) + self.currentIndexChanged.connect(self._on_change) self.setFocusPolicy(QtCore.Qt.StrongFocus) + self._delegate = delegate + def wheelEvent(self, event): if self.hasFocus(): return super(SettingsComboBox, self).wheelEvent(event) @@ -180,14 +266,14 @@ class ExpandingWidget(QtWidgets.QWidget): button_size = QtCore.QSize(5, 5) button_toggle = QtWidgets.QToolButton(parent=side_line_widget) - button_toggle.setProperty("btn-type", "expand-toggle") + button_toggle.setObjectName("ExpandToggleBtn") button_toggle.setIconSize(button_size) button_toggle.setArrowType(QtCore.Qt.RightArrow) button_toggle.setCheckable(True) button_toggle.setChecked(False) label_widget = QtWidgets.QLabel(label, parent=side_line_widget) - label_widget.setObjectName("DictLabel") + label_widget.setObjectName("ExpandLabel") before_label_widget = QtWidgets.QWidget(side_line_widget) before_label_layout = QtWidgets.QHBoxLayout(before_label_widget) @@ -381,6 +467,7 @@ class GridLabelWidget(QtWidgets.QWidget): self.properties = {} label_widget = QtWidgets.QLabel(label, self) + label_widget.setObjectName("SettingsLabel") label_proxy_layout = QtWidgets.QHBoxLayout() label_proxy_layout.setContentsMargins(0, 0, 0, 0) @@ -415,197 +502,12 @@ class GridLabelWidget(QtWidgets.QWidget): return super(GridLabelWidget, self).mouseReleaseEvent(event) -class NiceCheckboxMoveWidget(QtWidgets.QFrame): - def __init__(self, height, border_width, parent): - super(NiceCheckboxMoveWidget, self).__init__(parent=parent) - - self.checkstate = False - - self.half_size = int(height / 2) - self.full_size = self.half_size * 2 - self.border_width = border_width - self.setFixedHeight(self.full_size) - self.setFixedWidth(self.full_size) - - self.setStyleSheet(( - "background: #444444;border-style: none;" - "border-radius: {};border-width:{}px;" - ).format(self.half_size, self.border_width)) - - def update_position(self): - parent_rect = self.parent().rect() - if self.checkstate is True: - pos_x = ( - parent_rect.x() - + parent_rect.width() - - self.full_size - - self.border_width - ) - else: - pos_x = parent_rect.x() + self.border_width - - pos_y = parent_rect.y() + int( - parent_rect.height() / 2 - self.half_size - ) - self.setGeometry(pos_x, pos_y, self.width(), self.height()) - - def state_offset(self): - diff_x = ( - self.parent().rect().width() - - self.full_size - - (2 * self.border_width) - ) - return QtCore.QPoint(diff_x, 0) - - def change_position(self, checkstate): - self.checkstate = checkstate - - self.update_position() - - def resizeEvent(self, event): - super().resizeEvent(event) - self.update_position() - - -class NiceCheckbox(QtWidgets.QFrame): - stateChanged = QtCore.Signal(int) - checked_bg_color = QtGui.QColor(69, 128, 86) - unchecked_bg_color = QtGui.QColor(170, 80, 80) +class SettingsNiceCheckbox(NiceCheckbox): focused_in = QtCore.Signal() - def set_bg_color(self, color): - self._bg_color = color - self.setStyleSheet(self._stylesheet_template.format( - color.red(), color.green(), color.blue() - )) - - def bg_color(self): - return self._bg_color - - bgcolor = QtCore.Property(QtGui.QColor, bg_color, set_bg_color) - - def __init__(self, checked=True, height=30, *args, **kwargs): - super(NiceCheckbox, self).__init__(*args, **kwargs) - - self._checkstate = checked - if checked: - bg_color = self.checked_bg_color - else: - bg_color = self.unchecked_bg_color - - self.half_height = int(height / 2) - height = self.half_height * 2 - tenth_height = int(height / 10) - - self.setFixedHeight(height) - self.setFixedWidth((height - tenth_height) * 2) - - move_item_size = height - (2 * tenth_height) - - self.move_item = NiceCheckboxMoveWidget( - move_item_size, tenth_height, self - ) - self.move_item.change_position(self._checkstate) - - self._stylesheet_template = ( - "border-radius: {}px;" - "border-width: {}px;" - "background: #333333;" - "border-style: solid;" - "border-color: #555555;" - ).format(self.half_height, tenth_height) - self._stylesheet_template += "background: rgb({},{},{});" - - self.set_bg_color(bg_color) - - def resizeEvent(self, event): - super(NiceCheckbox, self).resizeEvent(event) - self.move_item.update_position() - - def show(self, *args, **kwargs): - super(NiceCheckbox, self).show(*args, **kwargs) - self.move_item.update_position() - - def checkState(self): - if self._checkstate: - return QtCore.Qt.Checked - else: - return QtCore.Qt.Unchecked - - def _on_checkstate_change(self): - self.stateChanged.emit(self.checkState()) - - move_start_value = self.move_item.pos() - offset = self.move_item.state_offset() - if self._checkstate is True: - move_end_value = move_start_value + offset - else: - move_end_value = move_start_value - offset - move_animation = QtCore.QPropertyAnimation( - self.move_item, b"pos", self - ) - move_animation.setDuration(150) - move_animation.setEasingCurve(QtCore.QEasingCurve.OutQuad) - move_animation.setStartValue(move_start_value) - move_animation.setEndValue(move_end_value) - - color_animation = QtCore.QPropertyAnimation( - self, b"bgcolor" - ) - color_animation.setDuration(150) - if self._checkstate is True: - color_animation.setStartValue(self.unchecked_bg_color) - color_animation.setEndValue(self.checked_bg_color) - else: - color_animation.setStartValue(self.checked_bg_color) - color_animation.setEndValue(self.unchecked_bg_color) - - anim_group = QtCore.QParallelAnimationGroup(self) - anim_group.addAnimation(move_animation) - anim_group.addAnimation(color_animation) - - def _finished(): - self.move_item.change_position(self._checkstate) - - anim_group.finished.connect(_finished) - anim_group.start() - - def isChecked(self): - return self._checkstate - - def setChecked(self, checked): - if checked == self._checkstate: - return - self._checkstate = checked - self._on_checkstate_change() - - def setCheckState(self, state=None): - if state is None: - checkstate = not self._checkstate - elif state == QtCore.Qt.Checked: - checkstate = True - elif state == QtCore.Qt.Unchecked: - checkstate = False - else: - return - - if checkstate == self._checkstate: - return - - self._checkstate = checkstate - - self._on_checkstate_change() - def mousePressEvent(self, event): self.focused_in.emit() - super(NiceCheckbox, self).mousePressEvent(event) - - def mouseReleaseEvent(self, event): - if event.button() == QtCore.Qt.LeftButton: - self.setCheckState() - event.accept() - return - return super(NiceCheckbox, self).mouseReleaseEvent(event) + super(SettingsNiceCheckbox, self).mousePressEvent(event) class ProjectModel(QtGui.QStandardItemModel): diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index 4e88301349..fd0cd1d7cd 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -5,7 +5,7 @@ from .categories import ( ProjectWidget ) from .widgets import ShadowWidget, RestartDialog -from . import style +from openpype import style from openpype.lib import is_admin_password_required from openpype.widgets import PasswordDialog @@ -25,7 +25,7 @@ class MainWidget(QtWidgets.QWidget): self._password_dialog = None - self.setObjectName("MainWidget") + self.setObjectName("SettingsMainWidget") self.setWindowTitle("OpenPype Settings") self.resize(self.widget_width, self.widget_height) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 4626e35a93..6742df8557 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -25,6 +25,34 @@ def center_window(window): window.move(geo.topLeft()) +def paint_image_with_color(image, color): + """Redraw image with single color using it's alpha. + + It is expected that input image is singlecolor image with alpha. + + Args: + image (QImage): Loaded image with alpha. + color (QColor): Color that will be used to paint image. + """ + width = image.width() + height = image.height() + + alpha_mask = image.createAlphaMask() + alpha_region = QtGui.QRegion(QtGui.QBitmap.fromImage(alpha_mask)) + + pixmap = QtGui.QPixmap(width, height) + pixmap.fill(QtCore.Qt.transparent) + + painter = QtGui.QPainter(pixmap) + painter.setClipRegion(alpha_region) + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(color) + painter.drawRect(QtCore.QRect(0, 0, width, height)) + painter.end() + + return pixmap + + def format_version(value, hero_version=False): """Formats integer to displayable version name""" label = "v{0:03d}".format(value) @@ -477,6 +505,7 @@ def create_qthread(func, *args, **kwargs): def get_repre_icons(): + """Returns a dict {'provider_name': QIcon}""" try: from openpype_modules import sync_server except Exception: @@ -488,9 +517,17 @@ def get_repre_icons(): "providers", "resources" ) icons = {} - # TODO get from sync module - for provider in ['studio', 'local_drive', 'gdrive']: - pix_url = "{}/{}.png".format(resource_path, provider) + if not os.path.exists(resource_path): + print("No icons for Site Sync found") + return {} + + for file_name in os.listdir(resource_path): + if file_name and not file_name.endswith("png"): + continue + + provider, _ = os.path.splitext(file_name) + + pix_url = os.path.join(resource_path, file_name) icons[provider] = QtGui.QIcon(pix_url) return icons diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index ea80636d1a..009c1dc506 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -30,6 +30,32 @@ class PlaceholderLineEdit(QtWidgets.QLineEdit): self.setPalette(filter_palette) +class ImageButton(QtWidgets.QPushButton): + """PushButton with icon and size of font. + + Using font metrics height as icon size reference. + + TODO: + - handle changes of screen (different resolution) + """ + + def __init__(self, *args, **kwargs): + super(ImageButton, self).__init__(*args, **kwargs) + self.setObjectName("ImageButton") + + def _change_size(self): + font_height = self.fontMetrics().height() + self.setIconSize(QtCore.QSize(font_height, font_height)) + + def showEvent(self, event): + super(ImageButton, self).showEvent(event) + + self._change_size() + + def sizeHint(self): + return self.iconSize() + + class OptionalMenu(QtWidgets.QMenu): """A subclass of `QtWidgets.QMenu` to work with `OptionalAction` diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index 4f5e179d9b..a4b1717a1c 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -100,9 +100,7 @@ class NameWindow(QtWidgets.QDialog): # Store project anatomy self.anatomy = anatomy - self.template = anatomy.templates[template_key]["file"].replace( - "{task}", "{task[name]}" - ) + self.template = anatomy.templates[template_key]["file"] self.template_key = template_key # Btns widget diff --git a/openpype/version.py b/openpype/version.py index 5e23fc2168..2e9592f57d 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.7.0-nightly.2" +__version__ = "3.7.0-nightly.3" diff --git a/openpype/widgets/message_window.py b/openpype/widgets/message_window.py index 969d6ccdd1..94e51f5d4f 100644 --- a/openpype/widgets/message_window.py +++ b/openpype/widgets/message_window.py @@ -1,6 +1,6 @@ -from Qt import QtWidgets, QtCore import sys import logging +from Qt import QtWidgets, QtCore log = logging.getLogger(__name__) diff --git a/openpype/widgets/nice_checkbox.py b/openpype/widgets/nice_checkbox.py index d550f361ff..ccd079c0fb 100644 --- a/openpype/widgets/nice_checkbox.py +++ b/openpype/widgets/nice_checkbox.py @@ -1,11 +1,18 @@ from math import floor, sqrt, ceil from Qt import QtWidgets, QtCore, QtGui +from openpype.style import get_objected_colors + class NiceCheckbox(QtWidgets.QFrame): stateChanged = QtCore.Signal(int) clicked = QtCore.Signal() + _checked_bg_color = None + _unchecked_bg_color = None + _checker_color = None + _checker_hover_color = None + def __init__(self, checked=False, draw_icons=False, parent=None): super(NiceCheckbox, self).__init__(parent) @@ -41,12 +48,6 @@ class NiceCheckbox(QtWidgets.QFrame): self._pressed = False self._under_mouse = False - self.checked_bg_color = QtGui.QColor(67, 181, 129) - self.unchecked_bg_color = QtGui.QColor(79, 79, 79) - - self.checker_checked_color = QtGui.QColor(255, 255, 255) - self.checker_unchecked_color = self.checker_checked_color - self.icon_scale_factor = sqrt(2) / 2 icon_path_stroker = QtGui.QPainterPathStroker() @@ -58,6 +59,37 @@ class NiceCheckbox(QtWidgets.QFrame): self._animation_timer.timeout.connect(self._on_animation_timeout) self._base_size = QtCore.QSize(90, 50) + self._load_colors() + + @classmethod + def _load_colors(cls): + if cls._checked_bg_color is not None: + return + + colors_data = get_objected_colors() + colors_info = colors_data["nice-checkbox"] + + cls._checked_bg_color = colors_info["bg-checked"].get_qcolor() + cls._unchecked_bg_color = colors_info["bg-unchecked"].get_qcolor() + + cls._checker_color = colors_info["bg-checker"].get_qcolor() + cls._checker_hover_color = colors_info["bg-checker-hover"].get_qcolor() + + @property + def checked_bg_color(self): + return self._checked_bg_color + + @property + def unchecked_bg_color(self): + return self._unchecked_bg_color + + @property + def checker_color(self): + return self._checker_color + + @property + def checker_hover_color(self): + return self._checker_hover_color def setTristate(self, tristate=True): if self._is_tristate != tristate: @@ -73,15 +105,6 @@ class NiceCheckbox(QtWidgets.QFrame): self._draw_icons = draw_icons self.repaint() - def _checkbox_size_hint(self): - checkbox_height = self.style().pixelMetric( - QtWidgets.QStyle.PM_IndicatorHeight - ) - checkbox_height += checkbox_height % 2 - width = (2 * checkbox_height) - (checkbox_height / 5) - new_size = QtCore.QSize(width, checkbox_height) - return new_size - def sizeHint(self): height = self.fontMetrics().height() width = self.get_width_hint_by_height(height) @@ -159,7 +182,7 @@ class NiceCheckbox(QtWidgets.QFrame): if self._animation_timer.isActive(): self._animation_timer.stop() - if self.isEnabled(): + if self.isVisible() and self.isEnabled(): # Start animation self._animation_timer.start(self._animation_timeout) else: @@ -235,14 +258,16 @@ class NiceCheckbox(QtWidgets.QFrame): def _on_animation_timeout(self): if self._checkstate == QtCore.Qt.Checked: - self._current_step += 1 if self._current_step == self._steps: self._animation_timer.stop() + return + self._current_step += 1 elif self._checkstate == QtCore.Qt.Unchecked: - self._current_step -= 1 if self._current_step == 0: self._animation_timer.stop() + return + self._current_step -= 1 else: if self._current_step < self._middle_step: @@ -291,11 +316,9 @@ class NiceCheckbox(QtWidgets.QFrame): # Draw inner background if self._current_step == self._steps: bg_color = self.checked_bg_color - checker_color = self.checker_checked_color elif self._current_step == 0: bg_color = self.unchecked_bg_color - checker_color = self.checker_unchecked_color else: offset_ratio = self._current_step / self._steps @@ -305,11 +328,6 @@ class NiceCheckbox(QtWidgets.QFrame): self.unchecked_bg_color, offset_ratio ) - checker_color = self.steped_color( - self.checker_checked_color, - self.checker_unchecked_color, - offset_ratio - ) margins_ratio = self._checker_margins_divider if margins_ratio > 0: @@ -359,52 +377,14 @@ class NiceCheckbox(QtWidgets.QFrame): checker_rect = QtCore.QRect(pos_x, pos_y, checker_size, checker_size) under_mouse = self.isEnabled() and self._under_mouse - - shadow_x = checker_rect.x() - shadow_y = checker_rect.y() + margin_size_c - shadow_size = min( - frame_rect.right() - shadow_x, - frame_rect.bottom() - shadow_y, - checker_size + (2 * margin_size_c) - ) - shadow_rect = QtCore.QRect( - checker_rect.x(), - shadow_y, - shadow_size, - shadow_size - ) - - shadow_brush = QtGui.QRadialGradient( - shadow_rect.center(), - shadow_rect.height() / 2 - ) - shadow_brush.setColorAt(0.6, QtCore.Qt.black) - shadow_brush.setColorAt(1, QtCore.Qt.transparent) - - painter.setPen(QtCore.Qt.transparent) - painter.setBrush(shadow_brush) - painter.drawEllipse(shadow_rect) + if under_mouse: + checker_color = self.checker_hover_color + else: + checker_color = self.checker_color painter.setBrush(checker_color) painter.drawEllipse(checker_rect) - if under_mouse: - adjust = margin_size_c - if adjust < 1 and checker_rect.height() > 4: - adjust = 1 - - smaller_checker_rect = checker_rect.adjusted( - adjust, adjust, -adjust, -adjust - ) - gradient = QtGui.QLinearGradient( - smaller_checker_rect.bottomRight(), - smaller_checker_rect.topLeft() - ) - gradient.setColorAt(0, checker_color) - gradient.setColorAt(1, checker_color.darker(155)) - painter.setBrush(gradient) - painter.drawEllipse(smaller_checker_rect) - if self._draw_icons: painter.setBrush(bg_color) icon_path = self._get_icon_path(painter, checker_rect) diff --git a/openpype/widgets/popup.py b/openpype/widgets/popup.py index 7c0fa0f5c5..3c3f6283c4 100644 --- a/openpype/widgets/popup.py +++ b/openpype/widgets/popup.py @@ -3,7 +3,7 @@ import logging import contextlib -from avalon.vendor.Qt import QtCore, QtWidgets, QtGui +from Qt import QtCore, QtWidgets log = logging.getLogger(__name__) diff --git a/openpype/widgets/project_settings.py b/openpype/widgets/project_settings.py index c69d55fb39..43ff9f2789 100644 --- a/openpype/widgets/project_settings.py +++ b/openpype/widgets/project_settings.py @@ -1,10 +1,9 @@ - - -from avalon.vendor.Qt import QtCore, QtGui, QtWidgets import os import getpass import platform +from Qt import QtCore, QtGui, QtWidgets + from avalon import style import ftrack_api diff --git a/pyproject.toml b/pyproject.toml index 8f42fd31a7..ac1d133561 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.7.0-nightly.2" # OpenPype +version = "3.7.0-nightly.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" diff --git a/start.py b/start.py index 61d8d6a64a..0f7e82071d 100644 --- a/start.py +++ b/start.py @@ -100,6 +100,7 @@ import platform import traceback import subprocess import site +import distutils.spawn from pathlib import Path # OPENPYPE_ROOT is variable pointing to build (or code) directory @@ -384,23 +385,6 @@ def set_modules_environments(): os.environ.update(env) -def is_tool(name): - try: - import os.errno as errno - except ImportError: - import errno - - try: - devnull = open(os.devnull, "w") - subprocess.Popen( - [name], stdout=devnull, stderr=devnull - ).communicate() - except OSError as exc: - if exc.errno == errno.ENOENT: - return False - return True - - def _startup_validations(): """Validations before OpenPype starts.""" try: @@ -443,7 +427,8 @@ def _validate_thirdparty_binaries(): if low_platform == "windows": ffmpeg_dir = os.path.join(ffmpeg_dir, "bin") ffmpeg_executable = os.path.join(ffmpeg_dir, "ffmpeg") - if not is_tool(ffmpeg_executable): + ffmpeg_result = distutils.spawn.find_executable(ffmpeg_executable) + if ffmpeg_result is None: raise RuntimeError(error_msg.format("FFmpeg")) # Validate existence of OpenImageIO (not on MacOs) @@ -463,8 +448,11 @@ def _validate_thirdparty_binaries(): low_platform, "oiiotool" ) - if oiio_tool_path is not None and not is_tool(oiio_tool_path): - raise RuntimeError(error_msg.format("OpenImageIO")) + oiio_result = None + if oiio_tool_path is not None: + oiio_result = distutils.spawn.find_executable(oiio_tool_path) + if oiio_result is None: + raise RuntimeError(error_msg.format("OpenImageIO")) def _process_arguments() -> tuple: diff --git a/vendor/deadline/custom/plugins/GlobalJobPreLoad.py b/vendor/deadline/custom/plugins/GlobalJobPreLoad.py index 8631b035cf..0aa5adaa20 100644 --- a/vendor/deadline/custom/plugins/GlobalJobPreLoad.py +++ b/vendor/deadline/custom/plugins/GlobalJobPreLoad.py @@ -16,8 +16,9 @@ def inject_openpype_environment(deadlinePlugin): job = deadlinePlugin.GetJob() job = RepositoryUtils.GetJob(job.JobId, True) # invalidates cache - print("inject_openpype_environment start") + print(">>> Injecting OpenPype environments ...") try: + print(">>> Getting OpenPype executable ...") exe_list = job.GetJobExtraInfoKeyValue("openpype_executables") openpype_app = FileUtils.SearchFileList(exe_list) if openpype_app == "": @@ -27,11 +28,13 @@ def inject_openpype_environment(deadlinePlugin): "The path to the render executable can be configured " + "from the Plugin Configuration in the Deadline Monitor.") + print("--- OpenPype executable: {}".format(openpype_app)) + # tempfile.TemporaryFile cannot be used because of locking export_url = os.path.join(tempfile.gettempdir(), time.strftime('%Y%m%d%H%M%S'), 'env.json') # add HHMMSS + delete later - print("export_url {}".format(export_url)) + print(">>> Temporary path: {}".format(export_url)) args = [ openpype_app, @@ -55,41 +58,52 @@ def inject_openpype_environment(deadlinePlugin): "AVALON_TASK, AVALON_APP_NAME" raise RuntimeError(msg) - print("args:::{}".format(args)) + if not os.environ.get("OPENPYPE_MONGO"): + print(">>> Missing OPENPYPE_MONGO env var, process won't work") - exit_code = subprocess.call(args, cwd=os.path.dirname(openpype_app)) - if exit_code != 0: - raise RuntimeError("Publishing failed, check worker's log") + env = os.environ + env["OPENPYPE_HEADLESS_MODE"] = "1" + env["AVALON_TIMEOUT"] = "5000" + print(">>> Executing: {}".format(args)) + std_output = subprocess.check_output(args, + cwd=os.path.dirname(openpype_app), + env=env) + print(">>> Process result {}".format(std_output)) + + print(">>> Loading file ...") with open(export_url) as fp: contents = json.load(fp) for key, value in contents.items(): deadlinePlugin.SetProcessEnvironmentVariable(key, value) + print(">>> Removing temporary file") os.remove(export_url) - print("inject_openpype_environment end") - except Exception: + print(">> Injection end.") + except Exception as e: + if hasattr(e, "output"): + print(">>> Exception {}".format(e.output)) import traceback print(traceback.format_exc()) - print("inject_openpype_environment failed") + print("!!! Injection failed.") RepositoryUtils.FailJob(job) raise def inject_render_job_id(deadlinePlugin): """Inject dependency ids to publish process as env var for validation.""" - print("inject_render_job_id start") + print(">>> Injecting render job id ...") job = deadlinePlugin.GetJob() job = RepositoryUtils.GetJob(job.JobId, True) # invalidates cache dependency_ids = job.JobDependencyIDs - print("dependency_ids {}".format(dependency_ids)) + print(">>> Dependency IDs: {}".format(dependency_ids)) render_job_ids = ",".join(dependency_ids) deadlinePlugin.SetProcessEnvironmentVariable("RENDER_JOB_IDS", render_job_ids) - print("inject_render_job_id end") + print(">>> Injection end.") def pype_command_line(executable, arguments, workingDirectory): @@ -133,10 +147,13 @@ def pype(deadlinePlugin): deadlinePlugin: Deadline job plugin passed by Deadline """ + print(">>> Getting job ...") job = deadlinePlugin.GetJob() # PYPE should be here, not OPENPYPE - backward compatibility!! pype_metadata = job.GetJobEnvironmentKeyValue("PYPE_METADATA_FILE") pype_python = job.GetJobEnvironmentKeyValue("PYPE_PYTHON_EXE") + print(">>> Having backward compatible env vars {}/{}".format(pype_metadata, + pype_python)) # test if it is pype publish job. if pype_metadata: pype_metadata = RepositoryUtils.CheckPathMapping(pype_metadata) @@ -162,6 +179,8 @@ def pype(deadlinePlugin): def __main__(deadlinePlugin): + print("*** GlobalJobPreload start ...") + print(">>> Getting job ...") job = deadlinePlugin.GetJob() job = RepositoryUtils.GetJob(job.JobId, True) # invalidates cache @@ -170,6 +189,8 @@ def __main__(deadlinePlugin): openpype_publish_job = \ job.GetJobEnvironmentKeyValue('OPENPYPE_PUBLISH_JOB') or '0' + print("--- Job type - render {}".format(openpype_render_job)) + print("--- Job type - publish {}".format(openpype_publish_job)) if openpype_publish_job == '1' and openpype_render_job == '1': raise RuntimeError("Misconfiguration. Job couldn't be both " + "render and publish.") diff --git a/website/src/css/custom.css b/website/src/css/custom.css index 4f7f8396f6..0a72dc0f23 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -197,7 +197,7 @@ h5, h6 { font-weight: var(--ifm-font-weight-semibold); } } .showcase .client img { - max-height: 80px; + max-height: 70px; padding: 20px; max-width: 120px; align-self: center; @@ -215,10 +215,10 @@ h5, h6 { font-weight: var(--ifm-font-weight-semibold); } } .showcase .collab img { - max-height: 60px; + max-height: 70px; padding: 20px; align-self: center; - max-width: 200px; + max-width: 160px; } .showcase .pype_logo img{ diff --git a/website/src/pages/index.js b/website/src/pages/index.js index 00cf002aec..29b81e973f 100644 --- a/website/src/pages/index.js +++ b/website/src/pages/index.js @@ -64,6 +64,10 @@ const collab = [ title: 'Clothcat Animation', image: '/img/clothcat.png', infoLink: 'https://www.clothcatanimation.com/' + }, { + title: 'Ellipse Studio', + image: '/img/ellipse-studio.png', + infoLink: 'http://www.dargaudmedia.com' } ]; @@ -125,7 +129,7 @@ const studios = [ title: "Moonrock Animation Studio", image: "/img/moonrock_logo.png", infoLink: "https://www.moonrock.eu/", - } + } ]; function Service({imageUrl, title, description}) { diff --git a/website/static/img/ellipse-studio.png b/website/static/img/ellipse-studio.png new file mode 100644 index 0000000000..c6fd62a6d5 Binary files /dev/null and b/website/static/img/ellipse-studio.png differ