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/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_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 20a9d4ca12..53897b21f6 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -386,10 +386,13 @@ class CollectLook(pyblish.api.InstancePlugin): self.log.info("Collected resources: {}".format(instance.data["resources"])) - # Log a warning when no relevant sets were retrieved for the look. - if not instance.data["lookData"]["relationships"]: - self.log.warning("No sets found for the nodes in the instance: " - "%s" % instance[:]) + # Log warning when no relevant sets were retrieved for the look. + if ( + not instance.data["lookData"]["relationships"] + and "model" not in self.families + ): + self.log.warning("No sets found for the nodes in the " + "instance: %s" % instance[:]) # Ensure unique shader sets # Add shader sets to the instance for unify ID validation 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_model_content.py b/openpype/hosts/maya/plugins/publish/validate_model_content.py index 3d4f122af4..aee0ea52f0 100644 --- a/openpype/hosts/maya/plugins/publish/validate_model_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_model_content.py @@ -9,7 +9,7 @@ from openpype.hosts.maya.api import lib class ValidateModelContent(pyblish.api.InstancePlugin): """Adheres to the content of 'model' family - - Must have one top group. + - Must have one top group. (configurable) - Must only contain: transforms, meshes and groups """ @@ -20,6 +20,8 @@ class ValidateModelContent(pyblish.api.InstancePlugin): label = "Model Content" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] + validate_top_group = True + @classmethod def get_invalid(cls, instance): @@ -59,12 +61,13 @@ class ValidateModelContent(pyblish.api.InstancePlugin): # Top group assemblies = cmds.ls(content_instance, assemblies=True, long=True) - if len(assemblies) != 1: + if len(assemblies) != 1 and cls.validate_top_group: cls.log.error("Must have exactly one top group") - if len(assemblies) == 0: - cls.log.warning("No top group found. " - "(Are there objects in the instance?" - " Or is it parented in another group?)") + return assemblies + if len(assemblies) == 0: + cls.log.warning("No top group found. " + "(Are there objects in the instance?" + " Or is it parented in another group?)") return assemblies or True def _is_visible(node): 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/modules/base.py b/openpype/modules/base.py index 6f9ddb2fd4..7ecfeae7bd 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -271,9 +271,29 @@ def _load_modules(): log = PypeLogger.get_logger("ModulesLoader") - # Look for OpenPype modules in paths defined with `get_module_dirs` - dirpaths = get_module_dirs() + # Import default modules imported from 'openpype.modules' + for default_module_name in ( + "settings_action", + "launcher_action", + "project_manager_action", + "standalonepublish_action", + ): + try: + default_module = __import__( + "openpype.modules.{}".format(default_module_name), + fromlist=("", ) + ) + setattr(openpype_modules, default_module_name, default_module) + except Exception: + msg = ( + "Failed to import default module '{}'." + ).format(default_module_name) + log.error(msg, exc_info=True) + + # Look for OpenPype modules in paths defined with `get_module_dirs` + # - dynamically imported OpenPype modules and addons + dirpaths = get_module_dirs() for dirpath in dirpaths: if not os.path.exists(dirpath): log.warning(( 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/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/settings_module/__init__.py b/openpype/modules/default_modules/settings_module/__init__.py deleted file mode 100644 index 95510eba9d..0000000000 --- a/openpype/modules/default_modules/settings_module/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .settings_action import ( - LocalSettingsAction, - SettingsAction -) - -__all__ = ( - "LocalSettingsAction", - "SettingsAction" -) 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/modules/default_modules/launcher_action.py b/openpype/modules/launcher_action.py similarity index 100% rename from openpype/modules/default_modules/launcher_action.py rename to openpype/modules/launcher_action.py diff --git a/openpype/modules/default_modules/project_manager_action.py b/openpype/modules/project_manager_action.py similarity index 100% rename from openpype/modules/default_modules/project_manager_action.py rename to openpype/modules/project_manager_action.py diff --git a/openpype/modules/default_modules/settings_module/settings_action.py b/openpype/modules/settings_action.py similarity index 100% rename from openpype/modules/default_modules/settings_module/settings_action.py rename to openpype/modules/settings_action.py diff --git a/openpype/modules/default_modules/standalonepublish_action.py b/openpype/modules/standalonepublish_action.py similarity index 100% rename from openpype/modules/default_modules/standalonepublish_action.py rename to openpype/modules/standalonepublish_action.py 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 ba6ef17072..3ab6ffd489 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -1010,10 +1010,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/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 73c75ef3ee..f4b9760fe1 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -275,6 +275,11 @@ "optional": true, "active": true }, + "ValidateModelContent": { + "enabled": true, + "optional": false, + "validate_top_group": true + }, "ValidateNoAnimation": { "enabled": false, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 9fd19d7be2..606dd6c2bb 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -214,6 +214,30 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "ValidateModelContent", + "label": "Validate Model Content", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "validate_top_group", + "label": "Validate one top group" + } + ] + }, { "type": "dict", "collapsible": true, 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/mayalookassigner/models.py b/openpype/tools/mayalookassigner/models.py index 80de6c1897..233c84db54 100644 --- a/openpype/tools/mayalookassigner/models.py +++ b/openpype/tools/mayalookassigner/models.py @@ -101,7 +101,8 @@ class LookModel(models.TreeModel): for look in asset_item["looks"]: look_subsets[look["name"]].append(asset) - for subset, assets in sorted(look_subsets.iteritems()): + for subset in sorted(look_subsets.keys()): + assets = look_subsets[subset] # Define nice label without "look" prefix for readability label = subset if not subset.startswith("look") else subset[4:] 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/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 0f817d7130..8c6a6d3266 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -370,8 +370,12 @@ class PypeTrayStarter(QtCore.QObject): splash = self._get_splash() splash.show() self._tray_widget.show() + # Make sure tray and splash are painted out + QtWidgets.QApplication.processEvents() elif self._timer_counter == 1: + # Second processing of events to make sure splash is painted + QtWidgets.QApplication.processEvents() self._timer_counter += 1 self._tray_widget.initialize_modules() diff --git a/openpype/tools/utils/assets_widget.py b/openpype/tools/utils/assets_widget.py index 041bb1ef1c..f310aafe89 100644 --- a/openpype/tools/utils/assets_widget.py +++ b/openpype/tools/utils/assets_widget.py @@ -347,14 +347,22 @@ class AssetModel(QtGui.QStandardItemModel): return self.get_indexes_by_asset_ids(asset_ids) - def refresh(self, force=False): - """Refresh the data for the model.""" + def refresh(self, force=False, clear=False): + """Refresh the data for the model. + + Args: + force (bool): Stop currently running refresh start new refresh. + clear (bool): Clear model before refresh thread starts. + """ # Skip fetch if there is already other thread fetching documents if self._refreshing: if not force: return self.stop_refresh() + if clear: + self._clear_items() + # Fetch documents from mongo # Restart payload self._refreshing = True @@ -379,15 +387,18 @@ class AssetModel(QtGui.QStandardItemModel): continue item.setData(colors, ASSET_UNDERLINE_COLORS_ROLE) + def _clear_items(self): + root_item = self.invisibleRootItem() + root_item.removeRows(0, root_item.rowCount()) + self._items_by_asset_id = {} + self._items_with_color_by_id = {} + def _on_docs_fetched(self): # Make sure refreshing did not change # - since this line is refreshing sequential and # triggering of new refresh will happen when this method is done if not self._refreshing: - root_item = self.invisibleRootItem() - root_item.removeRows(0, root_item.rowCount()) - self._items_by_asset_id = {} - self._items_with_color_by_id = {} + self._clear_items() return # Collect asset documents as needed @@ -419,8 +430,6 @@ class AssetModel(QtGui.QStandardItemModel): parent_id, parent_item = asset_items_queue.popleft() # Skip if there are no children children_ids = asset_ids_by_parents[parent_id] - if not children_ids: - continue # Go through current children of parent item # - find out items that were deleted and skip creation of already @@ -626,6 +635,7 @@ class AssetsWidget(QtWidgets.QWidget): self._model = model self._proxy = proxy self._view = view + self._last_project_name = None self.model_selection = {} @@ -634,7 +644,12 @@ class AssetsWidget(QtWidgets.QWidget): return self._model.refreshing def refresh(self): - self._refresh_model() + project_name = self.dbcon.Session.get("AVALON_PROJECT") + clear_model = False + if project_name != self._last_project_name: + clear_model = True + self._last_project_name = project_name + self._refresh_model(clear_model) def stop_refresh(self): self._model.stop_refresh() @@ -680,14 +695,14 @@ class AssetsWidget(QtWidgets.QWidget): self._set_loading_state(loading=False, empty=not has_item) self.refreshed.emit() - def _refresh_model(self): + def _refresh_model(self, clear=False): # Store selection self._set_loading_state(loading=True, empty=True) # Trigger signal before refresh is called self.refresh_triggered.emit() # Refresh model - self._model.refresh() + self._model.refresh(clear=clear) def _set_loading_state(self, loading, empty): self._view.set_loading_state(loading, empty) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index ef1cd3cf5c..60c9e79829 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -5,6 +5,7 @@ use singleton approach with global functions (using helper anyway). """ import avalon.api +from .lib import qt_app_context class HostToolsHelper: @@ -61,22 +62,23 @@ class HostToolsHelper: if save is None: save = True - workfiles_tool = self.get_workfiles_tool(parent) - workfiles_tool.set_save_enabled(save) + with qt_app_context(): + workfiles_tool = self.get_workfiles_tool(parent) + workfiles_tool.set_save_enabled(save) - if not workfiles_tool.isVisible(): - workfiles_tool.show() + if not workfiles_tool.isVisible(): + workfiles_tool.show() - if use_context: - context = { - "asset": avalon.api.Session["AVALON_ASSET"], - "task": avalon.api.Session["AVALON_TASK"] - } - workfiles_tool.set_context(context) + if use_context: + context = { + "asset": avalon.api.Session["AVALON_ASSET"], + "task": avalon.api.Session["AVALON_TASK"] + } + workfiles_tool.set_context(context) - # Pull window to the front. - workfiles_tool.raise_() - workfiles_tool.activateWindow() + # Pull window to the front. + workfiles_tool.raise_() + workfiles_tool.activateWindow() def get_loader_tool(self, parent): """Create, cache and return loader tool window.""" @@ -90,20 +92,21 @@ class HostToolsHelper: def show_loader(self, parent=None, use_context=None): """Loader tool for loading representations.""" - loader_tool = self.get_loader_tool(parent) + with qt_app_context(): + loader_tool = self.get_loader_tool(parent) - loader_tool.show() - loader_tool.raise_() - loader_tool.activateWindow() + loader_tool.show() + loader_tool.raise_() + loader_tool.activateWindow() - if use_context is None: - use_context = False + if use_context is None: + use_context = False - if use_context: - context = {"asset": avalon.api.Session["AVALON_ASSET"]} - loader_tool.set_context(context, refresh=True) - else: - loader_tool.refresh() + if use_context: + context = {"asset": avalon.api.Session["AVALON_ASSET"]} + loader_tool.set_context(context, refresh=True) + else: + loader_tool.refresh() def get_creator_tool(self, parent): """Create, cache and return creator tool window.""" @@ -117,13 +120,14 @@ class HostToolsHelper: def show_creator(self, parent=None): """Show tool to create new instantes for publishing.""" - creator_tool = self.get_creator_tool(parent) - creator_tool.refresh() - creator_tool.show() + with qt_app_context(): + creator_tool = self.get_creator_tool(parent) + creator_tool.refresh() + creator_tool.show() - # Pull window to the front. - creator_tool.raise_() - creator_tool.activateWindow() + # Pull window to the front. + creator_tool.raise_() + creator_tool.activateWindow() def get_subset_manager_tool(self, parent): """Create, cache and return subset manager tool window.""" @@ -139,12 +143,13 @@ class HostToolsHelper: def show_subset_manager(self, parent=None): """Show tool display/remove existing created instances.""" - subset_manager_tool = self.get_subset_manager_tool(parent) - subset_manager_tool.show() + with qt_app_context(): + subset_manager_tool = self.get_subset_manager_tool(parent) + subset_manager_tool.show() - # Pull window to the front. - subset_manager_tool.raise_() - subset_manager_tool.activateWindow() + # Pull window to the front. + subset_manager_tool.raise_() + subset_manager_tool.activateWindow() def get_scene_inventory_tool(self, parent): """Create, cache and return scene inventory tool window.""" @@ -160,13 +165,14 @@ class HostToolsHelper: def show_scene_inventory(self, parent=None): """Show tool maintain loaded containers.""" - scene_inventory_tool = self.get_scene_inventory_tool(parent) - scene_inventory_tool.show() - scene_inventory_tool.refresh() + with qt_app_context(): + scene_inventory_tool = self.get_scene_inventory_tool(parent) + scene_inventory_tool.show() + scene_inventory_tool.refresh() - # Pull window to the front. - scene_inventory_tool.raise_() - scene_inventory_tool.activateWindow() + # Pull window to the front. + scene_inventory_tool.raise_() + scene_inventory_tool.activateWindow() def get_library_loader_tool(self, parent): """Create, cache and return library loader tool window.""" @@ -182,11 +188,12 @@ class HostToolsHelper: def show_library_loader(self, parent=None): """Loader tool for loading representations from library project.""" - library_loader_tool = self.get_library_loader_tool(parent) - library_loader_tool.show() - library_loader_tool.raise_() - library_loader_tool.activateWindow() - library_loader_tool.refresh() + with qt_app_context(): + library_loader_tool = self.get_library_loader_tool(parent) + library_loader_tool.show() + library_loader_tool.raise_() + library_loader_tool.activateWindow() + library_loader_tool.refresh() def show_publish(self, parent=None): """Publish UI.""" @@ -207,9 +214,10 @@ class HostToolsHelper: """Look manager is Maya specific tool for look management.""" from avalon import style - look_assigner_tool = self.get_look_assigner_tool(parent) - look_assigner_tool.show() - look_assigner_tool.setStyleSheet(style.load_stylesheet()) + with qt_app_context(): + look_assigner_tool = self.get_look_assigner_tool(parent) + look_assigner_tool.show() + look_assigner_tool.setStyleSheet(style.load_stylesheet()) def get_experimental_tools_dialog(self, parent=None): """Dialog of experimental tools. @@ -232,11 +240,12 @@ class HostToolsHelper: def show_experimental_tools_dialog(self, parent=None): """Show dialog with experimental tools.""" - dialog = self.get_experimental_tools_dialog(parent) + with qt_app_context(): + dialog = self.get_experimental_tools_dialog(parent) - dialog.show() - dialog.raise_() - dialog.activateWindow() + dialog.show() + dialog.raise_() + dialog.activateWindow() def get_tool_by_name(self, tool_name, parent=None, *args, **kwargs): """Show tool by it's name. diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 8246d606b7..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) 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/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/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: