From 843fa6f6e08db1a6934b50d0be9739f627ccefb9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 14 May 2021 17:51:24 +0100 Subject: [PATCH 01/68] Removed Animation Set as asset type --- .../blender/plugins/create/create_setdress.py | 25 ---- .../hosts/blender/plugins/load/load_layout.py | 27 +--- .../publish/extract_animation_collection.py | 61 --------- .../unreal/plugins/load/load_setdress.py | 127 ------------------ 4 files changed, 7 insertions(+), 233 deletions(-) delete mode 100644 openpype/hosts/blender/plugins/create/create_setdress.py delete mode 100644 openpype/hosts/blender/plugins/publish/extract_animation_collection.py delete mode 100644 openpype/hosts/unreal/plugins/load/load_setdress.py diff --git a/openpype/hosts/blender/plugins/create/create_setdress.py b/openpype/hosts/blender/plugins/create/create_setdress.py deleted file mode 100644 index 97c737c235..0000000000 --- a/openpype/hosts/blender/plugins/create/create_setdress.py +++ /dev/null @@ -1,25 +0,0 @@ -import bpy - -from avalon import api, blender -import openpype.hosts.blender.api.plugin - - -class CreateSetDress(openpype.hosts.blender.api.plugin.Creator): - """A grouped package of loaded content""" - - name = "setdressMain" - label = "Set Dress" - family = "setdress" - icon = "cubes" - defaults = ["Main", "Anim"] - - def process(self): - asset = self.data["asset"] - subset = self.data["subset"] - name = openpype.hosts.blender.api.plugin.asset_name(asset, subset) - collection = bpy.data.collections.new(name=name) - bpy.context.scene.collection.children.link(collection) - self.data['task'] = api.Session.get('AVALON_TASK') - blender.lib.imprint(collection, self.data) - - return collection diff --git a/openpype/hosts/blender/plugins/load/load_layout.py b/openpype/hosts/blender/plugins/load/load_layout.py index f1f2fdcddd..08a905fbf0 100644 --- a/openpype/hosts/blender/plugins/load/load_layout.py +++ b/openpype/hosts/blender/plugins/load/load_layout.py @@ -25,9 +25,6 @@ class BlendLayoutLoader(plugin.AssetLoader): icon = "code-fork" color = "orange" - animation_creator_name = "CreateAnimation" - setdress_creator_name = "CreateSetDress" - def _remove(self, objects, obj_container): for obj in list(objects): if obj.type == 'ARMATURE': @@ -293,7 +290,6 @@ class UnrealLayoutLoader(plugin.AssetLoader): color = "orange" animation_creator_name = "CreateAnimation" - setdress_creator_name = "CreateSetDress" def _remove_objects(self, objects): for obj in list(objects): @@ -383,7 +379,7 @@ class UnrealLayoutLoader(plugin.AssetLoader): def _process( self, libpath, layout_container, container_name, representation, - actions, parent + actions, parent_collection ): with open(libpath, "r") as fp: data = json.load(fp) @@ -392,6 +388,11 @@ class UnrealLayoutLoader(plugin.AssetLoader): layout_collection = bpy.data.collections.new(container_name) scene.collection.children.link(layout_collection) + parent = parent_collection + + if parent is None: + parent = scene.collection + all_loaders = api.discover(api.Loader) avalon_container = bpy.data.collections.get( @@ -516,23 +517,9 @@ class UnrealLayoutLoader(plugin.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - # Create a setdress subset to contain all the animation for all - # the rigs in the layout - creator_plugin = get_creator_by_name(self.setdress_creator_name) - if not creator_plugin: - raise ValueError("Creator plugin \"{}\" was not found.".format( - self.setdress_creator_name - )) - parent = api.create( - creator_plugin, - name="animation", - asset=api.Session["AVALON_ASSET"], - options={"useSelection": True}, - data={"dependencies": str(context["representation"]["_id"])}) - layout_collection = self._process( libpath, layout_container, container_name, - str(context["representation"]["_id"]), None, parent) + str(context["representation"]["_id"]), None, None) container_metadata["obj_container"] = layout_collection diff --git a/openpype/hosts/blender/plugins/publish/extract_animation_collection.py b/openpype/hosts/blender/plugins/publish/extract_animation_collection.py deleted file mode 100644 index 19dc59c5cd..0000000000 --- a/openpype/hosts/blender/plugins/publish/extract_animation_collection.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -import json - -import openpype.api -import pyblish.api - -import bpy - - -class ExtractSetDress(openpype.api.Extractor): - """Extract setdress.""" - - label = "Extract SetDress" - hosts = ["blender"] - families = ["setdress"] - optional = True - order = pyblish.api.ExtractorOrder + 0.1 - - def process(self, instance): - stagingdir = self.staging_dir(instance) - - json_data = [] - - for i in instance.context: - collection = i.data.get("name") - container = None - for obj in bpy.data.collections[collection].objects: - if obj.type == "ARMATURE": - container_name = obj.get("avalon").get("container_name") - container = bpy.data.collections[container_name] - if container: - json_dict = { - "subset": i.data.get("subset"), - "container": container.name, - } - json_dict["instance_name"] = container.get("avalon").get( - "instance_name" - ) - json_data.append(json_dict) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - json_filename = f"{instance.name}.json" - json_path = os.path.join(stagingdir, json_filename) - - with open(json_path, "w+") as file: - json.dump(json_data, fp=file, indent=2) - - json_representation = { - "name": "json", - "ext": "json", - "files": json_filename, - "stagingDir": stagingdir, - } - instance.data["representations"].append(json_representation) - - self.log.info( - "Extracted instance '{}' to: {}".format(instance.name, - json_representation) - ) diff --git a/openpype/hosts/unreal/plugins/load/load_setdress.py b/openpype/hosts/unreal/plugins/load/load_setdress.py deleted file mode 100644 index da302deb1c..0000000000 --- a/openpype/hosts/unreal/plugins/load/load_setdress.py +++ /dev/null @@ -1,127 +0,0 @@ -import json - -from avalon import api -import unreal - - -class AnimationCollectionLoader(api.Loader): - """Load Unreal SkeletalMesh from FBX""" - - families = ["setdress"] - representations = ["json"] - - label = "Load Animation Collection" - icon = "cube" - color = "orange" - - def load(self, context, name, namespace, options): - from avalon import api, pipeline - from avalon.unreal import lib - from avalon.unreal import pipeline as unreal_pipeline - import unreal - - # Create directory for asset and avalon container - root = "/Game/Avalon/Assets" - asset = context.get('asset').get('name') - suffix = "_CON" - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}".format(root, asset), suffix="") - - container_name += suffix - - unreal.EditorAssetLibrary.make_directory(asset_dir) - - libpath = self.fname - - with open(libpath, "r") as fp: - data = json.load(fp) - - all_loaders = api.discover(api.Loader) - - for element in data: - reference = element.get('_id') - - loaders = api.loaders_from_representation(all_loaders, reference) - loader = None - for l in loaders: - if l.__name__ == "AnimationFBXLoader": - loader = l - break - - if not loader: - continue - - instance_name = element.get('instance_name') - - api.load( - loader, - reference, - namespace=instance_name, - options=element - ) - - # Create Asset Container - lib.create_avalon_container( - container=container_name, path=asset_dir) - - data = { - "schema": "openpype:container-2.0", - "id": pipeline.AVALON_CONTAINER_ID, - "asset": asset, - "namespace": asset_dir, - "container_name": container_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] - } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - return asset_content - - def update(self, container, representation): - from avalon import api, io - from avalon.unreal import pipeline - - source_path = api.get_representation_path(representation) - - with open(source_path, "r") as fp: - data = json.load(fp) - - animation_containers = [ - i for i in pipeline.ls() if - i.get('asset') == container.get('asset') and - i.get('family') == 'animation'] - - for element in data: - new_version = io.find_one({"_id": io.ObjectId(element.get('_id'))}) - new_version_number = new_version.get('context').get('version') - anim_container = None - for i in animation_containers: - if i.get('container_name') == (element.get('subset') + "_CON"): - anim_container = i - break - if not anim_container: - continue - - api.update(anim_container, new_version_number) - - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) - # update metadata - pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) - - def remove(self, container): - unreal.EditorAssetLibrary.delete_directory(container["namespace"]) From b4c826c4a93208ac85098fde0a1483ecff4c6e61 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 14 May 2021 17:52:11 +0100 Subject: [PATCH 02/68] Fixed problem with non local actions when loading rigs --- openpype/hosts/blender/plugins/load/load_rig.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index c5690a6ab8..0c92354310 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -107,6 +107,9 @@ class BlendRigLoader(plugin.AssetLoader): if action is not None: local_obj.animation_data.action = action + elif local_obj.animation_data.action is not None: + plugin.prepare_data( + local_obj.animation_data.action, collection_name) # Set link the drivers to the local object if local_obj.data.animation_data: From 8c03e16314db5fe886f5864a8c2e593bb79d5762 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 14 May 2021 17:56:16 +0100 Subject: [PATCH 03/68] Improved extraction of animation from Blender and loading in Unreal A json file is extracted together with the animation fbx. The json file stores the instance name of the asset in Unreal. Thus, when loading the animation in Unreal, it can be associated with a skeleton and automatically applied to the right SkeletalMesh. --- .../plugins/publish/extract_fbx_animation.py | 33 ++++++++++++++++ .../unreal/plugins/load/load_animation.py | 38 +++++++++++++++---- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py index 1036800705..418dd100b2 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -1,4 +1,5 @@ import os +import json import openpype.api @@ -121,6 +122,30 @@ class ExtractAnimationFBX(openpype.api.Extractor): pair[1].user_clear() bpy.data.actions.remove(pair[1]) + json_filename = f"{instance.name}.json" + json_path = os.path.join(stagingdir, json_filename) + + json_dict = {} + + collection = instance.data.get("name") + container = None + for obj in bpy.data.collections[collection].objects: + if obj.type == "ARMATURE": + container_name = obj.get("avalon").get("container_name") + container = bpy.data.collections[container_name] + if container: + json_dict = { + # "representation": container.get("avalon").get( + # "representation" + # ), + "instance_name": container.get("avalon").get( + "instance_name" + ) + } + + with open(json_path, "w+") as file: + json.dump(json_dict, fp=file, indent=2) + if "representations" not in instance.data: instance.data["representations"] = [] @@ -130,7 +155,15 @@ class ExtractAnimationFBX(openpype.api.Extractor): 'files': fbx_filename, "stagingDir": stagingdir, } + json_representation = { + 'name': 'json', + 'ext': 'json', + 'files': json_filename, + "stagingDir": stagingdir, + } instance.data["representations"].append(fbx_representation) + instance.data["representations"].append(json_representation) + self.log.info("Extracted instance '{}' to: {}".format( instance.name, fbx_representation)) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index 18910983ea..a53328847d 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -1,4 +1,5 @@ import os +import json from avalon import api, pipeline from avalon.unreal import lib @@ -61,10 +62,16 @@ class AnimationFBXLoader(api.Loader): task = unreal.AssetImportTask() task.options = unreal.FbxImportUI() - # If there are no options, the process cannot be automated - if options: + libpath = self.fname.replace("fbx", "json") + + with open(libpath, "r") as fp: + data = json.load(fp) + + instance_name = data.get("instance_name") + + if instance_name: automated = True - actor_name = 'PersistentLevel.' + options.get('instance_name') + actor_name = 'PersistentLevel.' + instance_name actor = unreal.EditorLevelLibrary.get_actor_reference(actor_name) skeleton = actor.skeletal_mesh_component.skeletal_mesh.skeleton task.options.set_editor_property('skeleton', skeleton) @@ -81,16 +88,31 @@ class AnimationFBXLoader(api.Loader): # set import options here task.options.set_editor_property( - 'automated_import_should_detect_type', True) + 'automated_import_should_detect_type', False) task.options.set_editor_property( - 'original_import_type', unreal.FBXImportType.FBXIT_ANIMATION) + 'original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH) + task.options.set_editor_property( + 'mesh_type_to_import', unreal.FBXImportType.FBXIT_ANIMATION) task.options.set_editor_property('import_mesh', False) task.options.set_editor_property('import_animations', True) + task.options.set_editor_property('override_full_name', True) - task.options.skeletal_mesh_import_data.set_editor_property( - 'import_content_type', - unreal.FBXImportContentType.FBXICT_SKINNING_WEIGHTS + task.options.anim_sequence_import_data.set_editor_property( + 'animation_length', + unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME ) + task.options.anim_sequence_import_data.set_editor_property( + 'import_meshes_in_bone_hierarchy', False) + task.options.anim_sequence_import_data.set_editor_property( + 'use_default_sample_rate', True) + task.options.anim_sequence_import_data.set_editor_property( + 'import_custom_attribute', True) + task.options.anim_sequence_import_data.set_editor_property( + 'import_bone_tracks', True) + task.options.anim_sequence_import_data.set_editor_property( + 'remove_redundant_keys', True) + task.options.anim_sequence_import_data.set_editor_property( + 'convert_scene', True) unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) From 963127e08e45698e03a7f79a720c4f4e44c0d56c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 14 May 2021 18:04:07 +0100 Subject: [PATCH 04/68] Hound fix --- .../hosts/blender/plugins/publish/extract_fbx_animation.py | 7 +------ openpype/hosts/unreal/plugins/load/load_animation.py | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py index 418dd100b2..8312114c7b 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -135,12 +135,7 @@ class ExtractAnimationFBX(openpype.api.Extractor): container = bpy.data.collections[container_name] if container: json_dict = { - # "representation": container.get("avalon").get( - # "representation" - # ), - "instance_name": container.get("avalon").get( - "instance_name" - ) + "instance_name": container.get("avalon").get("instance_name") } with open(json_path, "w+") as file: diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index a53328847d..481285d603 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -98,7 +98,7 @@ class AnimationFBXLoader(api.Loader): task.options.set_editor_property('override_full_name', True) task.options.anim_sequence_import_data.set_editor_property( - 'animation_length', + 'animation_length', unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME ) task.options.anim_sequence_import_data.set_editor_property( From 424e9950e719a4f4a965bc9c8da4fd3bb4be9540 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 10:40:38 +0200 Subject: [PATCH 05/68] blender implementation has copy of blender `load_scripts` function --- openpype/hosts/blender/api/lib.py | 93 +++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 openpype/hosts/blender/api/lib.py diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py new file mode 100644 index 0000000000..38a745f6ca --- /dev/null +++ b/openpype/hosts/blender/api/lib.py @@ -0,0 +1,93 @@ +import os +import traceback +import importlib + +import bpy + + +def load_scripts(paths): + """Copy of `load_scripts` from Blender's implementation. + + It is possible that whis function will be changed in future and usage will + be based on Blender version. + """ + import bpy_types + + loaded_modules = set() + + previous_classes = [ + cls + for cls in bpy.types.bpy_struct.__subclasses__() + ] + + def register_module_call(mod): + register = getattr(mod, "register", None) + if register: + try: + register() + except: + traceback.print_exc() + else: + print("\nWarning! '%s' has no register function, " + "this is now a requirement for registerable scripts" % + mod.__file__) + + def unregister_module_call(mod): + unregister = getattr(mod, "unregister", None) + if unregister: + try: + unregister() + except: + traceback.print_exc() + + def test_reload(mod): + # reloading this causes internal errors + # because the classes from this module are stored internally + # possibly to refresh internal references too but for now, best not to. + if mod == bpy_types: + return mod + + try: + return importlib.reload(mod) + except: + traceback.print_exc() + + def test_register(mod): + if mod: + register_module_call(mod) + bpy.utils._global_loaded_modules.append(mod.__name__) + + from bpy_restrict_state import RestrictBlend + + with RestrictBlend(): + for base_path in paths: + for path_subdir in bpy.utils._script_module_dirs: + path = os.path.join(base_path, path_subdir) + if not os.path.isdir(path): + continue + + bpy.utils._sys_path_ensure_prepend(path) + + # Only add to 'sys.modules' unless this is 'startup'. + if path_subdir != "startup": + continue + for mod in bpy.utils.modules_from_path(path, loaded_modules): + test_register(mod) + + # load template (if set) + if any(bpy.utils.app_template_paths()): + import bl_app_template_utils + bl_app_template_utils.reset(reload_scripts=False) + del bl_app_template_utils + + for cls in bpy.types.bpy_struct.__subclasses__(): + if cls in previous_classes: + continue + if not getattr(cls, "is_registered", False): + continue + for subcls in cls.__subclasses__(): + if not subcls.is_registered: + print( + "Warning, unregistered class: %s(%s)" % + (subcls.__name__, cls.__name__) + ) From 5110374a1b0dcc40e1fb0f554300bb287c5fe926 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 10:40:58 +0200 Subject: [PATCH 06/68] implemented `append_user_scripts` using `OPENPYPE_BLENDER_USER_SCRIPTS` env to load user scripts --- openpype/hosts/blender/api/lib.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index 38a745f6ca..6aa1cb46ac 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -91,3 +91,15 @@ def load_scripts(paths): "Warning, unregistered class: %s(%s)" % (subcls.__name__, cls.__name__) ) + + +def append_user_scripts(): + user_scripts = os.environ.get("OPENPYPE_BLENDER_USER_SCRIPTS") + if not user_scripts: + return + + try: + load_scripts(user_scripts.split(os.pathsep)) + except Exception: + print("Couldn't load user scripts \"{}\"".format(user_scripts)) + traceback.print_exc() From 83590556c5b8adfb07303e9972a490d5fbdb9b3e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 10:41:10 +0200 Subject: [PATCH 07/68] run append_user_scripts on blender install --- openpype/hosts/blender/api/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/__init__.py b/openpype/hosts/blender/api/__init__.py index 66102a2ae1..ecf4fdf4da 100644 --- a/openpype/hosts/blender/api/__init__.py +++ b/openpype/hosts/blender/api/__init__.py @@ -4,6 +4,8 @@ import traceback import bpy +from .lib import append_user_scripts + from avalon import api as avalon from pyblish import api as pyblish @@ -29,7 +31,7 @@ def install(): pyblish.register_plugin_path(str(PUBLISH_PATH)) avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH)) avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH)) - + append_user_scripts() avalon.on("new", on_new) avalon.on("open", on_open) From 8df855aa115559e3d9542c5b667cc11a52baae90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 10:54:12 +0200 Subject: [PATCH 08/68] modified blender's init to expect somebody will set OPENPYPE_BLENDER_USER_SCRIPTS --- openpype/hosts/blender/__init__.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/blender/__init__.py b/openpype/hosts/blender/__init__.py index 4d93233449..747394aad0 100644 --- a/openpype/hosts/blender/__init__.py +++ b/openpype/hosts/blender/__init__.py @@ -23,18 +23,32 @@ def add_implementation_envs(env, _app): env["PYTHONPATH"] = os.pathsep.join(python_path_parts) # Modify Blender user scripts path + previous_user_scripts = set() + # Implementation path is added to set for easier paths check inside loops + # - will be removed at the end + previous_user_scripts.add(implementation_user_script_path) + + openpype_blender_user_scripts = ( + env.get("OPENPYPE_BLENDER_USER_SCRIPTS") or "" + ) + for path in openpype_blender_user_scripts.split(os.pathsep): + if path and os.path.exists(path): + previous_user_scripts.add(os.path.normpath(path)) + blender_user_scripts = env.get("BLENDER_USER_SCRIPTS") or "" - previous_user_scripts = [] for path in blender_user_scripts.split(os.pathsep): if path and os.path.exists(path): - path = os.path.normpath(path) - if path != implementation_user_script_path: - previous_user_scripts.append(path) + previous_user_scripts.add(os.path.normpath(path)) + # Remove implementation path from user script paths as is set to + # `BLENDER_USER_SCRIPTS` + previous_user_scripts.remove(implementation_user_script_path) + env["BLENDER_USER_SCRIPTS"] = implementation_user_script_path + + # Set custom user scripts env env["OPENPYPE_BLENDER_USER_SCRIPTS"] = os.pathsep.join( previous_user_scripts ) - env["BLENDER_USER_SCRIPTS"] = implementation_user_script_path # Define Qt binding if not defined if not env.get("QT_PREFERRED_BINDING"): From 996b93abce1d1990b23008d2e328f0f64a3cd9fe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 11:15:09 +0200 Subject: [PATCH 09/68] removed invalid is_file values from schemas --- .../schemas/projects_schema/schema_project_deadline.json | 1 - .../schemas/projects_schema/schema_project_ftrack.json | 1 - .../projects_schema/schema_project_standalonepublisher.json | 2 -- .../schemas/projects_schema/schema_project_syncserver.json | 2 -- .../schemas/projects_schema/schema_project_tvpaint.json | 1 - .../entities/schemas/system_schema/schema_modules.json | 4 +--- 6 files changed, 1 insertion(+), 10 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 1346fb3dad..d47a6917da 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -10,7 +10,6 @@ "collapsible": true, "key": "publish", "label": "Publish plugins", - "is_file": true, "children": [ { "type": "dict", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index b1bb207578..aae2bb2539 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -603,7 +603,6 @@ "collapsible": true, "key": "publish", "label": "Publish plugins", - "is_file": true, "children": [ { "type": "dict", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json index 47eea3441c..28755ad268 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json @@ -11,7 +11,6 @@ "key": "create", "label": "Creator plugins", "collapsible_key": true, - "is_file": true, "object_type": { "type": "dict", "children": [ @@ -56,7 +55,6 @@ "collapsible": true, "key": "publish", "label": "Publish plugins", - "is_file": true, "children": [ { "type": "dict", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json b/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json index 9428ce2db0..cb2cc9c9d1 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_syncserver.json @@ -4,7 +4,6 @@ "label": "Site Sync (beta testing)", "collapsible": true, "checkbox_key": "enabled", - "is_file": true, "children": [ { "type": "boolean", @@ -44,7 +43,6 @@ "key": "sites", "label": "Sites", "collapsible_key": false, - "is_file": true, "object_type": { "type": "dict", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index ab404f03ff..2f69ea8864 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -10,7 +10,6 @@ "collapsible": true, "key": "publish", "label": "Publish plugins", - "is_file": true, "children": [ { "type": "schema_template", diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index d1b498bb86..b643293c87 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -97,7 +97,6 @@ "key": "sites", "label": "Sites", "collapsible_key": false, - "is_file": true, "object_type": { "type": "dict", @@ -156,8 +155,7 @@ }, "is_group": true, "key": "templates_mapping", - "label": "Templates mapping", - "is_file": true + "label": "Templates mapping" } ] }, From 190e778e061ad49ed20853c4a3ecbcf2f3471a77 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 11:15:39 +0200 Subject: [PATCH 10/68] added validation of is_file attribute --- openpype/settings/entities/base_entity.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 3e73fa8aa6..b2d0f8224d 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -846,6 +846,13 @@ class ItemEntity(BaseItemEntity): ) raise EntitySchemaError(self, reason) + if self.is_file and self.file_item is not None: + reason = ( + "Entity has set `is_file` to true but" + " it's parent is already marked as file item." + ) + raise EntitySchemaError(self, reason) + super(ItemEntity, self).schema_validations() def create_schema_object(self, *args, **kwargs): From c28a2acfdf50b4b39662c6eeb806da0cba21cd21 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 11:28:30 +0200 Subject: [PATCH 11/68] added settings for tvpaint LoadImage and ImportImage plugins --- .../schema_project_tvpaint.json | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index ab404f03ff..903c5de842 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -47,6 +47,84 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "load", + "label": "Loader plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "LoadImage", + "label": "Load Image", + "children": [ + { + "key": "defaults", + "type": "dict", + "children": [ + { + "type": "boolean", + "key": "stretch", + "label": "Stretch" + }, + { + "type": "boolean", + "key": "timestretch", + "label": "TimeStretch" + }, + { + "type": "boolean", + "key": "preload", + "label": "Preload" + } + ] + }, + { + "type": "list", + "key": "families", + "label": "Families", + "object_type": "text" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ImportImage", + "label": "Import Image", + "children": [ + { + "key": "defaults", + "type": "dict", + "children": [ + { + "type": "boolean", + "key": "stretch", + "label": "Stretch" + }, + { + "type": "boolean", + "key": "timestretch", + "label": "TimeStretch" + }, + { + "type": "boolean", + "key": "preload", + "label": "Preload" + } + ] + }, + { + "type": "list", + "key": "families", + "label": "Families", + "object_type": "text" + } + ] + } + ] + }, { "type": "schema", "name": "schema_publish_gui_filter" From 99c4f11fa6375e8df910bc0654060e15a2c15085 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 11:28:42 +0200 Subject: [PATCH 12/68] saved defaults for the settings --- .../defaults/project_settings/tvpaint.json | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 4a424b1c03..d7fc46763c 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -16,5 +16,33 @@ "active": true } }, + "load": { + "LoadImage": { + "defaults": { + "stretch": true, + "timestretch": true, + "preload": true + }, + "families": [ + "render", + "image", + "background", + "plate" + ] + }, + "ImportImage": { + "defaults": { + "stretch": true, + "timestretch": true, + "preload": true + }, + "families": [ + "render", + "image", + "background", + "plate" + ] + } + }, "filters": {} } \ No newline at end of file From 8fab6f8e7015a679c36bcaf2c5999f9b4d69d22a Mon Sep 17 00:00:00 2001 From: Petr Dvorak Date: Wed, 19 May 2021 12:26:36 +0200 Subject: [PATCH 13/68] manual for users --- website/docs/artist_install.md | 80 +++++++++++++++++++++++++ website/docs/assets/artist_systray.png | Bin 0 -> 11397 bytes website/docs/assets/install_01.png | Bin 0 -> 12023 bytes website/docs/assets/install_02.png | Bin 0 -> 9290 bytes website/docs/assets/install_03.png | Bin 0 -> 26277 bytes website/docs/assets/install_04.png | Bin 0 -> 31451 bytes website/docs/assets/install_05.png | Bin 0 -> 8057 bytes website/sidebars.js | 1 + 8 files changed, 81 insertions(+) create mode 100644 website/docs/artist_install.md create mode 100644 website/docs/assets/artist_systray.png create mode 100644 website/docs/assets/install_01.png create mode 100644 website/docs/assets/install_02.png create mode 100644 website/docs/assets/install_03.png create mode 100644 website/docs/assets/install_04.png create mode 100644 website/docs/assets/install_05.png diff --git a/website/docs/artist_install.md b/website/docs/artist_install.md new file mode 100644 index 0000000000..94a8bcdfe1 --- /dev/null +++ b/website/docs/artist_install.md @@ -0,0 +1,80 @@ +--- +id: artist_install +title: Installation +sidebar_label: Installation +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + +## Installation + +OpenPype comes in packages for Windows (10 or Server), Mac OS X (Mojave or higher), and Linux distribution (Centos, Ubuntu), and you can install them on your machine the same way as you are used to. + +:::important +To install OpenPype you will need administrator permissions. +::: + + + + + +For installation on Windows, download and run the executable file `OpenPype-3.0.0.exe`. +During the installation process, you can change the destination location path of the application, + +![Windows installation](assets/install_01.png) + +and create an icon on the desktop. + +![Windows create icon](assets/install_02.png) + + + + + + +For installation on your Linux distribution, download and unzip `OpenPype-3.0.0.zip`. A new folder `OpenPype-3.0.0` will be created. +Inside this folder find and run `openpype_gui`, + +![Linux launch](assets/install_03.png) + + + + + + +For installation on Mac OS X, download and run dmg image file `OpenPype-3.0.0.dmg`. + +Drag the OpenPype icon into the Application folder. + +![Mac installation](assets/install_04.png) + +After the installation, you can find OpenPype among the other Applications. + + + + +## Run OpenPype + +To run OpenPype click on the icon or find executable file (e.g. `C:\Program Files (x86)\OpenPype\openpype_gui.exe`) in the application location. +On the very first run of OpenPype the user will be asked for OpenPype Mongo URL. +This piece of information will be provided by the administrator or project manager who set up the studio. + +![Mongo example](assets/install_05.png) + +Once the Mongo URL address is entered, press `Start`, and OpenPype will be initiated. +OpenPype will also remember the connection for the next launch, so it is a one-time process. + +:::note +If the launch was successful, the artist should see a turquoise OpenPype logo in their +tray menu. Keep in mind that on Windows this icon might be hidden by default, in which case, the artist can simply drag the icon down to the tray. + +![Systray](assets/artist_systray.png) +::: \ No newline at end of file diff --git a/website/docs/assets/artist_systray.png b/website/docs/assets/artist_systray.png new file mode 100644 index 0000000000000000000000000000000000000000..6a0dd23375d0eb8223229a5547476e89d544ba75 GIT binary patch literal 11397 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/install_01.png b/website/docs/assets/install_01.png new file mode 100644 index 0000000000000000000000000000000000000000..c6b55826a3c3f2b427f42a2b5f7a0338b073002d GIT binary patch literal 12023 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

nhD7*CDKvXRlD~BAWDpSh3 zMN!W%`;u?oFv`6mDE=dc(bZ5uOZs5BOHh~<#U{a6b7)OFQ}G;Y>!M&$d8@Mba9Pd6 zOTIS!9+?CazNlm4s1{-a$2lF}ADDMwj0Pq>c6$UX&w2Yb_K_8_gw?VX+irD6{RRle zj>LQ<`oe>QW09*c2u7H6zJA;)jJnI>X3(Y==G=I7Eq#Es=3d&@!_S3h%w3;vrnsp7 z&TFFtys{)d5n0+Vk0&$MLv5Cxb>9eWW?0sASTU`ydYc$%qB5nuvK=?x7Nz5~0(vxM z;e;B_$)4`lP086;zJ5{m72frFBx+4%LB)w5ied{nQXHfu9 z;)mYCjvZVlMU*zNdm5bKg=2HGGz;QuAOZZ2PpF4CHn}9*-Bhh!wAu0I@D)7We4Lkb zAPasMawI|qX4G3}wQnXZMk3=UWR_KH_9k2F7u&l=7Pr|YjccPa>X73VqUi?eshVx) zDp!uWhV_LV)dh`}M?^MIZ&EKtOyt7(LQ3z9f2`BtKT@N90ljLX*v_&}jj-M`Dnc>u z*;U9}3`;+Hxri5bRau&`t;M&4-Kggvxv5BHeZ6EQ1})VMu}a2p={6;3Rs9dB5 z8i+9OWpoRVXT`uUzthV?-T24Cet6FgMvjQtW01xXMh}I_w+K^%#CcGAN<)_{mhmY4 zFq!fM)O}v&5BI~Jb{11E^~Po(weis4=BCX$0q@-{ZY&^FiF3pi(ItgDnqhD?FxxU1}kHrneyzx4C3y8WF`Y~alAS9 zxg)<%upU#=kEcJO&Xy2;T4po&QO^cW_rJ$Gp03Db9NS(Nu@PLxX>iG_yBuLM{3(LE zIxexsz-do_0p?4Q-0p~5^0{RyD~65VOy%332;H;*e0*TTH?ySf+Ah@GSliE7JX*}P zeRuiBoxLHx7;%QI&I2Z!K59`*m)2zM)d01yF53d*Vz<@Qnhu6scNQ3e8aDKzf{+Q5 zj-XC2a?OD2<{3)uSe>2?!p8@r_9|ck++i}Tlg|jWQq&_A-X|7wYy0SA1?9tj*&ht} z7r&lZ)I0BpG_A>g73kX>!PzrUjg?o3JNLc(&0=$lRPEb|KddTtTc>)rwEUzwV+$|< zKo`_gigtJ{JXONIUXe0`_&+=>{de%)*A6Lda=WBFSGvYkS2G}}j=wjIpkF$xo~3;L z?-DNUYpn8fy!_xL^yg1r@DKl=2?G5*mp9O?t3*Dj*8lGO5#sY;1|0Wg_CP=4$Ff;> zav0k%CDqeE;hb@vnNp~QGw!?9i6xU6%JWsw7qLWTm(bqIHwt?Nee;qvJ#OD=_Jgmx1$A!C5+7=A# z5)vp&b)bd4lkfXZSDT5}BH~6NG@OBbZL^KX7d9iwG0OAMTi(*=oHe&l7}GgI5v}H4 zK;FAMVK*>lhqQ%&@BV|*9$G~`o?euHvg9{zWtT9h=g5w_3Jxzhp?KgCypvQ&K9)U3 zuplt&_KqZ^;KB#Jw2XsQy}L!a_~l$0KScB?h!xmEB8Z5LL#WIw-_FlwQE`_Xz)I&_ zWF@j_VHUqL)2;;i8Zou96G++*eU}1SZyfyA=`!P4V{Og8>pZa6z1@7CrEcek)7MkI z<29V)pQL^^uV&Sfg%fkse0{-kX1h*N6u=9_l>3T=D@a&u^`Ilm+XLx N=bV44JA327{{rn$3-$m2 literal 0 HcmV?d00001 diff --git a/website/docs/assets/install_02.png b/website/docs/assets/install_02.png new file mode 100644 index 0000000000000000000000000000000000000000..1e97fd8139106d799fcfa4a8d6a6c30dcb467fd1 GIT binary patch literal 9290 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 Q{|9jVsQ(X-ea`0m2gB+U6951J literal 0 HcmV?d00001 diff --git a/website/docs/assets/install_03.png b/website/docs/assets/install_03.png new file mode 100644 index 0000000000000000000000000000000000000000..1ddd0eea8b7c720100db84068d79b8b8ebd37df3 GIT binary patch literal 26277 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 DI@hsL literal 0 HcmV?d00001 diff --git a/website/docs/assets/install_04.png b/website/docs/assets/install_04.png new file mode 100644 index 0000000000000000000000000000000000000000..6c03c5abd44b02e5bde73ff817d4cc069ac307f0 GIT binary patch literal 31451 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 z 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/install_05.png b/website/docs/assets/install_05.png new file mode 100644 index 0000000000000000000000000000000000000000..a7119a49fa586ed9ceed4ddc4915f0a5e779d400 GIT binary patch literal 8057 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 Date: Wed, 19 May 2021 12:42:02 +0200 Subject: [PATCH 14/68] rename_task skip removed tasks --- openpype/tools/project_manager/project_manager/model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 6e20dd368f..90b4734a28 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1810,6 +1810,9 @@ class AssetItem(BaseItem): _item.setData(False, DUPLICATED_ROLE) def _rename_task(self, item): + if item.data(REMOVED_ROLE): + return + new_name = item.data(QtCore.Qt.EditRole, "name").lower() item_id = item.data(IDENTIFIER_ROLE) prev_name = self._task_name_by_item_id[item_id] From 6305a10656585d08cd972383c6448950ee2cb912 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 12:42:27 +0200 Subject: [PATCH 15/68] AssetItem has callback on task remove state change --- .../project_manager/project_manager/model.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 90b4734a28..5dae12901e 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1843,6 +1843,32 @@ class AssetItem(BaseItem): def on_task_name_change(self, task_item): self._rename_task(task_item) + def on_task_remove_state_change(self, task_item): + is_removed = task_item.data(REMOVED_ROLE) + item_id = task_item.data(IDENTIFIER_ROLE) + if is_removed: + name = self._task_name_by_item_id.pop(item_id) + self._task_items_by_name[name].remove(task_item) + + else: + name = task_item.data(QtCore.Qt.EditRole, "name").lower() + self._task_name_by_item_id[item_id] = name + self._task_items_by_name[name].append(task_item) + + # Remove from previous name mapping + if not self._task_items_by_name[name]: + self._task_items_by_name.pop(name) + + elif len(self._task_items_by_name[name]) == 1: + if name in self._duplicated_task_names: + self._duplicated_task_names.remove(name) + task_item.setData(False, DUPLICATED_ROLE) + + else: + self._duplicated_task_names.add(name) + for _item in self._task_items_by_name[name]: + _item.setData(True, DUPLICATED_ROLE) + def add_child(self, item, row=None): if item in self._children: return From 56acc5026e7ab8e6b18291cc935f7168ee0ec42c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 12:42:55 +0200 Subject: [PATCH 16/68] task is calling `on_task_remove_state_change` on it's parent when REMOVED_ROLE state has changed --- openpype/tools/project_manager/project_manager/model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 5dae12901e..18dd5bc46d 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -2004,7 +2004,10 @@ class TaskItem(BaseItem): return True if role == REMOVED_ROLE: + if value == self._removed: + return False self._removed = value + self.parent().on_task_remove_state_change(self) return True if ( From 0fc5a542c88b44220d65d49e1004cf0a4f20b4e5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 12:43:13 +0200 Subject: [PATCH 17/68] _remove task skip item ids that are not in _task_name_by_item_id --- openpype/tools/project_manager/project_manager/model.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index 18dd5bc46d..5000729adf 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -1797,7 +1797,11 @@ class AssetItem(BaseItem): item.setData(False, DUPLICATED_ROLE) def _remove_task(self, item): + # This method is probably obsolete with changed logic and added + # `on_task_remove_state_change` method. item_id = item.data(IDENTIFIER_ROLE) + if item_id not in self._task_name_by_item_id: + return name = self._task_name_by_item_id.pop(item_id) self._task_items_by_name[name].remove(item) From 86d5e1c068fa739d58ad2e366fd27ce989e508ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 19 May 2021 14:37:15 +0200 Subject: [PATCH 18/68] make ascii art compatible, docstrings update --- tools/build.ps1 | 30 ++++++++++++++++++----------- tools/build_win_installer.ps1 | 29 +++++++++++++++++----------- tools/create_env.ps1 | 27 +++++++++++++++----------- tools/create_zip.ps1 | 31 +++++++++++++++++------------- tools/make_docs.ps1 | 22 ++++++++++----------- tools/run_documentation.ps1 | 36 ++++++++++++++++++++++++----------- tools/run_mongo.ps1 | 22 ++++++++++----------- tools/run_project_manager.ps1 | 4 ++-- tools/run_tests.ps1 | 22 ++++++++++----------- 9 files changed, 131 insertions(+), 92 deletions(-) diff --git a/tools/build.ps1 b/tools/build.ps1 index 611d8af668..d9fef0f471 100644 --- a/tools/build.ps1 +++ b/tools/build.ps1 @@ -12,6 +12,14 @@ PS> .\build.ps1 +.EXAMPLE + +To build without automatical submodule update: +PS> .\build.ps1 --no-submodule-update + +.LINK +https://openpype.io/docs + #> $arguments=$ARGS @@ -82,17 +90,17 @@ $art = @" . . .. . .. _oOOP3OPP3Op_. . - .PPpo~· ·· ~2p. ·· ···· · · - ·Ppo · .pPO3Op.· · O:· · · · - .3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · · - ·~OP 3PO· .Op3 : · ·· _____ _____ _____ - ·P3O · oP3oP3O3P' · · · · / /·/ /·/ / - O3:· O3p~ · ·:· · ·/____/·/____/ /____/ - 'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · · - · ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · · - · '_ .. · . _OP3·· · ·https://openpype.io·· · - ~P3·OPPPO3OP~ · ·· · - · ' '· · ·· · · · ·· · + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ diff --git a/tools/build_win_installer.ps1 b/tools/build_win_installer.ps1 index 4a4d011258..e46cd6a84d 100644 --- a/tools/build_win_installer.ps1 +++ b/tools/build_win_installer.ps1 @@ -1,16 +1,15 @@ <# .SYNOPSIS - Helper script to build OpenPype. + Helper script to build OpenPype Installer. .DESCRIPTION - This script will detect Python installation, and build OpenPype to `build` - directory using existing virtual environment created by Poetry (or - by running `/tools/create_venv.ps1`). It will then shuffle dependencies in - build folder to optimize for different Python versions (2/3) in Python host. + This script will use already built OpenPype (in `build` directory) and + create Windows installer from it using Inno Setup (https://jrsoftware.org/) + #> .EXAMPLE -PS> .\build.ps1 +PS> .\build_win_installer.ps1 #> @@ -76,11 +75,19 @@ function Install-Poetry() { $art = @" -▒█▀▀▀█ █▀▀█ █▀▀ █▀▀▄ ▒█▀▀█ █░░█ █▀▀█ █▀▀ ▀█▀ ▀█▀ ▀█▀ -▒█░░▒█ █░░█ █▀▀ █░░█ ▒█▄▄█ █▄▄█ █░░█ █▀▀ ▒█░ ▒█░ ▒█░ -▒█▄▄▄█ █▀▀▀ ▀▀▀ ▀░░▀ ▒█░░░ ▄▄▄█ █▀▀▀ ▀▀▀ ▄█▄ ▄█▄ ▄█▄ - .---= [ by Pype Club ] =---. - https://openpype.io + . . .. . .. + _oOOP3OPP3Op_. . + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ diff --git a/tools/create_env.ps1 b/tools/create_env.ps1 index 5600ae71c7..7ada92c1e8 100644 --- a/tools/create_env.ps1 +++ b/tools/create_env.ps1 @@ -11,6 +11,11 @@ PS> .\create_env.ps1 +.EXAMPLE + +Print verbose information from Poetry: +PS> .\create_env.ps1 --verbose + #> $arguments=$ARGS @@ -98,17 +103,17 @@ $art = @" . . .. . .. _oOOP3OPP3Op_. . - .PPpo~· ·· ~2p. ·· ···· · · - ·Ppo · .pPO3Op.· · O:· · · · - .3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · · - ·~OP 3PO· .Op3 : · ·· _____ _____ _____ - ·P3O · oP3oP3O3P' · · · · / /·/ /·/ / - O3:· O3p~ · ·:· · ·/____/·/____/ /____/ - 'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · · - · ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · · - · '_ .. · . _OP3·· · ·https://openpype.io·· · - ~P3·OPPPO3OP~ · ·· · - · ' '· · ·· · · · ·· · + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ diff --git a/tools/create_zip.ps1 b/tools/create_zip.ps1 index 2fef4d216b..a34af89159 100644 --- a/tools/create_zip.ps1 +++ b/tools/create_zip.ps1 @@ -4,14 +4,19 @@ .DESCRIPTION This script will detect Python installation and run OpenPype to create - zip. It needs mongodb running. I will create zip from current source code - version and copy it top `%LOCALAPPDATA%/pypeclub/pype` if `--path` or `-p` + zip. It will create zip from current source code + version and copy it top `%LOCALAPPDATA%/pypeclub/openpype` if `--path` or `-p` argument is not used. .EXAMPLE PS> .\create_zip.ps1 +.EXAMPLE + +To put generated zip to C:\OpenPype directory: +PS> .\create_zip.ps1 --path C:\OpenPype + #> function Exit-WithCode($exitcode) { @@ -52,17 +57,17 @@ $art = @" . . .. . .. _oOOP3OPP3Op_. . - .PPpo~· ·· ~2p. ·· ···· · · - ·Ppo · .pPO3Op.· · O:· · · · - .3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · · - ·~OP 3PO· .Op3 : · ·· _____ _____ _____ - ·P3O · oP3oP3O3P' · · · · / /·/ /·/ / - O3:· O3p~ · ·:· · ·/____/·/____/ /____/ - 'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · · - · ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · · - · '_ .. · . _OP3·· · ·https://openpype.io·· · - ~P3·OPPPO3OP~ · ·· · - · ' '· · ·· · · · ·· · + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ diff --git a/tools/make_docs.ps1 b/tools/make_docs.ps1 index 01edaf9c58..2f9350eff0 100644 --- a/tools/make_docs.ps1 +++ b/tools/make_docs.ps1 @@ -32,17 +32,17 @@ $art = @" . . .. . .. _oOOP3OPP3Op_. . - .PPpo~· ·· ~2p. ·· ···· · · - ·Ppo · .pPO3Op.· · O:· · · · - .3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · · - ·~OP 3PO· .Op3 : · ·· _____ _____ _____ - ·P3O · oP3oP3O3P' · · · · / /·/ /·/ / - O3:· O3p~ · ·:· · ·/____/·/____/ /____/ - 'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · · - · ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · · - · '_ .. · . _OP3·· · ·https://openpype.io·· · - ~P3·OPPPO3OP~ · ·· · - · ' '· · ·· · · · ·· · + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ diff --git a/tools/run_documentation.ps1 b/tools/run_documentation.ps1 index 1be3709642..a3e3a9b8dd 100644 --- a/tools/run_documentation.ps1 +++ b/tools/run_documentation.ps1 @@ -1,23 +1,38 @@ <# .SYNOPSIS - Helper script to run mongodb. + Helper script to run Docusaurus for easy editing of OpenPype documentation. .DESCRIPTION - This script will detect mongodb, add it to the PATH and launch it on specified port and db location. + This script is using `yarn` package manager to run Docusaurus. If you don't + have `yarn`, install Node.js (https://nodejs.org/) and then run: + + npm install -g yarn + + It take some time to run this script. If all is successful you should see + new browser window with OpenPype documentation. All changes is markdown files + under .\website should be immediately seen in browser. .EXAMPLE -PS> .\run_mongo.ps1 +PS> .\run_documentation.ps1 #> $art = @" -▒█▀▀▀█ █▀▀█ █▀▀ █▀▀▄ ▒█▀▀█ █░░█ █▀▀█ █▀▀ ▀█▀ ▀█▀ ▀█▀ -▒█░░▒█ █░░█ █▀▀ █░░█ ▒█▄▄█ █▄▄█ █░░█ █▀▀ ▒█░ ▒█░ ▒█░ -▒█▄▄▄█ █▀▀▀ ▀▀▀ ▀░░▀ ▒█░░░ ▄▄▄█ █▀▀▀ ▀▀▀ ▄█▄ ▄█▄ ▄█▄ - .---= [ by Pype Club ] =---. - https://openpype.io + . . .. . .. + _oOOP3OPP3Op_. . + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ @@ -26,7 +41,6 @@ Write-Host $art -ForegroundColor DarkGreen $script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent $openpype_root = (Get-Item $script_dir).parent.FullName -cd $openpype_root/website - -yarn run start +Set-Location $openpype_root/website +& yarn run start diff --git a/tools/run_mongo.ps1 b/tools/run_mongo.ps1 index 05fc497d34..6719e520fe 100644 --- a/tools/run_mongo.ps1 +++ b/tools/run_mongo.ps1 @@ -15,17 +15,17 @@ $art = @" . . .. . .. _oOOP3OPP3Op_. . - .PPpo~· ·· ~2p. ·· ···· · · - ·Ppo · .pPO3Op.· · O:· · · · - .3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · · - ·~OP 3PO· .Op3 : · ·· _____ _____ _____ - ·P3O · oP3oP3O3P' · · · · / /·/ /·/ / - O3:· O3p~ · ·:· · ·/____/·/____/ /____/ - 'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · · - · ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · · - · '_ .. · . _OP3·· · ·https://openpype.io·· · - ~P3·OPPPO3OP~ · ·· · - · ' '· · ·· · · · ·· · + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ diff --git a/tools/run_project_manager.ps1 b/tools/run_project_manager.ps1 index 78dce19df1..67c2d2eb5e 100644 --- a/tools/run_project_manager.ps1 +++ b/tools/run_project_manager.ps1 @@ -1,13 +1,13 @@ <# .SYNOPSIS - Helper script OpenPype Tray. + Helper script to run Project Manager. .DESCRIPTION .EXAMPLE -PS> .\run_tray.ps1 +PS> .\run_project_manager.ps1 #> $current_dir = Get-Location diff --git a/tools/run_tests.ps1 b/tools/run_tests.ps1 index 7b9a5c841d..30e1f29e59 100644 --- a/tools/run_tests.ps1 +++ b/tools/run_tests.ps1 @@ -34,17 +34,17 @@ $art = @" . . .. . .. _oOOP3OPP3Op_. . - .PPpo~· ·· ~2p. ·· ···· · · - ·Ppo · .pPO3Op.· · O:· · · · - .3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · · - ·~OP 3PO· .Op3 : · ·· _____ _____ _____ - ·P3O · oP3oP3O3P' · · · · / /·/ /·/ / - O3:· O3p~ · ·:· · ·/____/·/____/ /____/ - 'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · · - · ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · · - · '_ .. · . _OP3·· · ·https://openpype.io·· · - ~P3·OPPPO3OP~ · ·· · - · ' '· · ·· · · · ·· · + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . "@ From 0ef14e7ff25264c797e3716c9abfbd7d72f8d379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 19 May 2021 14:39:27 +0200 Subject: [PATCH 19/68] fix premature docstring end --- tools/build_win_installer.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/build_win_installer.ps1 b/tools/build_win_installer.ps1 index e46cd6a84d..05ec0f9823 100644 --- a/tools/build_win_installer.ps1 +++ b/tools/build_win_installer.ps1 @@ -5,7 +5,6 @@ .DESCRIPTION This script will use already built OpenPype (in `build` directory) and create Windows installer from it using Inno Setup (https://jrsoftware.org/) - #> .EXAMPLE From e13fb230f2d711ab494e56ef65e6871f1fe3a4a7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 14:40:14 +0200 Subject: [PATCH 20/68] use AVALON_APP to get host name if is available --- openpype/plugins/publish/collect_anatomy_context_data.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 5b955a0592..5c4bc6cb75 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -71,6 +71,10 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): app = app_manager.applications.get(app_name) if app: context_data["app"] = app.host_name + # Use AVALON_APP as first if available it is the same as host name + # - only if is not defined use AVALON_APP_NAME (e.g. on Farm) and + # set it back to AVALON_APP env variable + host_name = os.environ.get("AVALON_APP") datetime_data = context.data.get("datetimeData") or {} context_data.update(datetime_data) From 85abe800392979be139a36a7da75c59acbafe3f4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 14:40:33 +0200 Subject: [PATCH 21/68] use AVALON_APP_NAME if AVALON_APP is not available --- .../publish/collect_anatomy_context_data.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 5c4bc6cb75..ce23aa82bf 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -65,16 +65,18 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): "username": context.data["user"] } - app_manager = ApplicationManager() - app_name = os.environ.get("AVALON_APP_NAME") - if app_name: - app = app_manager.applications.get(app_name) - if app: - context_data["app"] = app.host_name # Use AVALON_APP as first if available it is the same as host name # - only if is not defined use AVALON_APP_NAME (e.g. on Farm) and # set it back to AVALON_APP env variable host_name = os.environ.get("AVALON_APP") + if not host_name: + app_manager = ApplicationManager() + app_name = os.environ.get("AVALON_APP_NAME") + if app_name: + app = app_manager.applications.get(app_name) + if app: + host_name = app.host_name + context_data["app"] = host_name datetime_data = context.data.get("datetimeData") or {} context_data.update(datetime_data) From cebcfb4a1517d6ae73783cacd892dc2047c4a185 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 14:40:54 +0200 Subject: [PATCH 22/68] set host name back to AVALON_APP as it's used across other plugins --- openpype/plugins/publish/collect_anatomy_context_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index ce23aa82bf..f121760e27 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -76,6 +76,7 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): app = app_manager.applications.get(app_name) if app: host_name = app.host_name + os.environ["AVALON_APP"] = host_name context_data["app"] = host_name datetime_data = context.data.get("datetimeData") or {} From e9a38a5f53ff48912b9747f113dde56be643f91b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 17:23:13 +0200 Subject: [PATCH 23/68] created base of InvalidValueType (BaseInvalidValueType) to be able pass reason directly --- openpype/settings/entities/__init__.py | 2 ++ openpype/settings/entities/base_entity.py | 3 ++- openpype/settings/entities/exceptions.py | 16 +++++++++------- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index 2c71b622ee..5d83a7cde4 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -57,6 +57,7 @@ from .exceptions import ( SchemaError, DefaultsNotDefined, StudioDefaultsNotDefined, + BaseInvalidValueType, InvalidValueType, InvalidKeySymbols, SchemaMissingFileInfo, @@ -115,6 +116,7 @@ from .anatomy_entities import AnatomyEntity __all__ = ( "DefaultsNotDefined", "StudioDefaultsNotDefined", + "BaseInvalidValueType", "InvalidValueType", "InvalidKeySymbols", "SchemaMissingFileInfo", diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 3e73fa8aa6..76150950b5 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -9,6 +9,7 @@ from .lib import ( ) from .exceptions import ( + BaseInvalidValueType, InvalidValueType, SchemeGroupHierarchyBug, EntitySchemaError @@ -377,7 +378,7 @@ class BaseItemEntity(BaseEntity): try: new_value = self.convert_to_valid_type(value) - except InvalidValueType: + except BaseInvalidValueType: new_value = NOT_SET if new_value is not NOT_SET: diff --git a/openpype/settings/entities/exceptions.py b/openpype/settings/entities/exceptions.py index 3649e63ab7..f352c94f20 100644 --- a/openpype/settings/entities/exceptions.py +++ b/openpype/settings/entities/exceptions.py @@ -15,20 +15,22 @@ class StudioDefaultsNotDefined(Exception): super(StudioDefaultsNotDefined, self).__init__(msg) -class InvalidValueType(Exception): - msg_template = "{}" +class BaseInvalidValueType(Exception): + def __init__(self, reason, path): + msg = "Path \"{}\". {}".format(path, reason) + self.msg = msg + super(BaseInvalidValueType, self).__init__(msg) + +class InvalidValueType(BaseInvalidValueType): def __init__(self, valid_types, invalid_type, path): - msg = "Path \"{}\". ".format(path) - joined_types = ", ".join( [str(valid_type) for valid_type in valid_types] ) - msg += "Got invalid type \"{}\". Expected: {}".format( + msg = "Got invalid type \"{}\". Expected: {}".format( invalid_type, joined_types ) - self.msg = msg - super(InvalidValueType, self).__init__(msg) + super(InvalidValueType, self).__init__(msg, path) class RequiredKeyModified(KeyError): From b420ea6971c01410ec49b81cde5356cec9f18532 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 17:24:53 +0200 Subject: [PATCH 24/68] base implementation of ColorEntity --- openpype/settings/entities/__init__.py | 4 +- openpype/settings/entities/color_entity.py | 53 ++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 openpype/settings/entities/color_entity.py diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index 5d83a7cde4..33881a6097 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -97,7 +97,7 @@ from .input_entities import ( PathInput, RawJsonEntity ) - +from .color_entity import ColorEntity from .enum_entity import ( BaseEnumEntity, EnumEntity, @@ -148,6 +148,8 @@ __all__ = ( "PathInput", "RawJsonEntity", + "ColorEntity", + "BaseEnumEntity", "EnumEntity", "AppsEnumEntity", diff --git a/openpype/settings/entities/color_entity.py b/openpype/settings/entities/color_entity.py new file mode 100644 index 0000000000..7d31ba42b9 --- /dev/null +++ b/openpype/settings/entities/color_entity.py @@ -0,0 +1,53 @@ +from .lib import STRING_TYPE +from .input_entities import InputEntity +from .exceptions import ( + BaseInvalidValueType, + InvalidValueType +) + + +class ColorEntity(InputEntity): + schema_types = ["color"] + def _item_initalization(self): + self.valid_value_types = (list, ) + self.value_on_not_set = [0, 0, 0, 255] + + def convert_to_valid_type(self, value): + """Conversion to valid type. + + Complexity of entity requires to override BaseEntity implementation. + """ + # Convertion to valid value type `list` + if isinstance(value, (set, tuple)): + value = list(value) + + # Skip other validations if is not `list` + if not isinstance(value, list): + raise InvalidValueType( + self.valid_value_types, type(value), self.path + ) + + # Allow list of len 3 (last aplha is set to max) + if len(value) == 3: + value.append(255) + + if len(value) != 4: + reason = "Color entity expect 4 items in list got {}".format( + len(value) + ) + raise BaseInvalidValueType(reason, self.path) + + new_value = [] + for item in value: + if not isinstance(item, int): + if isinstance(item, (STRING_TYPE, float)): + item = int(item) + + is_valid = isinstance(item, int) and -1 < item < 256 + if not is_valid: + reason = ( + "Color entity expect 4 integers in range 0-255 got {}" + ).format(value) + raise BaseInvalidValueType(reason, self.path) + new_value.append(item) + return new_value From d3673ae627b4e115cd6574183b33f921e2b84690 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 17:25:54 +0200 Subject: [PATCH 25/68] copy pasted PyQtColorTriangle project --- openpype/widgets/color_widgets/__init__.py | 6 + .../widgets/color_widgets/color_inputs.py | 514 ++++++ .../color_widgets/color_picker_widget.py | 115 ++ .../color_widgets/color_screen_pick.py | 248 +++ .../widgets/color_widgets/color_triangle.py | 1431 +++++++++++++++++ openpype/widgets/color_widgets/color_view.py | 78 + 6 files changed, 2392 insertions(+) create mode 100644 openpype/widgets/color_widgets/__init__.py create mode 100644 openpype/widgets/color_widgets/color_inputs.py create mode 100644 openpype/widgets/color_widgets/color_picker_widget.py create mode 100644 openpype/widgets/color_widgets/color_screen_pick.py create mode 100644 openpype/widgets/color_widgets/color_triangle.py create mode 100644 openpype/widgets/color_widgets/color_view.py diff --git a/openpype/widgets/color_widgets/__init__.py b/openpype/widgets/color_widgets/__init__.py new file mode 100644 index 0000000000..3423e26cf8 --- /dev/null +++ b/openpype/widgets/color_widgets/__init__.py @@ -0,0 +1,6 @@ +from .color_picker_widget import ColorPickerWidget + + +__all__ = ( + "ColorPickerWidget", +) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py new file mode 100644 index 0000000000..ddf8aebd4e --- /dev/null +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -0,0 +1,514 @@ +import re +from Qt import QtWidgets, QtCore, QtGui + + +slide_style = """ +QSlider::groove:horizontal { + background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #000, stop: 1 #fff); + height: 8px; + border-radius: 4px; +} + +QSlider::handle:horizontal { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #ddd, stop:1 #bbb); + border: 1px solid #777; + width: 8px; + margin-top: -1px; + margin-bottom: -1px; + border-radius: 4px; +} + +QSlider::handle:horizontal:hover { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eee, stop:1 #ddd); + border: 1px solid #444;ff + border-radius: 4px; +}""" + + +class AlphaInputs(QtWidgets.QGroupBox): + alpha_changed = QtCore.Signal(int) + + def __init__(self, parent=None): + super(AlphaInputs, self).__init__("Alpha", parent) + + self._block_changes = False + self.alpha_value = None + + # Opacity slider + alpha_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal, self) + alpha_slider.setSingleStep(1) + alpha_slider.setMinimum(0) + alpha_slider.setMaximum(255) + alpha_slider.setStyleSheet(slide_style) + alpha_slider.setValue(255) + + inputs_widget = QtWidgets.QWidget(self) + inputs_layout = QtWidgets.QHBoxLayout(inputs_widget) + inputs_layout.setContentsMargins(0, 0, 0, 0) + + percent_input = QtWidgets.QDoubleSpinBox(self) + percent_input.setMinimum(0) + percent_input.setMaximum(100) + percent_input.setDecimals(2) + + int_input = QtWidgets.QSpinBox(self) + int_input.setMinimum(0) + int_input.setMaximum(255) + + inputs_layout.addWidget(int_input) + inputs_layout.addWidget(QtWidgets.QLabel("0-255")) + inputs_layout.addWidget(percent_input) + inputs_layout.addWidget(QtWidgets.QLabel("%")) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(alpha_slider) + layout.addWidget(inputs_widget) + + alpha_slider.valueChanged.connect(self._on_slider_change) + percent_input.valueChanged.connect(self._on_percent_change) + int_input.valueChanged.connect(self._on_int_change) + + self.alpha_slider = alpha_slider + self.percent_input = percent_input + self.int_input = int_input + + self.set_alpha(255) + + def set_alpha(self, alpha): + if alpha == self.alpha_value: + return + self.alpha_value = alpha + + self.update_alpha() + + def _on_slider_change(self): + if self._block_changes: + return + self.alpha_value = self.alpha_slider.value() + self.alpha_changed.emit(self.alpha_value) + self.update_alpha() + + def _on_percent_change(self): + if self._block_changes: + return + self.alpha_value = int(self.percent_input.value() * 255 / 100) + self.alpha_changed.emit(self.alpha_value) + self.update_alpha() + + def _on_int_change(self): + if self._block_changes: + return + + self.alpha_value = self.int_input.value() + self.alpha_changed.emit(self.alpha_value) + self.update_alpha() + + def update_alpha(self): + self._block_changes = True + + if self.alpha_slider.value() != self.alpha_value: + self.alpha_slider.setValue(self.alpha_value) + + if self.int_input.value() != self.alpha_value: + self.int_input.setValue(self.alpha_value) + + percent = round(100 * self.alpha_value / 255, 2) + if self.percent_input.value() != percent: + self.percent_input.setValue(percent) + + self._block_changes = False + + +class RGBInputs(QtWidgets.QGroupBox): + value_changed = QtCore.Signal() + + def __init__(self, color, parent=None): + super(RGBInputs, self).__init__("RGB", parent) + + self._block_changes = False + + self.color = color + + input_red = QtWidgets.QSpinBox(self) + input_green = QtWidgets.QSpinBox(self) + input_blue = QtWidgets.QSpinBox(self) + + input_red.setMinimum(0) + input_green.setMinimum(0) + input_blue.setMinimum(0) + + input_red.setMaximum(255) + input_green.setMaximum(255) + input_blue.setMaximum(255) + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(input_red) + layout.addWidget(input_green) + layout.addWidget(input_blue) + + input_red.valueChanged.connect(self._on_red_change) + input_green.valueChanged.connect(self._on_green_change) + input_blue.valueChanged.connect(self._on_blue_change) + + self.input_red = input_red + self.input_green = input_green + self.input_blue = input_blue + + def _on_red_change(self, value): + if self._block_changes: + return + self.color.setRed(value) + self._on_change() + + def _on_green_change(self, value): + if self._block_changes: + return + self.color.setGreen(value) + self._on_change() + + def _on_blue_change(self, value): + if self._block_changes: + return + self.color.setBlue(value) + self._on_change() + + def _on_change(self): + self.value_changed.emit() + + def color_changed(self): + if ( + self.input_red.value() == self.color.red() + and self.input_green.value() == self.color.green() + and self.input_blue.value() == self.color.blue() + ): + return + + self._block_changes = True + + self.input_red.setValue(self.color.red()) + self.input_green.setValue(self.color.green()) + self.input_blue.setValue(self.color.blue()) + + self._block_changes = False + + +class CMYKInputs(QtWidgets.QGroupBox): + value_changed = QtCore.Signal() + + def __init__(self, color, parent=None): + super(CMYKInputs, self).__init__("CMYK", parent) + + self.color = color + + self._block_changes = False + + input_cyan = QtWidgets.QSpinBox(self) + input_magenta = QtWidgets.QSpinBox(self) + input_yellow = QtWidgets.QSpinBox(self) + input_black = QtWidgets.QSpinBox(self) + + input_cyan.setMinimum(0) + input_magenta.setMinimum(0) + input_yellow.setMinimum(0) + input_black.setMinimum(0) + + input_cyan.setMaximum(255) + input_magenta.setMaximum(255) + input_yellow.setMaximum(255) + input_black.setMaximum(255) + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(input_cyan) + layout.addWidget(input_magenta) + layout.addWidget(input_yellow) + layout.addWidget(input_black) + + input_cyan.valueChanged.connect(self._on_change) + input_magenta.valueChanged.connect(self._on_change) + input_yellow.valueChanged.connect(self._on_change) + input_black.valueChanged.connect(self._on_change) + + self.input_cyan = input_cyan + self.input_magenta = input_magenta + self.input_yellow = input_yellow + self.input_black = input_black + + def _on_change(self): + if self._block_changes: + return + self.color.setCmyk( + self.input_cyan.value(), + self.input_magenta.value(), + self.input_yellow.value(), + self.input_black.value() + ) + self.value_changed.emit() + + def color_changed(self): + if self._block_changes: + return + _cur_color = QtGui.QColor() + _cur_color.setCmyk( + self.input_cyan.value(), + self.input_magenta.value(), + self.input_yellow.value(), + self.input_black.value() + ) + if ( + _cur_color.red() == self.color.red() + and _cur_color.green() == self.color.green() + and _cur_color.blue() == self.color.blue() + ): + return + + c, m, y, k, _ = self.color.getCmyk() + self._block_changes = True + + self.input_cyan.setValue(c) + self.input_magenta.setValue(m) + self.input_yellow.setValue(y) + self.input_black.setValue(k) + + self._block_changes = False + + +class HEXInputs(QtWidgets.QGroupBox): + hex_regex = re.compile("^#(([0-9a-fA-F]{2}){3}|([0-9a-fA-F]){3})$") + value_changed = QtCore.Signal() + + def __init__(self, color, parent=None): + super(HEXInputs, self).__init__("HEX", parent) + self.color = color + + input_field = QtWidgets.QLineEdit() + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(input_field) + + input_field.textChanged.connect(self._on_change) + + self.input_field = input_field + + def _on_change(self): + if self._block_changes: + return + input_value = self.input_field.text() + # TODO what if does not match? + if self.hex_regex.match(input_value): + self.color.setNamedColor(input_value) + self.value_changed.emit() + + def color_changed(self): + input_value = self.input_field.text() + if self.hex_regex.match(input_value): + _cur_color = QtGui.QColor() + _cur_color.setNamedColor(input_value) + if ( + _cur_color.red() == self.color.red() + and _cur_color.green() == self.color.green() + and _cur_color.blue() == self.color.blue() + ): + return + self._block_changes = True + + self.input_field.setText(self.color.name()) + + self._block_changes = False + + +class HSVInputs(QtWidgets.QGroupBox): + value_changed = QtCore.Signal() + + def __init__(self, color, parent=None): + super(HSVInputs, self).__init__("HSV", parent) + + self._block_changes = False + + self.color = color + + input_hue = QtWidgets.QSpinBox(self) + input_sat = QtWidgets.QSpinBox(self) + input_val = QtWidgets.QSpinBox(self) + + input_hue.setMinimum(0) + input_sat.setMinimum(0) + input_val.setMinimum(0) + + input_hue.setMaximum(359) + input_sat.setMaximum(255) + input_val.setMaximum(255) + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(input_hue) + layout.addWidget(input_sat) + layout.addWidget(input_val) + + input_hue.valueChanged.connect(self._on_change) + input_sat.valueChanged.connect(self._on_change) + input_val.valueChanged.connect(self._on_change) + + self.input_hue = input_hue + self.input_sat = input_sat + self.input_val = input_val + + def _on_change(self): + if self._block_changes: + return + self.color.setHsv( + self.input_hue.value(), + self.input_sat.value(), + self.input_val.value() + ) + self.value_changed.emit() + + def color_changed(self): + _cur_color = QtGui.QColor() + _cur_color.setHsv( + self.input_hue.value(), + self.input_sat.value(), + self.input_val.value() + ) + if ( + _cur_color.red() == self.color.red() + and _cur_color.green() == self.color.green() + and _cur_color.blue() == self.color.blue() + ): + return + + self._block_changes = True + h, s, v, _ = self.color.getHsv() + + self.input_hue.setValue(h) + self.input_sat.setValue(s) + self.input_val.setValue(v) + + self._block_changes = False + + +class HSLInputs(QtWidgets.QGroupBox): + value_changed = QtCore.Signal() + + def __init__(self, color, parent=None): + super(HSLInputs, self).__init__("HSL", parent) + + self._block_changes = False + + self.color = color + + input_hue = QtWidgets.QSpinBox(self) + input_sat = QtWidgets.QSpinBox(self) + input_light = QtWidgets.QSpinBox(self) + + input_hue.setMinimum(0) + input_sat.setMinimum(0) + input_light.setMinimum(0) + + input_hue.setMaximum(359) + input_sat.setMaximum(255) + input_light.setMaximum(255) + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(input_hue) + layout.addWidget(input_sat) + layout.addWidget(input_light) + + input_hue.valueChanged.connect(self._on_change) + input_sat.valueChanged.connect(self._on_change) + input_light.valueChanged.connect(self._on_change) + + self.input_hue = input_hue + self.input_sat = input_sat + self.input_light = input_light + + def _on_change(self): + if self._block_changes: + return + self.color.setHsl( + self.input_hue.value(), + self.input_sat.value(), + self.input_light.value() + ) + self.value_changed.emit() + + def color_changed(self): + _cur_color = QtGui.QColor() + _cur_color.setHsl( + self.input_hue.value(), + self.input_sat.value(), + self.input_light.value() + ) + if ( + _cur_color.red() == self.color.red() + and _cur_color.green() == self.color.green() + and _cur_color.blue() == self.color.blue() + ): + return + + self._block_changes = True + h, s, l, _ = self.color.getHsl() + + self.input_hue.setValue(h) + self.input_sat.setValue(s) + self.input_light.setValue(l) + + self._block_changes = False + + +class ColorInputsWidget(QtWidgets.QWidget): + color_changed = QtCore.Signal(QtGui.QColor) + + def __init__(self, parent=None, **kwargs): + super(ColorInputsWidget, self).__init__(parent) + + color = QtGui.QColor() + + input_fields = [] + + if kwargs.get("use_hex", True): + input_fields.append(HEXInputs(color, self)) + + if kwargs.get("use_rgb", True): + input_fields.append(RGBInputs(color, self)) + + if kwargs.get("use_hsl", True): + input_fields.append(HSLInputs(color, self)) + + if kwargs.get("use_hsv", True): + input_fields.append(HSVInputs(color, self)) + + if kwargs.get("use_cmyk", True): + input_fields.append(CMYKInputs(color, self)) + + inputs_widget = QtWidgets.QWidget(self) + inputs_layout = QtWidgets.QVBoxLayout(inputs_widget) + + for input_field in input_fields: + inputs_layout.addWidget(input_field) + input_field.value_changed.connect(self._on_value_change) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(inputs_widget, 0) + spacer = QtWidgets.QWidget(self) + layout.addWidget(spacer, 1) + + self.input_fields = input_fields + + self.color = color + + def set_color(self, color): + if ( + color.red() == self.color.red() + and color.green() == self.color.green() + and color.blue() == self.color.blue() + ): + return + self.color.setRed(color.red()) + self.color.setGreen(color.green()) + self.color.setBlue(color.blue()) + self._on_value_change() + + def _on_value_change(self): + for input_field in self.input_fields: + input_field.color_changed() + self.color_changed.emit(self.color) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py new file mode 100644 index 0000000000..d06af73cbf --- /dev/null +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -0,0 +1,115 @@ +from Qt import QtWidgets, QtCore, QtGui + +from .color_triangle import QtColorTriangle +from .color_view import ColorViewer +from .color_screen_pick import PickScreenColorWidget +from .color_inputs import ( + ColorInputsWidget, + AlphaInputs +) + + +class ColorPickerWidget(QtWidgets.QWidget): + color_changed = QtCore.Signal(QtGui.QColor) + + def __init__(self, color=None, parent=None): + super(ColorPickerWidget, self).__init__(parent) + + # Eye picked widget + pick_widget = PickScreenColorWidget() + + # Color utils + utils_widget = QtWidgets.QWidget(self) + utils_layout = QtWidgets.QVBoxLayout(utils_widget) + + bottom_utils_widget = QtWidgets.QWidget(utils_widget) + + # Color triangle + color_triangle = QtColorTriangle(utils_widget) + + # Color preview + color_view = ColorViewer(bottom_utils_widget) + color_view.setMaximumHeight(50) + + # Color pick button + btn_pick_color = QtWidgets.QPushButton( + "Pick a color", bottom_utils_widget + ) + + # Color inputs widget + color_inputs = ColorInputsWidget(self) + + # Alpha inputs + alpha_input_wrapper_widget = QtWidgets.QWidget(self) + alpha_input_wrapper_layout = QtWidgets.QVBoxLayout( + alpha_input_wrapper_widget + ) + + alpha_inputs = AlphaInputs(alpha_input_wrapper_widget) + alpha_input_wrapper_layout.addWidget(alpha_inputs) + alpha_input_wrapper_layout.addWidget(QtWidgets.QWidget(), 1) + + bottom_utils_layout = QtWidgets.QHBoxLayout(bottom_utils_widget) + bottom_utils_layout.setContentsMargins(0, 0, 0, 0) + bottom_utils_layout.addWidget(color_view, 1) + bottom_utils_layout.addWidget(btn_pick_color, 0) + + utils_layout.addWidget(bottom_utils_widget, 0) + utils_layout.addWidget(color_triangle, 1) + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(utils_widget, 1) + layout.addWidget(color_inputs, 0) + layout.addWidget(alpha_input_wrapper_widget, 0) + + color_view.set_color(color_triangle.cur_color) + color_inputs.set_color(color_triangle.cur_color) + + color_triangle.color_changed.connect(self.triangle_color_changed) + pick_widget.color_selected.connect(self.on_color_change) + color_inputs.color_changed.connect(self.on_color_change) + alpha_inputs.alpha_changed.connect(self.alpha_changed) + btn_pick_color.released.connect(self.pick_color) + + self.pick_widget = pick_widget + self.utils_widget = utils_widget + self.bottom_utils_widget = bottom_utils_widget + + self.color_triangle = color_triangle + self.color_view = color_view + self.btn_pick_color = btn_pick_color + self.color_inputs = color_inputs + self.alpha_inputs = alpha_inputs + + if color: + self.set_color(color) + self.alpha_changed(color.alpha()) + + def showEvent(self, event): + super(ColorPickerWidget, self).showEvent(event) + triangle_width = int(( + self.utils_widget.height() - self.bottom_utils_widget.height() + ) / 5 * 4) + self.color_triangle.setMinimumWidth(triangle_width) + + def color(self): + return self.color_view.color() + + def set_color(self, color): + self.alpha_inputs.set_alpha(color.alpha()) + self.on_color_change(color) + + def pick_color(self): + self.pick_widget.pick_color() + + def triangle_color_changed(self, color): + self.color_view.set_color(color) + self.color_inputs.set_color(color) + + def on_color_change(self, color): + self.color_view.set_color(color) + self.color_triangle.set_color(color) + self.color_inputs.set_color(color) + + def alpha_changed(self, alpha): + self.color_view.set_alpha(alpha) diff --git a/openpype/widgets/color_widgets/color_screen_pick.py b/openpype/widgets/color_widgets/color_screen_pick.py new file mode 100644 index 0000000000..87f50745eb --- /dev/null +++ b/openpype/widgets/color_widgets/color_screen_pick.py @@ -0,0 +1,248 @@ +import Qt +from Qt import QtWidgets, QtCore, QtGui + + +class PickScreenColorWidget(QtWidgets.QWidget): + color_selected = QtCore.Signal(QtGui.QColor) + + def __init__(self, parent=None): + super(PickScreenColorWidget, self).__init__(parent) + self.labels = [] + self.magnification = 2 + + self._min_magnification = 1 + self._max_magnification = 10 + + def add_magnification_delta(self, delta): + _delta = abs(delta / 1000) + if delta > 0: + self.magnification += _delta + else: + self.magnification -= _delta + + if self.magnification > self._max_magnification: + self.magnification = self._max_magnification + elif self.magnification < self._min_magnification: + self.magnification = self._min_magnification + + def pick_color(self): + if self.labels: + if self.labels[0].isVisible(): + return + self.labels = [] + + for screen in QtWidgets.QApplication.screens(): + label = PickLabel(self) + label.pick_color(screen) + label.color_selected.connect(self.on_color_select) + label.close_session.connect(self.end_pick_session) + self.labels.append(label) + + def end_pick_session(self): + for label in self.labels: + label.close() + self.labels = [] + + def on_color_select(self, color): + self.color_selected.emit(color) + self.end_pick_session() + + +class PickLabel(QtWidgets.QLabel): + color_selected = QtCore.Signal(QtGui.QColor) + close_session = QtCore.Signal() + + def __init__(self, pick_widget): + super(PickLabel, self).__init__() + self.setMouseTracking(True) + + self.pick_widget = pick_widget + + self.radius_pen = QtGui.QPen(QtGui.QColor(27, 27, 27), 2) + self.text_pen = QtGui.QPen(QtGui.QColor(127, 127, 127), 4) + self.text_bg = QtGui.QBrush(QtGui.QColor(27, 27, 27)) + self._mouse_over = False + + self.radius = 100 + self.radius_ratio = 11 + + @property + def magnification(self): + return self.pick_widget.magnification + + def pick_color(self, screen_obj): + self.show() + self.windowHandle().setScreen(screen_obj) + geo = screen_obj.geometry() + args = ( + QtWidgets.QApplication.desktop().winId(), + geo.x(), geo.y(), geo.width(), geo.height() + ) + if Qt.__binding__ in ("PyQt4", "PySide"): + pix = QtGui.QPixmap.grabWindow(*args) + else: + pix = screen_obj.grabWindow(*args) + + if pix.width() > pix.height(): + size = pix.height() + else: + size = pix.width() + + self.radius = int(size / self.radius_ratio) + + self.setPixmap(pix) + self.showFullScreen() + + def wheelEvent(self, event): + y_delta = event.angleDelta().y() + self.pick_widget.add_magnification_delta(y_delta) + self.update() + + def enterEvent(self, event): + self._mouse_over = True + super().enterEvent(event) + + def leaveEvent(self, event): + self._mouse_over = False + super().leaveEvent(event) + self.update() + + def mouseMoveEvent(self, event): + self.update() + + def paintEvent(self, event): + super().paintEvent(event) + if not self._mouse_over: + return + + mouse_pos_to_widet = self.mapFromGlobal(QtGui.QCursor.pos()) + + magnified_half_size = self.radius / self.magnification + magnified_size = magnified_half_size * 2 + + zoom_x_1 = mouse_pos_to_widet.x() - magnified_half_size + zoom_x_2 = mouse_pos_to_widet.x() + magnified_half_size + zoom_y_1 = mouse_pos_to_widet.y() - magnified_half_size + zoom_y_2 = mouse_pos_to_widet.y() + magnified_half_size + pix_width = magnified_size + pix_height = magnified_size + draw_pos_x = 0 + draw_pos_y = 0 + if zoom_x_1 < 0: + draw_pos_x = abs(zoom_x_1) + pix_width -= draw_pos_x + zoom_x_1 = 1 + elif zoom_x_2 > self.pixmap().width(): + pix_width -= zoom_x_2 - self.pixmap().width() + + if zoom_y_1 < 0: + draw_pos_y = abs(zoom_y_1) + pix_height -= draw_pos_y + zoom_y_1 = 1 + elif zoom_y_2 > self.pixmap().height(): + pix_height -= zoom_y_2 - self.pixmap().height() + + new_pix = QtGui.QPixmap(magnified_size, magnified_size) + new_pix.fill(QtCore.Qt.transparent) + new_pix_painter = QtGui.QPainter(new_pix) + new_pix_painter.drawPixmap( + QtCore.QRect(draw_pos_x, draw_pos_y, pix_width, pix_height), + self.pixmap().copy(zoom_x_1, zoom_y_1, pix_width, pix_height) + ) + new_pix_painter.end() + + painter = QtGui.QPainter(self) + + ellipse_rect = QtCore.QRect( + mouse_pos_to_widet.x() - self.radius, + mouse_pos_to_widet.y() - self.radius, + self.radius * 2, + self.radius * 2 + ) + ellipse_rect_f = QtCore.QRectF(ellipse_rect) + path = QtGui.QPainterPath() + path.addEllipse(ellipse_rect_f) + painter.setClipPath(path) + + new_pix_rect = QtCore.QRect( + mouse_pos_to_widet.x() - self.radius + 1, + mouse_pos_to_widet.y() - self.radius + 1, + new_pix.width() * self.magnification, + new_pix.height() * self.magnification + ) + + painter.drawPixmap(new_pix_rect, new_pix) + + painter.setClipping(False) + + painter.setRenderHint(QtGui.QPainter.Antialiasing) + + painter.setPen(self.radius_pen) + painter.drawEllipse(ellipse_rect_f) + + image = self.pixmap().toImage() + if image.valid(mouse_pos_to_widet): + color = QtGui.QColor(image.pixel(mouse_pos_to_widet)) + else: + color = QtGui.QColor() + + color_text = "Red: {} - Green: {} - Blue: {}".format( + color.red(), color.green(), color.blue() + ) + font = painter.font() + font.setPointSize(self.radius / 10) + painter.setFont(font) + + text_rect_height = int(painter.fontMetrics().height() + 10) + text_rect = QtCore.QRect( + ellipse_rect.x(), + ellipse_rect.bottom(), + ellipse_rect.width(), + text_rect_height + ) + if text_rect.bottom() > self.pixmap().height(): + text_rect.moveBottomLeft(ellipse_rect.topLeft()) + + rect_radius = text_rect_height / 2 + path = QtGui.QPainterPath() + path.addRoundedRect( + QtCore.QRectF(text_rect), + rect_radius, + rect_radius + ) + painter.fillPath(path, self.text_bg) + + painter.setPen(self.text_pen) + painter.drawText( + text_rect, + QtCore.Qt.AlignLeft | QtCore.Qt.AlignCenter, + color_text + ) + + color_rect_x = ellipse_rect.x() - text_rect_height + if color_rect_x < 0: + color_rect_x += (text_rect_height + ellipse_rect.width()) + + color_rect = QtCore.QRect( + color_rect_x, + ellipse_rect.y(), + text_rect_height, + ellipse_rect.height() + ) + path = QtGui.QPainterPath() + path.addRoundedRect( + QtCore.QRectF(color_rect), + rect_radius, + rect_radius + ) + painter.fillPath(path, color) + painter.drawRoundedRect(color_rect, rect_radius, rect_radius) + painter.end() + + def mouseReleaseEvent(self, event): + color = QtGui.QColor(self.pixmap().toImage().pixel(event.pos())) + self.color_selected.emit(color) + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Escape: + self.close_session.emit() diff --git a/openpype/widgets/color_widgets/color_triangle.py b/openpype/widgets/color_widgets/color_triangle.py new file mode 100644 index 0000000000..d4db175d84 --- /dev/null +++ b/openpype/widgets/color_widgets/color_triangle.py @@ -0,0 +1,1431 @@ +from enum import Enum +from math import floor, sqrt, sin, cos, acos, pi as PI +from Qt import QtWidgets, QtCore, QtGui + +TWOPI = PI * 2 + + +class TriangleState(Enum): + IdleState = object() + SelectingHueState = object() + SelectingSatValueState = object() + + +class DoubleColor: + def __init__(self, r, g=None, b=None): + if g is None: + g = r.g + b = r.b + r = r.r + self.r = float(r) + self.g = float(g) + self.b = float(b) + + +class Vertex: + def __init__(self, color, point): + # Convert GlobalColor to QColor as globals don't have red, green, blue + if isinstance(color, QtCore.Qt.GlobalColor): + color = QtGui.QColor(color) + + # Convert QColor to DoubleColor + if isinstance(color, QtGui.QColor): + color = DoubleColor(color.red(), color.green(), color.blue()) + + self.color = color + self.point = point + + +class QtColorTriangle(QtWidgets.QWidget): + """The QtColorTriangle class provides a triangular color selection widget. + + This widget uses the HSV color model, and is therefore useful for + selecting colors by eye. + + The triangle in the center of the widget is used for selecting + saturation and value, and the surrounding circle is used for + selecting hue. + + Use set_color() and color() to set and get the current color. + """ + color_changed = QtCore.Signal(QtGui.QColor) + + # Thick of color wheel ratio where 1 is fully filled circle + inner_radius_ratio = 5.0 + # Ratio where hue selector on wheel is relative to `inner_radius_ratio` + # - middle of the wheel is twice `inner_radius_ratio` + selector_radius_ratio = inner_radius_ratio * 2 + # Size ratio of selectors on wheel and in triangle + ellipse_size_ratio = 10.0 + # Ration of selectors thickness + ellipse_thick_ratio = 50.0 + # Hue offset on color wheel (0 - 359) + # - red on top if set to "0" + hue_offset = 90 + + def __init__(self, parent=None): + super(QtColorTriangle, self).__init__(parent) + self.setSizePolicy( + QtWidgets.QSizePolicy.Minimum, + QtWidgets.QSizePolicy.Minimum + ) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + self.angle_a = float() + self.angle_b = float() + self.angle_c = float() + + self.bg_image = QtGui.QImage( + self.sizeHint(), QtGui.QImage.Format_RGB32 + ) + self.cur_color = QtGui.QColor() + self.point_a = QtCore.QPointF() + self.point_b = QtCore.QPointF() + self.point_c = QtCore.QPointF() + self.point_d = QtCore.QPointF() + + self.cur_hue = int() + + self.pen_width = int() + self.ellipse_size = int() + self.outer_radius = int() + self.selector_pos = QtCore.QPointF() + + self.sel_mode = TriangleState.IdleState + + self._triangle_outline_pen = QtGui.QPen( + QtGui.QColor(40, 40, 40, 128), + 2 + ) + # Prepare hue numbers for color circle + _hue_circle_range = [] + for idx in range(11): + # Some Qt versions may require: + # hue = int(idx * 360.0) + percent_idx = idx * 0.1 + hue = int(360.0 - (percent_idx * 360.0)) + _hue_circle_range.append((percent_idx, hue)) + self._hue_circle_range = tuple(_hue_circle_range) + + color = QtGui.QColor() + color.setHsv(0, 255, 255) + self.set_color(color) + + def set_color(self, col): + if ( + col.red() == self.cur_color.red() + and col.green() == self.cur_color.green() + and col.blue() == self.cur_color.blue() + ): + return + + self.cur_color = col + + hue, *_ = self.cur_color.getHsv() + + # Never use an invalid hue to display colors + if hue != -1: + self.cur_hue = hue + + angle_with_offset = (360 - self.cur_hue - self.hue_offset) % 360 + self.angle_a = (angle_with_offset * TWOPI) / 360.0 + self.angle_a += PI / 2.0 + if self.angle_a > TWOPI: + self.angle_a -= TWOPI + + self.angle_b = self.angle_a + TWOPI / 3 + self.angle_c = self.angle_b + TWOPI / 3 + + if self.angle_b > TWOPI: + self.angle_b -= TWOPI + if self.angle_c > TWOPI: + self.angle_c -= TWOPI + + cx = float(self.contentsRect().center().x()) + cy = float(self.contentsRect().center().y()) + inner_radius = ( + self.outer_radius + - (self.outer_radius / self.inner_radius_ratio) + ) + selector_radius = ( + self.outer_radius + - (self.outer_radius / self.selector_radius_ratio) + ) + self.point_a = QtCore.QPointF( + cx + (cos(self.angle_a) * inner_radius), + cy - (sin(self.angle_a) * inner_radius) + ) + self.point_b = QtCore.QPointF( + cx + (cos(self.angle_b) * inner_radius), + cy - (sin(self.angle_b) * inner_radius) + ) + self.point_c = QtCore.QPointF( + cx + (cos(self.angle_c) * inner_radius), + cy - (sin(self.angle_c) * inner_radius) + ) + self.point_d = QtCore.QPointF( + cx + (cos(self.angle_a) * selector_radius), + cy - (sin(self.angle_a) * selector_radius) + ) + + self.selector_pos = self._point_from_color(self.cur_color) + self.update() + + self.color_changed.emit(self.cur_color) + + def heightForWidth(self, width): + return width + + def polish(self): + size_w = self.contentsRect().width() + size_h = self.contentsRect().height() + if size_w < size_h: + size = size_w + else: + size = size_h + + self.outer_radius = (size - 1) / 2 + + self.pen_width = int( + floor(self.outer_radius / self.ellipse_thick_ratio) + ) + self.ellipse_size = int( + floor(self.outer_radius / self.ellipse_size_ratio) + ) + + cx = float(self.contentsRect().center().x()) + cy = float(self.contentsRect().center().y()) + + inner_radius = ( + self.outer_radius + - (self.outer_radius / self.inner_radius_ratio) + ) + selector_radius = ( + self.outer_radius + - (self.outer_radius / self.selector_radius_ratio) + ) + self.point_a = QtCore.QPointF( + cx + (cos(self.angle_a) * inner_radius), + cy - (sin(self.angle_a) * inner_radius) + ) + self.point_b = QtCore.QPointF( + cx + (cos(self.angle_b) * inner_radius), + cy - (sin(self.angle_b) * inner_radius) + ) + self.point_c = QtCore.QPointF( + cx + (cos(self.angle_c) * inner_radius), + cy - (sin(self.angle_c) * inner_radius) + ) + self.point_d = QtCore.QPointF( + cx + (cos(self.angle_a) * selector_radius), + cy - (sin(self.angle_a) * selector_radius) + ) + + self.selector_pos = self._point_from_color(self.cur_color) + + self.update() + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + if event.rect().intersects(self.contentsRect()): + event_region = event.region() + if hasattr(event_region, "intersect"): + clip_region = event_region.intersect(self.contentsRect()) + else: + clip_region = event_region.intersected( + self.contentsRect() + ) + painter.setClipRegion(clip_region) + + self.paint_bg() + + # Blit the static generated background with the hue gradient onto + # the double buffer. + buf = QtGui.QImage(self.bg_image.copy()) + + # Draw the trigon + # Find the color with only the hue, and max value and saturation + hue_color = QtGui.QColor() + hue_color.setHsv(self.cur_hue, 255, 255) + + # Draw the triangle + self.drawTrigon( + buf, self.point_a, self.point_b, self.point_c, hue_color + ) + + # Slow step: convert the image to a pixmap + pix = QtGui.QPixmap.fromImage(buf) + pix_painter = QtGui.QPainter(pix) + pix_painter.setRenderHint(QtGui.QPainter.Antialiasing) + + # Draw an outline of the triangle + pix_painter.setPen(self._triangle_outline_pen) + pix_painter.drawLine(self.point_a, self.point_b) + pix_painter.drawLine(self.point_b, self.point_c) + pix_painter.drawLine(self.point_c, self.point_a) + + # Draw the color wheel selector + pix_painter.setPen(QtGui.QPen(QtCore.Qt.white, self.pen_width)) + pix_painter.drawEllipse( + int(self.point_d.x() - self.ellipse_size / 2.0), + int(self.point_d.y() - self.ellipse_size / 2.0), + self.ellipse_size, self.ellipse_size + ) + + # Draw the triangle selector + pix_painter.setBrush(self.cur_color) + pix_painter.drawEllipse( + QtCore.QRectF( + self.selector_pos.x() - self.ellipse_size / 2.0, + self.selector_pos.y() - self.ellipse_size / 2.0, + self.ellipse_size + 0.5, + self.ellipse_size + 0.5 + ) + ) + + pix_painter.end() + # Blit + painter.drawPixmap(self.contentsRect().topLeft(), pix) + painter.end() + + def mouseMoveEvent(self, event): + if (event.buttons() & QtCore.Qt.LeftButton) == 0: + return + + depos = QtCore.QPointF( + event.pos().x(), + event.pos().y() + ) + new_color = False + + if self.sel_mode is TriangleState.SelectingHueState: + self.angle_a = self._angle_at(depos, self.contentsRect()) + self.angle_b = self.angle_a + (TWOPI / 3.0) + self.angle_c = self.angle_b + (TWOPI / 3.0) + if self.angle_b > TWOPI: + self.angle_b -= TWOPI + if self.angle_c > TWOPI: + self.angle_c -= TWOPI + + am = self.angle_a - (PI / 2) + if am < 0: + am += TWOPI + self.cur_hue = ( + 360 - int((am * 360.0) / TWOPI) - self.hue_offset + ) % 360 + hue, sat, val, _ = self.cur_color.getHsv() + + if self.cur_hue != hue: + new_color = True + self.cur_color.setHsv(self.cur_hue, sat, val) + + cx = float(self.contentsRect().center().x()) + cy = float(self.contentsRect().center().y()) + inner_radius = ( + self.outer_radius + - (self.outer_radius / self.inner_radius_ratio) + ) + selector_radius = ( + self.outer_radius + - (self.outer_radius / self.selector_radius_ratio) + ) + self.point_a = QtCore.QPointF( + cx + (cos(self.angle_a) * inner_radius), + cy - (sin(self.angle_a) * inner_radius) + ) + self.point_b = QtCore.QPointF( + cx + (cos(self.angle_b) * inner_radius), + cy - (sin(self.angle_b) * inner_radius) + ) + self.point_c = QtCore.QPointF( + cx + (cos(self.angle_c) * inner_radius), + cy - (sin(self.angle_c) * inner_radius) + ) + self.point_d = QtCore.QPointF( + cx + (cos(self.angle_a) * selector_radius), + cy - (sin(self.angle_a) * selector_radius) + ) + + self.selector_pos = self._point_from_color(self.cur_color) + else: + aa = Vertex(QtCore.Qt.transparent, self.point_a) + bb = Vertex(QtCore.Qt.transparent, self.point_b) + cc = Vertex(QtCore.Qt.transparent, self.point_c) + + self.selector_pos = self._move_point_to_triangle( + depos.x(), depos.y(), aa, bb, cc + ) + col = self._color_from_point(self.selector_pos) + if col != self.cur_color: + # Ensure that hue does not change when selecting + # saturation and value. + _, sat, val, _ = col.getHsv() + self.cur_color.setHsv(self.cur_hue, sat, val) + new_color = True + + if new_color: + self.color_changed.emit(self.cur_color) + + self.update() + + def mousePressEvent(self, event): + # Only respond to the left mouse button. + if event.button() != QtCore.Qt.LeftButton: + return + + depos = QtCore.QPointF( + event.pos().x(), + event.pos().y() + ) + rad = self._radius_at(depos, self.contentsRect()) + new_color = False + + # As in mouseMoveEvent, either find the a, b, c angles or the + # radian position of the selector, then order an update. + inner_radius = ( + self.outer_radius - (self.outer_radius / self.inner_radius_ratio) + ) + if rad > inner_radius: + self.sel_mode = TriangleState.SelectingHueState + + self.angle_a = self._angle_at(depos, self.contentsRect()) + self.angle_b = self.angle_a + TWOPI / 3.0 + self.angle_c = self.angle_b + TWOPI / 3.0 + if self.angle_b > TWOPI: + self.angle_b -= TWOPI + if self.angle_c > TWOPI: + self.angle_c -= TWOPI + + am = self.angle_a - PI / 2 + if am < 0: + am += TWOPI + + self.cur_hue = ( + 360 - int((am * 360.0) / TWOPI) - self.hue_offset + ) % 360 + hue, sat, val, _ = self.cur_color.getHsv() + + if hue != self.cur_hue: + new_color = True + self.cur_color.setHsv(self.cur_hue, sat, val) + + cx = float(self.contentsRect().center().x()) + cy = float(self.contentsRect().center().y()) + + self.point_a = QtCore.QPointF( + cx + (cos(self.angle_a) * inner_radius), + cy - (sin(self.angle_a) * inner_radius) + ) + self.point_b = QtCore.QPointF( + cx + (cos(self.angle_b) * inner_radius), + cy - (sin(self.angle_b) * inner_radius) + ) + self.point_c = QtCore.QPointF( + cx + (cos(self.angle_c) * inner_radius), + cy - (sin(self.angle_c) * inner_radius) + ) + + selector_radius = ( + self.outer_radius + - (self.outer_radius / self.selector_radius_ratio) + ) + self.point_d = QtCore.QPointF( + cx + (cos(self.angle_a) * selector_radius), + cy - (sin(self.angle_a) * selector_radius) + ) + + self.selector_pos = self._point_from_color(self.cur_color) + self.color_changed.emit(self.cur_color) + else: + self.sel_mode = TriangleState.SelectingSatValueState + + aa = Vertex(QtCore.Qt.transparent, self.point_a) + bb = Vertex(QtCore.Qt.transparent, self.point_b) + cc = Vertex(QtCore.Qt.transparent, self.point_c) + + self.selector_pos = self._move_point_to_triangle( + depos.x(), depos.y(), aa, bb, cc + ) + col = self._color_from_point(self.selector_pos) + if col != self.cur_color: + self.cur_color = col + new_color = True + + if new_color: + self.color_changed.emit(self.cur_color) + + self.update() + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.sel_mode = TriangleState.IdleState + + def keyPressEvent(self, event): + key = event.key() + if key == QtCore.Qt.Key_Left: + self.cur_hue -= 1 + if self.cur_hue < 0: + self.cur_hue += 360 + _, sat, val, _ = self.cur_color.getHsv() + + tmp = QtGui.QColor() + tmp.setHsv(self.cur_hue, sat, val) + self.set_color(tmp) + + elif key == QtCore.Qt.Key_Right: + self.cur_hue += 1 + if (self.cur_hue > 359): + self.cur_hue -= 360 + _, sat, val, _ = self.cur_color.getHsv() + tmp = QtGui.QColor() + tmp.setHsv(self.cur_hue, sat, val) + self.set_color(tmp) + + elif key == QtCore.Qt.Key_Up: + _, sat, val, _ = self.cur_color.getHsv() + if event.modifiers() & QtCore.Qt.ShiftModifier: + if sat > 5: + sat -= 5 + else: + sat = 0 + else: + if val > 5: + val -= 5 + else: + val = 0 + + tmp = QtGui.QColor() + tmp.setHsv(self.cur_hue, sat, val) + self.set_color(tmp) + + elif key == QtCore.Qt.Key_Down: + _, sat, val, _ = self.cur_color.getHsv() + if event.modifiers() & QtCore.Qt.ShiftModifier: + if sat < 250: + sat += 5 + else: + sat = 255 + else: + if val < 250: + val += 5 + else: + val = 255 + + tmp = QtGui.QColor() + tmp.setHsv(self.cur_hue, sat, val) + self.set_color(tmp) + + def resizeEvent(self, _event): + size_w = self.contentsRect().width() + size_h = self.contentsRect().height() + if size_w < size_h: + size = size_w + else: + size = size_h + + self.outer_radius = (size - 1) / 2 + + self.pen_width = int( + floor(self.outer_radius / self.ellipse_thick_ratio) + ) + self.ellipse_size = int( + floor(self.outer_radius / self.ellipse_size_ratio) + ) + + cx = float(self.contentsRect().center().x()) + cy = float(self.contentsRect().center().y()) + inner_radius = ( + self.outer_radius + - (self.outer_radius / self.inner_radius_ratio) + ) + selector_radius = ( + self.outer_radius + - (self.outer_radius / self.selector_radius_ratio) + ) + self.point_a = QtCore.QPointF( + cx + (cos(self.angle_a) * inner_radius), + cy - (sin(self.angle_a) * inner_radius) + ) + self.point_b = QtCore.QPointF( + cx + (cos(self.angle_b) * inner_radius), + cy - (sin(self.angle_b) * inner_radius) + ) + self.point_c = QtCore.QPointF( + cx + (cos(self.angle_c) * inner_radius), + cy - (sin(self.angle_c) * inner_radius) + ) + self.point_d = QtCore.QPointF( + cx + (cos(self.angle_a) * selector_radius), + cy - (sin(self.angle_a) * selector_radius) + ) + + # Find the current position of the selector + self.selector_pos = self._point_from_color(self.cur_color) + + self.update() + + def drawTrigon(self, buf, pa, pb, pc, color): + # Create three Vertex objects. A Vertex contains a double-point + # coordinate and a color. + # pa is the tip of the arrow + # pb is the black corner + # pc is the white corner + p1 = Vertex(color, pa) + p2 = Vertex(QtCore.Qt.black, pb) + p3 = Vertex(QtCore.Qt.white, pc) + + # Sort. Make p1 above p2, which is above p3 (using y coordinate). + # Bubble sorting is fastest here. + if p1.point.y() > p2.point.y(): + p1, p2 = p2, p1 + if p1.point.y() > p3.point.y(): + p1, p3 = p3, p1 + if p2.point.y() > p3.point.y(): + p2, p3 = p3, p2 + + # All the three y deltas are >= 0 + p1p2ydist = float(p2.point.y() - p1.point.y()) + p1p3ydist = float(p3.point.y() - p1.point.y()) + p2p3ydist = float(p3.point.y() - p2.point.y()) + p1p2xdist = float(p2.point.x() - p1.point.x()) + p1p3xdist = float(p3.point.x() - p1.point.x()) + p2p3xdist = float(p3.point.x() - p2.point.x()) + + # The first x delta decides wether we have a lefty or a righty + # trigon. + lefty = p1p2xdist < 0 + + # Left and right colors and X values. The key in this map is the + # y values. Our goal is to fill these structures with all the + # information needed to do a single pass top-to-bottom, + # left-to-right drawing of the trigon. + leftColors = {} + rightColors = {} + leftX = {} + rightX = {} + + # Scan longy - find all left and right colors and X-values for + # the tallest edge (p1-p3). + # Initialize with known values + x = p1.point.x() + source = p1.color + dest = p3.color + r = source.r + g = source.g + b = source.b + y1 = int(floor(p1.point.y())) + y2 = int(floor(p3.point.y())) + + # Find slopes (notice that if the y dists are 0, we don't care + # about the slopes) + xdelta = 0.0 + rdelta = 0.0 + gdelta = 0.0 + bdelta = 0.0 + if p1p3ydist != 0.0: + xdelta = p1p3xdist / p1p3ydist + rdelta = (dest.r - r) / p1p3ydist + gdelta = (dest.g - g) / p1p3ydist + bdelta = (dest.b - b) / p1p3ydist + + # Calculate gradients using linear approximation + for y in range(y1, y2): + if lefty: + rightColors[y] = DoubleColor(r, g, b) + rightX[y] = x + else: + leftColors[y] = DoubleColor(r, g, b) + leftX[y] = x + + r += rdelta + g += gdelta + b += bdelta + x += xdelta + + # Scan top shorty - find all left and right colors and x-values + # for the topmost of the two not-tallest short edges. + x = p1.point.x() + source = p1.color + dest = p2.color + r = source.r + g = source.g + b = source.b + y1 = int(floor(p1.point.y())) + y2 = int(floor(p2.point.y())) + + # Find slopes (notice that if the y dists are 0, we don't care + # about the slopes) + xdelta = 0.0 + rdelta = 0.0 + gdelta = 0.0 + bdelta = 0.0 + if p1p2ydist != 0.0: + xdelta = p1p2xdist / p1p2ydist + rdelta = (dest.r - r) / p1p2ydist + gdelta = (dest.g - g) / p1p2ydist + bdelta = (dest.b - b) / p1p2ydist + + # Calculate gradients using linear approximation + for y in range(y1, y2): + if lefty: + leftColors[y] = DoubleColor(r, g, b) + leftX[y] = x + else: + rightColors[y] = DoubleColor(r, g, b) + rightX[y] = x + + r += rdelta + g += gdelta + b += bdelta + x += xdelta + + # Scan bottom shorty - find all left and right colors and + # x-values for the bottommost of the two not-tallest short edges. + x = p2.point.x() + source = p2.color + dest = p3.color + r = source.r + g = source.g + b = source.b + y1 = int(floor(p2.point.y())) + y2 = int(floor(p3.point.y())) + + # Find slopes (notice that if the y dists are 0, we don't care + # about the slopes) + xdelta = 0.0 + rdelta = 0.0 + gdelta = 0.0 + bdelta = 0.0 + if p2p3ydist != 0.0: + xdelta = p2p3xdist / p2p3ydist + rdelta = (dest.r - r) / p2p3ydist + gdelta = (dest.g - g) / p2p3ydist + bdelta = (dest.b - b) / p2p3ydist + + # Calculate gradients using linear approximation + for y in range(y1, y2): + if lefty: + leftColors[y] = DoubleColor(r, g, b) + leftX[y] = x + else: + rightColors[y] = DoubleColor(r, g, b) + rightX[y] = x + + r += rdelta + g += gdelta + b += bdelta + x += xdelta + + # Inner loop. For each y in the left map of x-values, draw one + # line from left to right. + p3yfloor = int(floor(p3.point.y())) + p1yfloor = int(floor(p1.point.y())) + for y in range(p1yfloor, p3yfloor): + lx = leftX[y] + rx = rightX[y] + + lxi = int(floor(lx)) + rxi = int(floor(rx)) + rc = rightColors[y] + lc = leftColors[y] + + # if the xdist is 0, don't draw anything. + xdist = rx - lx + if xdist != 0.0: + r = lc.r + g = lc.g + b = lc.b + rdelta = (rc.r - r) / xdist + gdelta = (rc.g - g) / xdist + bdelta = (rc.b - b) / xdist + + # Inner loop 2. Draws the line from left to right. + for x in range(lxi, rxi + 1): + buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b))) + r += rdelta + g += gdelta + b += bdelta + + def _radius_at(self, pos, rect): + mousexdist = pos.x() - float(rect.center().x()) + mouseydist = pos.y() - float(rect.center().y()) + return sqrt(mousexdist ** 2 + mouseydist ** 2) + + def _angle_at(self, pos, rect): + mousexdist = pos.x() - float(rect.center().x()) + mouseydist = pos.y() - float(rect.center().y()) + mouserad = sqrt(mousexdist ** 2 + mouseydist ** 2) + if mouserad == 0.0: + return 0.0 + + angle = acos(mousexdist / mouserad) + if mouseydist >= 0: + angle = TWOPI - angle + + return angle + + def _point_from_color(self, col): + # Simplifications for the corner cases. + if col == QtCore.Qt.black: + return self.point_b + elif col == QtCore.Qt.white: + return self.point_c + + # Find the x and y slopes + ab_deltax = self.point_b.x() - self.point_a.x() + ab_deltay = self.point_b.y() - self.point_a.y() + bc_deltax = self.point_c.x() - self.point_b.x() + bc_deltay = self.point_c.y() - self.point_b.y() + ac_deltax = self.point_c.x() - self.point_a.x() + ac_deltay = self.point_c.y() - self.point_a.y() + + # Extract the h,s,v values of col. + _, sat, val, _ = col.getHsv() + + # Find the line that passes through the triangle where the value + # is equal to our color's value. + p1 = self.point_a.x() + (ab_deltax * float(255 - val)) / 255.0 + q1 = self.point_a.y() + (ab_deltay * float(255 - val)) / 255.0 + p2 = self.point_b.x() + (bc_deltax * float(val)) / 255.0 + q2 = self.point_b.y() + (bc_deltay * float(val)) / 255.0 + + # Find the line that passes through the triangle where the + # saturation is equal to our color's value. + p3 = self.point_a.x() + (ac_deltax * float(255 - sat)) / 255.0 + q3 = self.point_a.y() + (ac_deltay * float(255 - sat)) / 255.0 + p4 = self.point_b.x() + q4 = self.point_b.y() + + # Find the intersection between these lines. + if p1 != p2: + a = (q2 - q1) / (p2 - p1) + c = (q4 - q3) / (p4 - p3) + b = q1 - a * p1 + d = q3 - c * p3 + + x = (d - b) / (a - c) + y = a * x + b + else: + x = p1 + p4_p3 = p4 - p3 + if p4_p3 == 0: + y = 0 + else: + y = q3 + (x - p3) * (q4 - q3) / p4_p3 + + return QtCore.QPointF(x, y) + + def _color_from_point(self, p): + # Find the outer radius of the hue gradient. + size_w = self.contentsRect().width() + size_h = self.contentsRect().height() + if size_w < size_h: + size = size_w + else: + size = size_h + outer_radius = (size - 1) / 2 + + # Find the center coordinates + cx = float(self.contentsRect().center().x()) + cy = float(self.contentsRect().center().y()) + + # Find the a, b and c from their angles, the center of the rect + # and the radius of the hue gradient donut. + inner_radius = outer_radius - (outer_radius / self.inner_radius_ratio) + pa = QtCore.QPointF( + cx + (cos(self.angle_a) * inner_radius), + cy - (sin(self.angle_a) * inner_radius) + ) + pb = QtCore.QPointF( + cx + (cos(self.angle_b) * inner_radius), + cy - (sin(self.angle_b) * inner_radius) + ) + pc = QtCore.QPointF( + cx + (cos(self.angle_c) * inner_radius), + cy - (sin(self.angle_c) * inner_radius) + ) + + # Find the hue value from the angle of the 'a' point. + angle = self.angle_a - PI / 2.0 + if angle < 0: + angle += TWOPI + hue = ( + 360 + - int(floor((360.0 * angle) / TWOPI)) + - self.hue_offset + ) % 360 + + # Create the color of the 'a' corner point. We know that b is + # black and c is white. + color = QtGui.QColor() + color.setHsv(hue, 255, 255) + + # See also drawTrigon(), which basically does exactly the same to + # determine all colors in the trigon. + p1 = Vertex(color, pa) + p2 = Vertex(QtCore.Qt.black, pb) + p3 = Vertex(QtCore.Qt.white, pc) + + # Make sure p1 is above p2, which is above p3. + if p1.point.y() > p2.point.y(): + p1, p2 = p2, p1 + if p1.point.y() > p3.point.y(): + p1, p3 = p3, p1 + if p2.point.y() > p3.point.y(): + p2, p3 = p3, p2 + + # Find the slopes of all edges in the trigon. All the three y + # deltas here are positive because of the above sorting. + p1p2ydist = p2.point.y() - p1.point.y() + p1p3ydist = p3.point.y() - p1.point.y() + p2p3ydist = p3.point.y() - p2.point.y() + p1p2xdist = p2.point.x() - p1.point.x() + p1p3xdist = p3.point.x() - p1.point.x() + p2p3xdist = p3.point.x() - p2.point.x() + + # The first x delta decides wether we have a lefty or a righty + # trigon. A lefty trigon has its tallest edge on the right hand + # side of the trigon. The righty trigon has it on its left side. + # This property determines wether the left or the right set of x + # coordinates will be continuous. + lefty = p1p2xdist < 0 + + # Find whether the selector's y is in the first or second shorty, + # counting from the top and downwards. This is used to find the + # color at the selector point. + firstshorty = (p.y() >= p1.point.y() and p.y() < p2.point.y()) + + # From the y value of the selector's position, find the left and + # right x values. + if lefty: + if firstshorty: + if (floor(p1p2ydist) != 0.0): + leftx = ( + p1.point.x() + + ((p1p2xdist * (p.y() - p1.point.y())) / p1p2ydist) + ) + else: + leftx = min(p1.point.x(), p2.point.x()) + + else: + if (floor(p2p3ydist) != 0.0): + leftx = ( + p2.point.x() + + (p2p3xdist * (p.y() - p2.point.y())) / p2p3ydist + ) + else: + leftx = min(p2.point.x(), p3.point.x()) + + rightx = ( + p1.point.x() + + ((p1p3xdist * (p.y() - p1.point.y())) / p1p3ydist) + ) + else: + leftx = ( + p1.point.x() + + ((p1p3xdist * (p.y() - p1.point.y())) / p1p3ydist) + ) + if firstshorty: + if floor(p1p2ydist) != 0.0: + rightx = ( + p1.point.x() + + ((p1p2xdist * (p.y() - p1.point.y())) / p1p2ydist) + ) + else: + rightx = max(p1.point.x(), p2.point.x()) + + else: + if floor(p2p3ydist) != 0.0: + rightx = ( + p2.point.x() + + ((p2p3xdist * (p.y() - p2.point.y())) / p2p3ydist) + ) + else: + rightx = max(p2.point.x(), p3.point.x()) + + # Find the r,g,b values of the points on the trigon's edges that + # are to the left and right of the selector. + if firstshorty: + if floor(p1p2ydist) != 0.0: + p_p1_ratio = (p.y() - p1.point.y()) / p1p2ydist + p2_p_ratio = (p2.point.y() - p.y()) / p1p2ydist + rshort = (p2.color.r * p_p1_ratio) + (p1.color.r * p2_p_ratio) + gshort = (p2.color.g * p_p1_ratio) + (p1.color.g * p2_p_ratio) + bshort = (p2.color.b * p_p1_ratio) + (p1.color.b * p2_p_ratio) + elif lefty: + if p1.point.x() <= p2.point.x(): + rshort = p1.color.r + gshort = p1.color.g + bshort = p1.color.b + else: + rshort = p2.color.r + gshort = p2.color.g + bshort = p2.color.b + + else: + if p1.point.x() > p2.point.x(): + rshort = p1.color.r + gshort = p1.color.g + bshort = p1.color.b + else: + rshort = p2.color.r + gshort = p2.color.g + bshort = p2.color.b + + else: + if floor(p2p3ydist) != 0.0: + p_p2_ratio = (p.y() - p2.point.y()) / p2p3ydist + p3_p_ratio = (p3.point.y() - p.y()) / p2p3ydist + rshort = (p3.color.r * p_p2_ratio) + (p2.color.r * p3_p_ratio) + gshort = (p3.color.g * p_p2_ratio) + (p2.color.g * p3_p_ratio) + bshort = (p3.color.b * p_p2_ratio) + (p2.color.b * p3_p_ratio) + elif lefty: + if p2.point.x() <= p3.point.x(): + rshort = p2.color.r + gshort = p2.color.g + bshort = p2.color.b + else: + rshort = p3.color.r + gshort = p3.color.g + bshort = p3.color.b + + else: + if p2.point.x() > p3.point.x(): + rshort = p2.color.r + gshort = p2.color.g + bshort = p2.color.b + else: + rshort = p3.color.r + gshort = p3.color.g + bshort = p3.color.b + + # p1p3ydist is never 0 + p_p1_ratio = (p.y() - p1.point.y()) / p1p3ydist + p3_p_ratio = (p3.point.y() - p.y()) / p1p3ydist + rlong = (p3.color.r * p_p1_ratio) + (p1.color.r * p3_p_ratio) + glong = (p3.color.g * p_p1_ratio) + (p1.color.g * p3_p_ratio) + blong = (p3.color.b * p_p1_ratio) + (p1.color.b * p3_p_ratio) + + # rshort,gshort,bshort is the color on one of the shortys. + # rlong,glong,blong is the color on the longy. So depending on + # wether we have a lefty trigon or not, we can determine which + # colors are on the left and right edge. + if lefty: + rl = rshort + gl = gshort + bl = bshort + rr = rlong + gr = glong + br = blong + else: + rl = rlong + gl = glong + bl = blong + rr = rshort + gr = gshort + br = bshort + + # Find the distance from the left x to the right x (xdist). Then + # find the distances from the selector to each of these (saxdist + # and saxdist2). These distances are used to find the color at + # the selector. + xdist = rightx - leftx + saxdist = p.x() - leftx + saxdist2 = xdist - saxdist + + # Now determine the r,g,b values of the selector using a linear + # approximation. + if xdist != 0.0: + r = (saxdist2 * rl / xdist) + (saxdist * rr / xdist) + g = (saxdist2 * gl / xdist) + (saxdist * gr / xdist) + b = (saxdist2 * bl / xdist) + (saxdist * br / xdist) + else: + # In theory, the left and right color will be equal here. But + # because of the loss of precision, we get an error on both + # colors. The best approximation we can get is from adding + # the two errors, which in theory will eliminate the error + # but in practise will only minimize it. + r = (rl + rr) / 2 + g = (gl + gr) / 2 + b = (bl + br) / 2 + + # Now floor the color components and fit them into proper + # boundaries. This again is to compensate for the error caused by + # loss of precision. + ri = int(floor(r)) + gi = int(floor(g)) + bi = int(floor(b)) + if ri < 0: + ri = 0 + elif ri > 255: + ri = 255 + + if gi < 0: + gi = 0 + elif gi > 255: + gi = 255 + + if bi < 0: + bi = 0 + elif bi > 255: + bi = 255 + + # Voila, we have the color at the point of the selector. + return QtGui.QColor(ri, gi, bi) + + def paint_bg(self): + bg_image = QtGui.QPixmap(self.contentsRect().size()) + bg_image.fill(QtCore.Qt.transparent) + self.bg_image = bg_image + + painter = QtGui.QPainter(self.bg_image) + + painter.setRenderHint(QtGui.QPainter.Antialiasing) + + hue_gradient = QtGui.QConicalGradient( + bg_image.rect().center(), 90 - self.hue_offset + ) + sat_val_gradient = QtGui.QConicalGradient( + bg_image.rect().center(), 90 - self.hue_offset + ) + + hue_color = QtGui.QColor() + sat_val_color = QtGui.QColor() + _, sat, val, _ = self.cur_color.getHsv() + + for idx, hue in self._hue_circle_range: + hue_color.setHsv(hue, 255, 255) + sat_val_color.setHsv(hue, sat, val) + + hue_gradient.setColorAt(idx, hue_color) + sat_val_gradient.setColorAt(idx, sat_val_color) + + inner_radius = self.outer_radius - ( + self.outer_radius / self.inner_radius_ratio + ) + half_radius = self.outer_radius - ( + (self.outer_radius - inner_radius) / 2 + ) + + hue_inner_radius_rect = QtCore.QRectF( + bg_image.rect().center().x() - inner_radius, + bg_image.rect().center().y() - inner_radius, + inner_radius * 2 + 1, + inner_radius * 2 + 1 + ) + hue_outer_radius_rect = QtCore.QRectF( + bg_image.rect().center().x() - half_radius - 1, + bg_image.rect().center().y() - half_radius - 1, + half_radius * 2 + 3, + half_radius * 2 + 3 + ) + sat_val_inner_radius_rect = QtCore.QRectF( + bg_image.rect().center().x() - half_radius, + bg_image.rect().center().y() - half_radius, + half_radius * 2 + 1, + half_radius * 2 + 1 + ) + sat_val_outer_radius_rect = QtCore.QRectF( + bg_image.rect().center().x() - self.outer_radius, + bg_image.rect().center().y() - self.outer_radius, + self.outer_radius * 2 + 1, + self.outer_radius * 2 + 1 + ) + hue_path = QtGui.QPainterPath() + hue_path.addEllipse(hue_inner_radius_rect) + hue_path.addEllipse(hue_outer_radius_rect) + + sat_val_path = QtGui.QPainterPath() + sat_val_path.addEllipse(sat_val_inner_radius_rect) + sat_val_path.addEllipse(sat_val_outer_radius_rect) + + painter.save() + painter.setClipPath(hue_path) + painter.fillRect(self.bg_image.rect(), hue_gradient) + painter.restore() + + painter.save() + painter.setClipPath(sat_val_path) + painter.fillRect(self.bg_image.rect(), sat_val_gradient) + painter.restore() + + painter.end() + + @staticmethod + def vlen(x, y): + return sqrt((x ** 2) + (y ** 2)) + + @staticmethod + def vprod(x1, y1, x2, y2): + return x1 * x2 + y1 * y2 + + @staticmethod + def _angle_between_angles(p, a1, a2): + if a1 > a2: + a2 += TWOPI + if p < PI: + p += TWOPI + + return p >= a1 and p < a2 + + @staticmethod + def _point_above_point(x, y, px, py, ax, ay, bx, by): + floored_ax = floor(ax) + floored_bx = floor(bx) + floored_ay = floor(ay) + floored_by = floor(by) + + if floored_ax == floored_bx: + # line is vertical + if floored_ay < floored_by: + return x < ax + elif floored_ay > floored_by: + return x > ax + return not (x == ax and y == ay) + + if floored_ax > floored_bx: + if floored_ay < floored_by: + # line is draw upright-to-downleft + return (floor(x) < floor(px) or floor(y) < floor(py)) + elif floored_ay > floored_by: + # line is draw downright-to-upleft + return (floor(x) > floor(px) or floor(y) < floor(py)) + # line is flat horizontal + return y < ay + + if floored_ay < floored_by: + # line is draw upleft-to-downright + return (floor(x) < floor(px) or floor(y) > floor(py)) + elif floored_ay > floored_by: + # line is draw downleft-to-upright + return (floor(x) > floor(px) or floor(y) > floor(py)) + # line is flat horizontal + return y > ay + + @staticmethod + def _point_in_line(x, y, ax, ay, bx, by): + if ax > bx: + if ay < by: + # line is draw upright-to-downleft + + # if (x,y) is in on or above the upper right point, + # return -1. + if y <= ay and x >= ax: + return -1 + + # if (x,y) is in on or below the lower left point, + # return 1. + if y >= by and x <= bx: + return 1 + else: + # line is draw downright-to-upleft + + # If the line is flat, only use the x coordinate. + if floor(ay) == floor(by): + # if (x is to the right of the rightmost point, + # return -1. otherwise if x is to the left of the + # leftmost point, return 1. + if x >= ax: + return -1 + elif x <= bx: + return 1 + else: + # if (x,y) is on or below the lower right point, + # return -1. + if y >= ay and x >= ax: + return -1 + + # if (x,y) is on or above the upper left point, return 1. + if y <= by and x <= bx: + return 1 + else: + if ay < by: + # line is draw upleft-to-downright + + # If (x,y) is on or above the upper left point, return -1. + if y <= ay and x <= ax: + return -1 + + # If (x,y) is on or below the lower right point, return 1. + if y >= by and x >= bx: + return 1 + else: + # line is draw downleft-to-upright + + # If the line is flat, only use the x coordinate. + if floor(ay) == floor(by): + if x <= ax: + return -1 + elif x >= bx: + return 1 + else: + # If (x,y) is on or below the lower left point, return -1. + if y >= ay and x <= ax: + return -1 + + # If (x,y) is on or above the upper right point, return 1. + if y <= by and x >= bx: + return 1 + + # No tests proved that (x,y) was outside [(ax,ay),(bx,by)], so we + # assume it's inside the line's bounds. + return 0 + + def _move_point_to_triangle(self, x, y, a, b, c): + # Let v1A be the vector from (x,y) to a. + # Let v2A be the vector from a to b. + # Find the angle alphaA between v1A and v2A. + v1xA = x - a.point.x() + v1yA = y - a.point.y() + v2xA = b.point.x() - a.point.x() + v2yA = b.point.y() - a.point.y() + vpA = self.vprod(v1xA, v1yA, v2xA, v2yA) + cosA = vpA / (self.vlen(v1xA, v1yA) * self.vlen(v2xA, v2yA)) + alphaA = acos(cosA) + + # Let v1B be the vector from x to b. + # Let v2B be the vector from b to c. + v1xB = x - b.point.x() + v1yB = y - b.point.y() + v2xB = c.point.x() - b.point.x() + v2yB = c.point.y() - b.point.y() + vpB = self.vprod(v1xB, v1yB, v2xB, v2yB) + cosB = vpB / (self.vlen(v1xB, v1yB) * self.vlen(v2xB, v2yB)) + alphaB = acos(cosB) + + # Let v1C be the vector from x to c. + # Let v2C be the vector from c back to a. + v1xC = x - c.point.x() + v1yC = y - c.point.y() + v2xC = a.point.x() - c.point.x() + v2yC = a.point.y() - c.point.y() + vpC = self.vprod(v1xC, v1yC, v2xC, v2yC) + cosC = vpC / (self.vlen(v1xC, v1yC) * self.vlen(v2xC, v2yC)) + alphaC = acos(cosC) + + # Find the radian angles between the (1,0) vector and the points + # A, B, C and (x,y). Use this information to determine which of + # the edges we should project (x,y) onto. + angleA = self._angle_at(a.point, self.contentsRect()) + angleB = self._angle_at(b.point, self.contentsRect()) + angleC = self._angle_at(c.point, self.contentsRect()) + angleP = self._angle_at(QtCore.QPointF(x, y), self.contentsRect()) + + # If (x,y) is in the a-b area, project onto the a-b vector. + if self._angle_between_angles(angleP, angleA, angleB): + # Find the distance from (x,y) to a. Then use the slope of + # the a-b vector with this distance and the angle between a-b + # and a-(x,y) to determine the point of intersection of the + # perpendicular projection from (x,y) onto a-b. + pdist = sqrt( + ((x - a.point.x()) ** 2) + ((y - a.point.y()) ** 2) + ) + + # the length of all edges is always > 0 + p0x = ( + a.point.x() + + ((b.point.x() - a.point.x()) / self.vlen(v2xB, v2yB)) + * cos(alphaA) * pdist + ) + p0y = ( + a.point.y() + + ((b.point.y() - a.point.y()) / self.vlen(v2xB, v2yB)) + * cos(alphaA) * pdist + ) + + # If (x,y) is above the a-b line, which basically means it's + # outside the triangle, then return its projection onto a-b. + if self._point_above_point( + x, y, + p0x, p0y, + a.point.x(), a.point.y(), + b.point.x(), b.point.y() + ): + # If the projection is "outside" a, return a. If it is + # outside b, return b. Otherwise return the projection. + n = self._point_in_line( + p0x, p0y, + a.point.x(), a.point.y(), + b.point.x(), b.point.y() + ) + if n < 0: + return a.point + elif n > 0: + return b.point + + return QtCore.QPointF(p0x, p0y) + + elif self._angle_between_angles(angleP, angleB, angleC): + # If (x,y) is in the b-c area, project onto the b-c vector. + pdist = sqrt( + ((x - b.point.x()) ** 2) + ((y - b.point.y()) ** 2) + ) + + # the length of all edges is always > 0 + p0x = ( + b.point.x() + + ((c.point.x() - b.point.x()) / self.vlen(v2xC, v2yC)) + * cos(alphaB) * pdist + ) + p0y = ( + b.point.y() + + ((c.point.y() - b.point.y()) / self.vlen(v2xC, v2yC)) + * cos(alphaB) + * pdist + ) + + if self._point_above_point( + x, y, + p0x, p0y, + b.point.x(), b.point.y(), + c.point.x(), c.point.y() + ): + n = self._point_in_line( + p0x, p0y, + b.point.x(), b.point.y(), + c.point.x(), c.point.y() + ) + if n < 0: + return b.point + elif n > 0: + return c.point + return QtCore.QPointF(p0x, p0y) + + elif self._angle_between_angles(angleP, angleC, angleA): + # If (x,y) is in the c-a area, project onto the c-a vector. + pdist = sqrt( + ((x - c.point.x()) ** 2) + ((y - c.point.y()) ** 2) + ) + + # the length of all edges is always > 0 + p0x = ( + c.point.x() + + ((a.point.x() - c.point.x()) / self.vlen(v2xA, v2yA)) + * cos(alphaC) + * pdist + ) + p0y = ( + c.point.y() + + ((a.point.y() - c.point.y()) / self.vlen(v2xA, v2yA)) + * cos(alphaC) * pdist + ) + + if self._point_above_point( + x, y, + p0x, p0y, + c.point.x(), c.point.y(), + a.point.x(), a.point.y() + ): + n = self._point_in_line( + p0x, p0y, + c.point.x(), c.point.y(), + a.point.x(), a.point.y() + ) + if n < 0: + return c.point + elif n > 0: + return a.point + return QtCore.QPointF(p0x, p0y) + + # (x,y) is inside the triangle (inside a-b, b-c and a-c). + return QtCore.QPointF(x, y) diff --git a/openpype/widgets/color_widgets/color_view.py b/openpype/widgets/color_widgets/color_view.py new file mode 100644 index 0000000000..a4393a6625 --- /dev/null +++ b/openpype/widgets/color_widgets/color_view.py @@ -0,0 +1,78 @@ +from Qt import QtWidgets, QtCore, QtGui + + +class ColorViewer(QtWidgets.QWidget): + def __init__(self, parent=None): + super(ColorViewer, self).__init__(parent) + + self.setMinimumSize(10, 10) + + self.alpha = 255 + self.actual_pen = QtGui.QPen() + self.actual_color = QtGui.QColor() + self._checkerboard = None + + def checkerboard(self): + if not self._checkerboard: + checkboard_piece_size = 10 + color_1 = QtGui.QColor(188, 188, 188) + color_2 = QtGui.QColor(90, 90, 90) + + pix = QtGui.QPixmap( + checkboard_piece_size * 2, + checkboard_piece_size * 2 + ) + pix_painter = QtGui.QPainter(pix) + + rect = QtCore.QRect( + 0, 0, checkboard_piece_size, checkboard_piece_size + ) + pix_painter.fillRect(rect, color_1) + rect.moveTo(checkboard_piece_size, checkboard_piece_size) + pix_painter.fillRect(rect, color_1) + rect.moveTo(checkboard_piece_size, 0) + pix_painter.fillRect(rect, color_2) + rect.moveTo(0, checkboard_piece_size) + pix_painter.fillRect(rect, color_2) + pix_painter.end() + self._checkerboard = pix + + return self._checkerboard + + def color(self): + return self.actual_color + + def set_color(self, color): + if color == self.actual_color: + return + + # Create copy of entered color + self.actual_color = QtGui.QColor(color) + # Set alpha by current alpha value + self.actual_color.setAlpha(self.alpha) + # Repaint + self.update() + + def set_alpha(self, alpha): + if alpha == self.alpha: + return + # Change alpha of current color + self.actual_color.setAlpha(alpha) + # Store the value + self.alpha = alpha + # Repaint + self.update() + + def paintEvent(self, event): + rect = event.rect() + + # Paint everything to pixmap as it has transparency + pix = QtGui.QPixmap(rect.width(), rect.height()) + pix_painter = QtGui.QPainter(pix) + pix_painter.drawTiledPixmap(rect, self.checkerboard()) + pix_painter.fillRect(rect, self.actual_color) + pix_painter.end() + + painter = QtGui.QPainter(self) + painter.drawPixmap(rect, pix) + painter.end() From ac3fbcb7ce1c2ab34ef30f80f44a61acfb922b2f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 17:26:19 +0200 Subject: [PATCH 26/68] base implementation of ColorEntity view --- .../tools/settings/settings/categories.py | 6 +- .../tools/settings/settings/color_widget.py | 174 ++++++++++++++++++ 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 openpype/tools/settings/settings/color_widget.py diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 4762aa4b6b..01d4babd0f 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -21,6 +21,7 @@ from openpype.settings.entities import ( TextEntity, PathInput, RawJsonEntity, + ColorEntity, DefaultsNotDefined, StudioDefaultsNotDefined, @@ -44,7 +45,7 @@ from .item_widgets import ( PathWidget, PathInputWidget ) - +from .color_widget import ColorWidget from avalon.vendor import qtawesome @@ -113,6 +114,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget): elif isinstance(entity, RawJsonEntity): return RawJsonWidget(*args) + elif isinstance(entity, ColorEntity): + return ColorWidget(*args) + elif isinstance(entity, BaseEnumEntity): return EnumeratorWidget(*args) diff --git a/openpype/tools/settings/settings/color_widget.py b/openpype/tools/settings/settings/color_widget.py new file mode 100644 index 0000000000..54545d7450 --- /dev/null +++ b/openpype/tools/settings/settings/color_widget.py @@ -0,0 +1,174 @@ +from Qt import QtWidgets, QtCore, QtGui + +from .item_widgets import InputWidget + +from openpype.widgets.color_widgets import ColorPickerWidget + + +class ColorWidget(InputWidget): + def _add_inputs_to_layout(self): + self.input_field = ColorViewer(self.content_widget) + + self.setFocusProxy(self.input_field) + + self.content_layout.addWidget(self.input_field, 1) + + self.input_field.clicked.connect(self._on_click) + + self._dialog = None + + def _on_click(self): + if self._dialog: + self._dialog.open() + return + + dialog = ColorDialog(self.input_field.color(), self) + self._dialog = dialog + + dialog.open() + dialog.finished.connect(self._on_dialog_finish) + + def _on_dialog_finish(self, *_args): + if not self._dialog: + return + + color = self._dialog.result() + if color is not None: + self.input_field.set_color(color) + self._on_value_change() + + self._dialog.deleteLater() + self._dialog = None + + def _on_entity_change(self): + if self.entity.value != self.input_value(): + self.set_entity_value() + + def set_entity_value(self): + self.input_field.set_color(*self.entity.value) + + def input_value(self): + color = self.input_field.color() + return [color.red(), color.green(), color.blue(), color.alpha()] + + def _on_value_change(self): + if self.ignore_input_changes: + return + + self.entity.set(self.input_value()) + + +class ColorViewer(QtWidgets.QWidget): + clicked = QtCore.Signal() + + def __init__(self, parent=None): + super(ColorViewer, self).__init__(parent) + + self.setMinimumSize(10, 10) + + self.actual_pen = QtGui.QPen() + self.actual_color = QtGui.QColor() + self._checkerboard = None + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.clicked.emit() + super(ColorViewer, self).mouseReleaseEvent(event) + + def checkerboard(self): + if not self._checkerboard: + checkboard_piece_size = 10 + color_1 = QtGui.QColor(188, 188, 188) + color_2 = QtGui.QColor(90, 90, 90) + + pix = QtGui.QPixmap( + checkboard_piece_size * 2, + checkboard_piece_size * 2 + ) + pix_painter = QtGui.QPainter(pix) + + rect = QtCore.QRect( + 0, 0, checkboard_piece_size, checkboard_piece_size + ) + pix_painter.fillRect(rect, color_1) + rect.moveTo(checkboard_piece_size, checkboard_piece_size) + pix_painter.fillRect(rect, color_1) + rect.moveTo(checkboard_piece_size, 0) + pix_painter.fillRect(rect, color_2) + rect.moveTo(0, checkboard_piece_size) + pix_painter.fillRect(rect, color_2) + pix_painter.end() + self._checkerboard = pix + + return self._checkerboard + + def color(self): + return self.actual_color + + def set_color(self, *args): + # Create copy of entered color + self.actual_color = QtGui.QColor(*args) + # Repaint + self.update() + + def set_alpha(self, alpha): + # Change alpha of current color + self.actual_color.setAlpha(alpha) + # Repaint + self.update() + + def paintEvent(self, event): + rect = event.rect() + + # Paint everything to pixmap as it has transparency + pix = QtGui.QPixmap(rect.width(), rect.height()) + pix_painter = QtGui.QPainter(pix) + pix_painter.drawTiledPixmap(rect, self.checkerboard()) + pix_painter.fillRect(rect, self.actual_color) + pix_painter.end() + + painter = QtGui.QPainter(self) + painter.drawPixmap(rect, pix) + painter.end() + + +class ColorDialog(QtWidgets.QDialog): + def __init__(self, color=None, parent=None): + super(ColorDialog, self).__init__(parent) + + self.setWindowTitle("Color picker dialog") + + picker_widget = ColorPickerWidget(color, self) + + footer_widget = QtWidgets.QWidget(self) + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + + ok_btn = QtWidgets.QPushButton("Ok", footer_widget) + cancel_btn = QtWidgets.QPushButton("Cancel", footer_widget) + + footer_layout.addWidget(ok_btn) + footer_layout.addWidget(cancel_btn) + footer_layout.addWidget(QtWidgets.QWidget(self), 1) + + layout = QtWidgets.QVBoxLayout(self) + + layout.addWidget(picker_widget, 1) + layout.addWidget(footer_widget, 0) + + ok_btn.clicked.connect(self.on_ok_clicked) + cancel_btn.clicked.connect(self.on_cancel_clicked) + + self.picker_widget = picker_widget + + self._result = None + + def on_ok_clicked(self): + self._result = self.picker_widget.color() + self.close() + + def on_cancel_clicked(self): + self._result = None + self.close() + + def result(self): + return self._result From 7c1e8f47b46091d2b1e617652cf5b92a593f7fae Mon Sep 17 00:00:00 2001 From: jezscha Date: Wed, 19 May 2021 15:37:47 +0000 Subject: [PATCH 27/68] Create draft PR for #1538 From 655063e9f60dabdfd619875131f3af2c91f16a44 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 19 May 2021 17:42:22 +0200 Subject: [PATCH 28/68] PS - fix hardcoding 'image' family into subset name --- .../photoshop/plugins/create/create_image.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 1df8502959..c0549106c0 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -16,7 +16,9 @@ class CreateImage(openpype.api.Creator): create_group = False stub = photoshop.stub() + useSelection = False if (self.options or {}).get("useSelection"): + useSelection = True multiple_instances = False selection = stub.get_selected_layers() self.log.info("selection {}".format(selection)) @@ -61,7 +63,9 @@ class CreateImage(openpype.api.Creator): # No selection creates an empty group. create_group = True else: - create_group = True + stub.select_layers(stub.get_layers()) + group = stub.group_selected_layers(self.name) + groups.append(group) if create_group: group = stub.create_group(self.name) @@ -77,13 +81,20 @@ class CreateImage(openpype.api.Creator): group.name = group.name.replace(stub.PUBLISH_ICON, ''). \ replace(stub.LOADED_ICON, '') + if useSelection: + clean_subset_name = self.data["subset"].replace("Default", '') + subset_name = clean_subset_name + group.name + else: + # use value provided by user from Creator + subset_name = self.data["subset"] + if group.long_name: for directory in group.long_name[::-1]: name = directory.replace(stub.PUBLISH_ICON, '').\ replace(stub.LOADED_ICON, '') long_names.append(name) - self.data.update({"subset": "image" + group.name}) + self.data.update({"subset": subset_name}) self.data.update({"uuid": str(group.id)}) self.data.update({"long_name": "_".join(long_names)}) stub.imprint(group, self.data) From 77295b0d7e014c3ee8cc115db753e58cb5af076a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 19 May 2021 17:44:23 +0200 Subject: [PATCH 29/68] PS - ExtractReview fill produce flattened image if no instances created --- .../plugins/publish/extract_review.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index 3b6d8ef951..b52078fd5f 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -6,7 +6,12 @@ from avalon import photoshop class ExtractReview(openpype.api.Extractor): - """Produce a flattened image file from all instances.""" + """ + Produce a flattened image file from all 'image' instances. + + If no 'image' instance is created, it produces flattened image from + all visible layers. + """ label = "Extract Review" hosts = ["photoshop"] @@ -30,14 +35,15 @@ class ExtractReview(openpype.api.Extractor): ) output_image_path = os.path.join(staging_dir, output_image) with photoshop.maintained_visibility(): - # Hide all other layers. - extract_ids = set([ll.id for ll in stub. - get_layers_in_layers(layers)]) - self.log.info("extract_ids {}".format(extract_ids)) - for layer in stub.get_layers(): - # limit unnecessary calls to client - if layer.visible and layer.id not in extract_ids: - stub.set_visible(layer.id, False) + if layers: + # Hide all other layers. + extract_ids = set([ll.id for ll in stub. + get_layers_in_layers(layers)]) + self.log.debug("extract_ids {}".format(extract_ids)) + for layer in stub.get_layers(): + # limit unnecessary calls to client + if layer.visible and layer.id not in extract_ids: + stub.set_visible(layer.id, False) stub.saveAs(output_image_path, 'jpg', True) From 589dcfd6d0e1e311e240d631e2753532ce2ef10a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 17:56:00 +0200 Subject: [PATCH 30/68] Converted inputs to widgets --- .../widgets/color_widgets/color_inputs.py | 79 ++++++++++++------- 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index ddf8aebd4e..a4409988b2 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -25,11 +25,11 @@ QSlider::handle:horizontal:hover { }""" -class AlphaInputs(QtWidgets.QGroupBox): +class AlphaInputs(QtWidgets.QWidget): alpha_changed = QtCore.Signal(int) def __init__(self, parent=None): - super(AlphaInputs, self).__init__("Alpha", parent) + super(AlphaInputs, self).__init__(parent) self._block_changes = False self.alpha_value = None @@ -47,11 +47,13 @@ class AlphaInputs(QtWidgets.QGroupBox): inputs_layout.setContentsMargins(0, 0, 0, 0) percent_input = QtWidgets.QDoubleSpinBox(self) + percent_input.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) percent_input.setMinimum(0) percent_input.setMaximum(100) percent_input.setDecimals(2) int_input = QtWidgets.QSpinBox(self) + int_input.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) int_input.setMinimum(0) int_input.setMaximum(255) @@ -61,6 +63,7 @@ class AlphaInputs(QtWidgets.QGroupBox): inputs_layout.addWidget(QtWidgets.QLabel("%")) layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(QtWidgets.QLabel("Alpha", self)) layout.addWidget(alpha_slider) layout.addWidget(inputs_widget) @@ -119,11 +122,11 @@ class AlphaInputs(QtWidgets.QGroupBox): self._block_changes = False -class RGBInputs(QtWidgets.QGroupBox): +class RGBInputs(QtWidgets.QWidget): value_changed = QtCore.Signal() def __init__(self, color, parent=None): - super(RGBInputs, self).__init__("RGB", parent) + super(RGBInputs, self).__init__(parent) self._block_changes = False @@ -133,6 +136,10 @@ class RGBInputs(QtWidgets.QGroupBox): input_green = QtWidgets.QSpinBox(self) input_blue = QtWidgets.QSpinBox(self) + input_red.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_green.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_blue.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_red.setMinimum(0) input_green.setMinimum(0) input_blue.setMinimum(0) @@ -142,9 +149,10 @@ class RGBInputs(QtWidgets.QGroupBox): input_blue.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(input_red) - layout.addWidget(input_green) - layout.addWidget(input_blue) + layout.addWidget(QtWidgets.QLabel("RGB", self), 0) + layout.addWidget(input_red, 1) + layout.addWidget(input_green, 1) + layout.addWidget(input_blue, 1) input_red.valueChanged.connect(self._on_red_change) input_green.valueChanged.connect(self._on_green_change) @@ -192,11 +200,11 @@ class RGBInputs(QtWidgets.QGroupBox): self._block_changes = False -class CMYKInputs(QtWidgets.QGroupBox): +class CMYKInputs(QtWidgets.QWidget): value_changed = QtCore.Signal() def __init__(self, color, parent=None): - super(CMYKInputs, self).__init__("CMYK", parent) + super(CMYKInputs, self).__init__(parent) self.color = color @@ -207,6 +215,11 @@ class CMYKInputs(QtWidgets.QGroupBox): input_yellow = QtWidgets.QSpinBox(self) input_black = QtWidgets.QSpinBox(self) + input_cyan.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_magenta.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_yellow.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_black.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_cyan.setMinimum(0) input_magenta.setMinimum(0) input_yellow.setMinimum(0) @@ -218,10 +231,11 @@ class CMYKInputs(QtWidgets.QGroupBox): input_black.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(input_cyan) - layout.addWidget(input_magenta) - layout.addWidget(input_yellow) - layout.addWidget(input_black) + layout.addWidget(QtWidgets.QLabel("CMYK", self)) + layout.addWidget(input_cyan, 1) + layout.addWidget(input_magenta, 1) + layout.addWidget(input_yellow, 1) + layout.addWidget(input_black, 1) input_cyan.valueChanged.connect(self._on_change) input_magenta.valueChanged.connect(self._on_change) @@ -272,18 +286,19 @@ class CMYKInputs(QtWidgets.QGroupBox): self._block_changes = False -class HEXInputs(QtWidgets.QGroupBox): +class HEXInputs(QtWidgets.QWidget): hex_regex = re.compile("^#(([0-9a-fA-F]{2}){3}|([0-9a-fA-F]){3})$") value_changed = QtCore.Signal() def __init__(self, color, parent=None): - super(HEXInputs, self).__init__("HEX", parent) + super(HEXInputs, self).__init__(parent) self.color = color - input_field = QtWidgets.QLineEdit() + input_field = QtWidgets.QLineEdit(self) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(input_field) + layout.addWidget(QtWidgets.QLabel("HEX", self), 0) + layout.addWidget(input_field, 1) input_field.textChanged.connect(self._on_change) @@ -316,11 +331,11 @@ class HEXInputs(QtWidgets.QGroupBox): self._block_changes = False -class HSVInputs(QtWidgets.QGroupBox): +class HSVInputs(QtWidgets.QWidget): value_changed = QtCore.Signal() def __init__(self, color, parent=None): - super(HSVInputs, self).__init__("HSV", parent) + super(HSVInputs, self).__init__(parent) self._block_changes = False @@ -330,6 +345,10 @@ class HSVInputs(QtWidgets.QGroupBox): input_sat = QtWidgets.QSpinBox(self) input_val = QtWidgets.QSpinBox(self) + input_hue.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_sat.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_val.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_hue.setMinimum(0) input_sat.setMinimum(0) input_val.setMinimum(0) @@ -339,9 +358,10 @@ class HSVInputs(QtWidgets.QGroupBox): input_val.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(input_hue) - layout.addWidget(input_sat) - layout.addWidget(input_val) + layout.addWidget(QtWidgets.QLabel("HSV", self), 0) + layout.addWidget(input_hue, 1) + layout.addWidget(input_sat, 1) + layout.addWidget(input_val, 1) input_hue.valueChanged.connect(self._on_change) input_sat.valueChanged.connect(self._on_change) @@ -385,11 +405,11 @@ class HSVInputs(QtWidgets.QGroupBox): self._block_changes = False -class HSLInputs(QtWidgets.QGroupBox): +class HSLInputs(QtWidgets.QWidget): value_changed = QtCore.Signal() def __init__(self, color, parent=None): - super(HSLInputs, self).__init__("HSL", parent) + super(HSLInputs, self).__init__(parent) self._block_changes = False @@ -399,6 +419,10 @@ class HSLInputs(QtWidgets.QGroupBox): input_sat = QtWidgets.QSpinBox(self) input_light = QtWidgets.QSpinBox(self) + input_hue.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_sat.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_light.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + input_hue.setMinimum(0) input_sat.setMinimum(0) input_light.setMinimum(0) @@ -408,9 +432,10 @@ class HSLInputs(QtWidgets.QGroupBox): input_light.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(input_hue) - layout.addWidget(input_sat) - layout.addWidget(input_light) + layout.addWidget(QtWidgets.QLabel("HSL", self), 0) + layout.addWidget(input_hue, 1) + layout.addWidget(input_sat, 1) + layout.addWidget(input_light, 1) input_hue.valueChanged.connect(self._on_change) input_sat.valueChanged.connect(self._on_change) From ed6848d9d02e76d3bd05d6f5817eb6c8bce5939a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 18:02:12 +0200 Subject: [PATCH 31/68] used color type at 2 places --- .../schemas/schema_global_publish.json | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 0efe3b8fea..426da4b71e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -228,14 +228,9 @@ ] }, { - "type": "schema_template", - "name": "template_rgba_color", - "template_data": [ - { - "label": "Fill Color", - "name": "fill_color" - } - ] + "type": "color", + "label": "Fill Color", + "key": "fill_color" }, { "key": "line_thickness", @@ -245,14 +240,9 @@ "maximum": 1000 }, { - "type": "schema_template", - "name": "template_rgba_color", - "template_data": [ - { - "label": "Line Color", - "name": "line_color" - } - ] + "type": "color", + "label": "Line Color", + "key": "line_color" } ] } From 3e1286531f27ab78441ba2f517e5f50da865bcea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 18:11:31 +0200 Subject: [PATCH 32/68] add empty line --- openpype/settings/entities/color_entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/entities/color_entity.py b/openpype/settings/entities/color_entity.py index 7d31ba42b9..7a1b1d9848 100644 --- a/openpype/settings/entities/color_entity.py +++ b/openpype/settings/entities/color_entity.py @@ -8,6 +8,7 @@ from .exceptions import ( class ColorEntity(InputEntity): schema_types = ["color"] + def _item_initalization(self): self.valid_value_types = (list, ) self.value_on_not_set = [0, 0, 0, 255] From 496e550d658c001409d5d977510c408d3702f390 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 May 2021 18:24:00 +0200 Subject: [PATCH 33/68] Nuke: creator was to setting correct settings --- openpype/hosts/nuke/api/lib.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index ea6476485b..bafffe36cb 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -55,9 +55,10 @@ def get_created_node_imageio_setting(**kwarg): log.info(node) if (node["nukeNodeClass"] != nodeclass) and ( creator not in node["plugins"]): - continue - - imageio_node = node + if (nodeclass in node["nukeNodeClass"]) and ( + creator in node["plugins"]): + imageio_node = node + break log.info("ImageIO node: {}".format(imageio_node)) return imageio_node @@ -340,9 +341,9 @@ def create_write_node(name, data, input=None, prenodes=None, review=True): nuke.message(msg) # build file path to workfiles - fpath = str(anatomy_filled["work"]["folder"]).replace("\\", "/") + fdir = str(anatomy_filled["work"]["folder"]).replace("\\", "/") fpath = data["fpath_template"].format( - work=fpath, version=data["version"], subset=data["subset"], + work=fdir, version=data["version"], subset=data["subset"], frame=data["frame"], ext=representation ) From 2861078620d62cca8fc6d24d72a3c60a05e66ec5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 18:24:13 +0200 Subject: [PATCH 34/68] added small info about color entity --- openpype/settings/entities/schemas/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 18312a8364..6c31b61f59 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -420,6 +420,18 @@ } ``` +### color +- preimplemented entity to store and load color values +- entity store and expect list of 4 integers in range 0-255 + - integers represents rgba [Red, Green, Blue, Alpha] + +``` +{ + "type": "color", + "key": "bg_color", + "label": "Background Color" +} +``` ## Noninteractive widgets - have nothing to do with data From ac2e759afc45e1347bc85994a10f2e33670447e7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 18:24:34 +0200 Subject: [PATCH 35/68] added color entity to examples --- .../entities/schemas/system_schema/example_schema.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/example_schema.json b/openpype/settings/entities/schemas/system_schema/example_schema.json index 48a21cc0c6..a4ed56df32 100644 --- a/openpype/settings/entities/schemas/system_schema/example_schema.json +++ b/openpype/settings/entities/schemas/system_schema/example_schema.json @@ -4,6 +4,11 @@ "type": "dict", "is_file": true, "children": [ + { + "key": "color", + "label": "Color input", + "type": "color" + }, { "type": "dict", "key": "schema_template_exaples", From a4b1a90e3118be0621f86edf7ded43425749047b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 19 May 2021 18:25:16 +0200 Subject: [PATCH 36/68] hound: suggestion --- openpype/hosts/nuke/api/lib.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index bafffe36cb..e6dde813a7 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -53,8 +53,6 @@ def get_created_node_imageio_setting(**kwarg): imageio_node = None for node in imageio_nodes: log.info(node) - if (node["nukeNodeClass"] != nodeclass) and ( - creator not in node["plugins"]): if (nodeclass in node["nukeNodeClass"]) and ( creator in node["plugins"]): imageio_node = node From f8936f5e4038f2516363c645fd04182ec309c9d8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 18:34:29 +0200 Subject: [PATCH 37/68] replaced pick color text with an icon --- .../widgets/color_widgets/color_picker_widget.py | 9 +++++++-- openpype/widgets/color_widgets/eyedropper.png | Bin 0 -> 3178 bytes 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 openpype/widgets/color_widgets/eyedropper.png diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index d06af73cbf..5786c529ed 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -1,3 +1,4 @@ +import os from Qt import QtWidgets, QtCore, QtGui from .color_triangle import QtColorTriangle @@ -32,9 +33,13 @@ class ColorPickerWidget(QtWidgets.QWidget): color_view.setMaximumHeight(50) # Color pick button - btn_pick_color = QtWidgets.QPushButton( - "Pick a color", bottom_utils_widget + btn_pick_color = QtWidgets.QPushButton(bottom_utils_widget) + icon_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "eyedropper.png" ) + btn_pick_color.setIcon(QtGui.QIcon(icon_path)) + btn_pick_color.setToolTip("Pick a color") # Color inputs widget color_inputs = ColorInputsWidget(self) diff --git a/openpype/widgets/color_widgets/eyedropper.png b/openpype/widgets/color_widgets/eyedropper.png new file mode 100644 index 0000000000000000000000000000000000000000..baf6209e0be3d0f32cf76aebbfc0ba5edef7dc3a GIT binary patch literal 3178 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 M`viEOSsR=CU*aw9$^ZZW literal 0 HcmV?d00001 From 101134bbdfdfe847e647f515296089d43c2e0b09 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 19 May 2021 18:37:20 +0200 Subject: [PATCH 38/68] moved with buttons --- openpype/tools/settings/settings/color_widget.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/settings/color_widget.py b/openpype/tools/settings/settings/color_widget.py index 54545d7450..7b534b749c 100644 --- a/openpype/tools/settings/settings/color_widget.py +++ b/openpype/tools/settings/settings/color_widget.py @@ -141,14 +141,15 @@ class ColorDialog(QtWidgets.QDialog): picker_widget = ColorPickerWidget(color, self) footer_widget = QtWidgets.QWidget(self) - footer_layout = QtWidgets.QHBoxLayout(footer_widget) ok_btn = QtWidgets.QPushButton("Ok", footer_widget) cancel_btn = QtWidgets.QPushButton("Cancel", footer_widget) + footer_layout = QtWidgets.QHBoxLayout(footer_widget) + footer_layout.setContentsMargins(0, 0, 0, 0) + footer_layout.addStretch(1) footer_layout.addWidget(ok_btn) footer_layout.addWidget(cancel_btn) - footer_layout.addWidget(QtWidgets.QWidget(self), 1) layout = QtWidgets.QVBoxLayout(self) From 60e3b1ee4745e853bb020ea92a3d1bf2ae83e7b9 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 19 May 2021 19:17:17 +0200 Subject: [PATCH 39/68] remove tvpaint families from settings --- .../defaults/project_settings/tvpaint.json | 16 ++-------------- .../projects_schema/schema_project_tvpaint.json | 12 ------------ 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index d7fc46763c..9d5b922b8e 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -22,26 +22,14 @@ "stretch": true, "timestretch": true, "preload": true - }, - "families": [ - "render", - "image", - "background", - "plate" - ] + } }, "ImportImage": { "defaults": { "stretch": true, "timestretch": true, "preload": true - }, - "families": [ - "render", - "image", - "background", - "plate" - ] + } } }, "filters": {} diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 903c5de842..3844d12e1b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -79,12 +79,6 @@ "label": "Preload" } ] - }, - { - "type": "list", - "key": "families", - "label": "Families", - "object_type": "text" } ] }, @@ -114,12 +108,6 @@ "label": "Preload" } ] - }, - { - "type": "list", - "key": "families", - "label": "Families", - "object_type": "text" } ] } From 6827f3283cce7a6979e7eb4d279450b683dd40a1 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 19 May 2021 21:02:24 +0200 Subject: [PATCH 40/68] don't remove Default from subset name --- openpype/hosts/photoshop/plugins/create/create_image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index c0549106c0..967a704ccf 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -9,6 +9,7 @@ class CreateImage(openpype.api.Creator): name = "imageDefault" label = "Image" family = "image" + defaults = ["Main"] def process(self): groups = [] @@ -82,8 +83,7 @@ class CreateImage(openpype.api.Creator): replace(stub.LOADED_ICON, '') if useSelection: - clean_subset_name = self.data["subset"].replace("Default", '') - subset_name = clean_subset_name + group.name + subset_name = self.data["subset"] + group.name else: # use value provided by user from Creator subset_name = self.data["subset"] From 0f6794e2baba21351da78786c22be5fc478adf3d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 19 May 2021 21:03:14 +0200 Subject: [PATCH 41/68] make extract formats configurable (within current possibilities) --- .../hosts/photoshop/plugins/publish/extract_image.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_image.py b/openpype/hosts/photoshop/plugins/publish/extract_image.py index b56f128831..87574d1269 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_image.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_image.py @@ -35,21 +35,16 @@ class ExtractImage(openpype.api.Extractor): if layer.visible and layer.id not in extract_ids: stub.set_visible(layer.id, False) - save_options = [] - if "png" in self.formats: - save_options.append('png') - if "jpg" in self.formats: - save_options.append('jpg') - file_basename = os.path.splitext( stub.get_active_document_name() )[0] - for extension in save_options: + for extension in self.formats: _filename = "{}.{}".format(file_basename, extension) files[extension] = _filename full_filename = os.path.join(staging_dir, _filename) stub.saveAs(full_filename, extension, True) + self.log.info(f"Extracted: {extension}") representations = [] for extension, filename in files.items(): From a2deaca8005d2523f9ab4e77db8b0f2af3be3cf0 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 19 May 2021 21:03:36 +0200 Subject: [PATCH 42/68] add photoshop create and publish options --- .../defaults/project_settings/photoshop.json | 17 ++++++ .../schemas/projects_schema/schema_main.json | 4 ++ .../schema_project_photoshop.json | 57 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 openpype/settings/defaults/project_settings/photoshop.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json new file mode 100644 index 0000000000..0db6e8248d --- /dev/null +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -0,0 +1,17 @@ +{ + "create": { + "CreateImage": { + "defaults": [ + "Main" + ] + } + }, + "publish": { + "ExtractImage": { + "formats": [ + "png", + "jpg" + ] + } + } +} \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json index b4666b302a..e77f13d351 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -82,6 +82,10 @@ "type": "schema", "name": "schema_project_aftereffects" }, + { + "type": "schema", + "name": "schema_project_photoshop" + }, { "type": "schema", "name": "schema_project_harmony" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json new file mode 100644 index 0000000000..3a20b4e79c --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -0,0 +1,57 @@ +{ + "type": "dict", + "collapsible": true, + "key": "photoshop", + "label": "Photoshop", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "create", + "label": "Creator plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CreateImage", + "label": "Create Image", + "children": [ + { + "type": "list", + "key": "defaults", + "label": "Default Subsets", + "object_type": "text" + } + ] + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "publish", + "label": "Publish plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "ExtractImage", + "label": "Extract Image", + "children": [ + { + "type": "label", + "label": "Currently only jpg and png are supported" + }, + { + "type": "list", + "key": "formats", + "label": "Extract Formats", + "object_type": "text" + } + ] + } + ] + } + ] +} From 8384f39656a08d2108eaa53226fc151128f36896 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 19 May 2021 22:20:44 +0200 Subject: [PATCH 43/68] wrap platform tabs in a coloured box --- website/docs/artist_install.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/docs/artist_install.md b/website/docs/artist_install.md index 94a8bcdfe1..6fb5abe9e2 100644 --- a/website/docs/artist_install.md +++ b/website/docs/artist_install.md @@ -16,6 +16,7 @@ OpenPype comes in packages for Windows (10 or Server), Mac OS X (Mojave or highe To install OpenPype you will need administrator permissions. ::: +:::note pick your platform +::: ## Run OpenPype From 9ff9aab0292fd5defb6d49134a730ea5f6bc80ce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 12:09:15 +0200 Subject: [PATCH 44/68] implemented alpha slider that can move pointer on click --- .../widgets/color_widgets/color_inputs.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index a4409988b2..b8737afd99 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -25,6 +25,40 @@ QSlider::handle:horizontal:hover { }""" +class AlphaSlider(QtWidgets.QSlider): + def __init__(self, *args, **kwargs): + super(AlphaSlider, self).__init__(*args, **kwargs) + self._mouse_clicked = False + self.setSingleStep(1) + self.setMinimum(0) + self.setMaximum(255) + self.setValue(255) + + self.setStyleSheet(slide_style) + + def mousePressEvent(self, event): + self._mouse_clicked = True + if event.button() == QtCore.Qt.LeftButton: + self._set_value_to_pos(event.pos().x()) + return event.accept() + return super(AlphaSlider, self).mousePressEvent(event) + + def _set_value_to_pos(self, pos_x): + value = ( + self.maximum() - self.minimum() + ) * pos_x / self.width() + self.minimum() + self.setValue(value) + + def mouseMoveEvent(self, event): + if self._mouse_clicked: + self._set_value_to_pos(event.pos().x()) + super(AlphaSlider, self).mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + self._mouse_clicked = True + super(AlphaSlider, self).mouseReleaseEvent(event) + + class AlphaInputs(QtWidgets.QWidget): alpha_changed = QtCore.Signal(int) From ce6c129f9ea27002d9d7b4d4a1a120776b61120a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 12:18:30 +0200 Subject: [PATCH 45/68] removed labels from input widgets and set content's margines --- openpype/widgets/color_widgets/color_inputs.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index b8737afd99..f19e28ba1d 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -183,7 +183,7 @@ class RGBInputs(QtWidgets.QWidget): input_blue.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("RGB", self), 0) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(input_red, 1) layout.addWidget(input_green, 1) layout.addWidget(input_blue, 1) @@ -265,7 +265,7 @@ class CMYKInputs(QtWidgets.QWidget): input_black.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("CMYK", self)) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(input_cyan, 1) layout.addWidget(input_magenta, 1) layout.addWidget(input_yellow, 1) @@ -331,7 +331,7 @@ class HEXInputs(QtWidgets.QWidget): input_field = QtWidgets.QLineEdit(self) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("HEX", self), 0) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(input_field, 1) input_field.textChanged.connect(self._on_change) @@ -392,7 +392,7 @@ class HSVInputs(QtWidgets.QWidget): input_val.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("HSV", self), 0) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(input_hue, 1) layout.addWidget(input_sat, 1) layout.addWidget(input_val, 1) @@ -466,7 +466,7 @@ class HSLInputs(QtWidgets.QWidget): input_light.setMaximum(255) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("HSL", self), 0) + layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(input_hue, 1) layout.addWidget(input_sat, 1) layout.addWidget(input_light, 1) From b9b18f34af3eef3d93dfb3f7e75b85385af8797b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 12:19:18 +0200 Subject: [PATCH 46/68] removed alpha slider from alpha inputs --- .../widgets/color_widgets/color_inputs.py | 40 +++---------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index f19e28ba1d..9aa35021c2 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -68,18 +68,6 @@ class AlphaInputs(QtWidgets.QWidget): self._block_changes = False self.alpha_value = None - # Opacity slider - alpha_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal, self) - alpha_slider.setSingleStep(1) - alpha_slider.setMinimum(0) - alpha_slider.setMaximum(255) - alpha_slider.setStyleSheet(slide_style) - alpha_slider.setValue(255) - - inputs_widget = QtWidgets.QWidget(self) - inputs_layout = QtWidgets.QHBoxLayout(inputs_widget) - inputs_layout.setContentsMargins(0, 0, 0, 0) - percent_input = QtWidgets.QDoubleSpinBox(self) percent_input.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) percent_input.setMinimum(0) @@ -91,21 +79,16 @@ class AlphaInputs(QtWidgets.QWidget): int_input.setMinimum(0) int_input.setMaximum(255) - inputs_layout.addWidget(int_input) - inputs_layout.addWidget(QtWidgets.QLabel("0-255")) - inputs_layout.addWidget(percent_input) - inputs_layout.addWidget(QtWidgets.QLabel("%")) + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(int_input) + layout.addWidget(QtWidgets.QLabel("0-255")) + layout.addWidget(percent_input) + layout.addWidget(QtWidgets.QLabel("%")) - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(QtWidgets.QLabel("Alpha", self)) - layout.addWidget(alpha_slider) - layout.addWidget(inputs_widget) - - alpha_slider.valueChanged.connect(self._on_slider_change) percent_input.valueChanged.connect(self._on_percent_change) int_input.valueChanged.connect(self._on_int_change) - self.alpha_slider = alpha_slider self.percent_input = percent_input self.int_input = int_input @@ -118,13 +101,6 @@ class AlphaInputs(QtWidgets.QWidget): self.update_alpha() - def _on_slider_change(self): - if self._block_changes: - return - self.alpha_value = self.alpha_slider.value() - self.alpha_changed.emit(self.alpha_value) - self.update_alpha() - def _on_percent_change(self): if self._block_changes: return @@ -142,10 +118,6 @@ class AlphaInputs(QtWidgets.QWidget): def update_alpha(self): self._block_changes = True - - if self.alpha_slider.value() != self.alpha_value: - self.alpha_slider.setValue(self.alpha_value) - if self.int_input.value() != self.alpha_value: self.int_input.setValue(self.alpha_value) From cbed588a75b8682f0ab3cbc9e4956df6dc29ec12 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 12:28:28 +0200 Subject: [PATCH 47/68] formatting changes --- openpype/widgets/color_widgets/color_inputs.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index 9aa35021c2..ddb832c655 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -4,13 +4,17 @@ from Qt import QtWidgets, QtCore, QtGui slide_style = """ QSlider::groove:horizontal { - background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #000, stop: 1 #fff); + background: qlineargradient( + x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #000, stop: 1 #fff + ); height: 8px; border-radius: 4px; } QSlider::handle:horizontal { - background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #ddd, stop:1 #bbb); + background: qlineargradient( + x1:0, y1:0, x2:1, y2:1, stop:0 #ddd, stop:1 #bbb + ); border: 1px solid #777; width: 8px; margin-top: -1px; @@ -19,7 +23,9 @@ QSlider::handle:horizontal { } QSlider::handle:horizontal:hover { - background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #eee, stop:1 #ddd); + background: qlineargradient( + x1:0, y1:0, x2:1, y2:1, stop:0 #eee, stop:1 #ddd + ); border: 1px solid #444;ff border-radius: 4px; }""" From 9f86bcff621298f82bdb11eda7d904ad3ada53ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 12:29:13 +0200 Subject: [PATCH 48/68] inputs are handled inside ColorPickerWidget --- .../color_widgets/color_picker_widget.py | 141 ++++++++++++------ 1 file changed, 93 insertions(+), 48 deletions(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 5786c529ed..7158a426a4 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -6,7 +6,13 @@ from .color_view import ColorViewer from .color_screen_pick import PickScreenColorWidget from .color_inputs import ( ColorInputsWidget, - AlphaInputs + AlphaSlider, + AlphaInputs, + HEXInputs, + RGBInputs, + HSLInputs, + HSVInputs, + CMYKInputs ) @@ -16,24 +22,27 @@ class ColorPickerWidget(QtWidgets.QWidget): def __init__(self, color=None, parent=None): super(ColorPickerWidget, self).__init__(parent) - # Eye picked widget - pick_widget = PickScreenColorWidget() - - # Color utils - utils_widget = QtWidgets.QWidget(self) - utils_layout = QtWidgets.QVBoxLayout(utils_widget) - - bottom_utils_widget = QtWidgets.QWidget(utils_widget) + top_part = QtWidgets.QWidget(self) + left_side = QtWidgets.QWidget(top_part) # Color triangle - color_triangle = QtColorTriangle(utils_widget) + color_triangle = QtColorTriangle(left_side) - # Color preview - color_view = ColorViewer(bottom_utils_widget) - color_view.setMaximumHeight(50) + alpha_slider = AlphaSlider(QtCore.Qt.Horizontal, left_side) + + left_layout = QtWidgets.QVBoxLayout(left_side) + left_layout.setContentsMargins(0, 0, 0, 0) + left_layout.addWidget(color_triangle) + left_layout.addWidget(alpha_slider) + + right_side = QtWidgets.QWidget(top_part) + + # Eye picked widget + pick_widget = PickScreenColorWidget() + pick_widget.setMaximumHeight(50) # Color pick button - btn_pick_color = QtWidgets.QPushButton(bottom_utils_widget) + btn_pick_color = QtWidgets.QPushButton(right_side) icon_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "eyedropper.png" @@ -41,50 +50,62 @@ class ColorPickerWidget(QtWidgets.QWidget): btn_pick_color.setIcon(QtGui.QIcon(icon_path)) btn_pick_color.setToolTip("Pick a color") - # Color inputs widget - color_inputs = ColorInputsWidget(self) + # Color preview + color_view = ColorViewer(right_side) + color_view.setMaximumHeight(50) - # Alpha inputs - alpha_input_wrapper_widget = QtWidgets.QWidget(self) - alpha_input_wrapper_layout = QtWidgets.QVBoxLayout( - alpha_input_wrapper_widget - ) + row = 0 + right_layout = QtWidgets.QGridLayout(right_side) + right_layout.setContentsMargins(0, 0, 0, 0) + right_layout.addWidget(btn_pick_color, row, 0) + right_layout.addWidget(color_view, row, 1) - alpha_inputs = AlphaInputs(alpha_input_wrapper_widget) - alpha_input_wrapper_layout.addWidget(alpha_inputs) - alpha_input_wrapper_layout.addWidget(QtWidgets.QWidget(), 1) + color_inputs_color = QtGui.QColor() + col_inputs_by_label = [ + ("HEX", HEXInputs(color_inputs_color, right_side)), + ("RGB", RGBInputs(color_inputs_color, right_side)), + ("HSL", HSLInputs(color_inputs_color, right_side)), + ("HSV", HSVInputs(color_inputs_color, right_side)) + ] + color_input_fields = [] + for label, input_field in col_inputs_by_label: + row += 1 + right_layout.addWidget(QtWidgets.QLabel(label, right_side), row, 0) + right_layout.addWidget(input_field, row, 1) + input_field.value_changed.connect( + self._on_color_input_value_change + ) + color_input_fields.append(input_field) - bottom_utils_layout = QtWidgets.QHBoxLayout(bottom_utils_widget) - bottom_utils_layout.setContentsMargins(0, 0, 0, 0) - bottom_utils_layout.addWidget(color_view, 1) - bottom_utils_layout.addWidget(btn_pick_color, 0) - - utils_layout.addWidget(bottom_utils_widget, 0) - utils_layout.addWidget(color_triangle, 1) + row += 1 + alpha_inputs = AlphaInputs(right_side) + right_layout.addWidget(QtWidgets.QLabel("Alpha", right_side), row, 0) + right_layout.addWidget(alpha_inputs, row, 1) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(utils_widget, 1) - layout.addWidget(color_inputs, 0) - layout.addWidget(alpha_input_wrapper_widget, 0) + layout.setSpacing(5) + layout.addWidget(left_side, 1) + layout.addWidget(right_side, 0) color_view.set_color(color_triangle.cur_color) - color_inputs.set_color(color_triangle.cur_color) color_triangle.color_changed.connect(self.triangle_color_changed) + alpha_slider.valueChanged.connect(self._on_alpha_slider_change) pick_widget.color_selected.connect(self.on_color_change) - color_inputs.color_changed.connect(self.on_color_change) - alpha_inputs.alpha_changed.connect(self.alpha_changed) + alpha_inputs.alpha_changed.connect(self._on_alpha_inputs_changed) btn_pick_color.released.connect(self.pick_color) + self.color_input_fields = color_input_fields + self.color_inputs_color = color_inputs_color + self.pick_widget = pick_widget - self.utils_widget = utils_widget - self.bottom_utils_widget = bottom_utils_widget self.color_triangle = color_triangle + self.alpha_slider = alpha_slider + self.color_view = color_view - self.btn_pick_color = btn_pick_color - self.color_inputs = color_inputs self.alpha_inputs = alpha_inputs + self.btn_pick_color = btn_pick_color if color: self.set_color(color) @@ -92,9 +113,7 @@ class ColorPickerWidget(QtWidgets.QWidget): def showEvent(self, event): super(ColorPickerWidget, self).showEvent(event) - triangle_width = int(( - self.utils_widget.height() - self.bottom_utils_widget.height() - ) / 5 * 4) + triangle_width = int(self.height() / 5 * 4) self.color_triangle.setMinimumWidth(triangle_width) def color(self): @@ -109,12 +128,38 @@ class ColorPickerWidget(QtWidgets.QWidget): def triangle_color_changed(self, color): self.color_view.set_color(color) - self.color_inputs.set_color(color) + if self.color_inputs_color != color: + self.color_inputs_color.setRgb( + color.red(), color.green(), color.blue() + ) + for color_input in self.color_input_fields: + color_input.color_changed() def on_color_change(self, color): self.color_view.set_color(color) self.color_triangle.set_color(color) - self.color_inputs.set_color(color) + if self.color_inputs_color != color: + self.color_inputs_color.setRgb( + color.red(), color.green(), color.blue() + ) + for color_input in self.color_input_fields: + color_input.color_changed() - def alpha_changed(self, alpha): - self.color_view.set_alpha(alpha) + def _on_color_input_value_change(self): + for input_field in self.color_input_fields: + input_field.color_changed() + self.on_color_change(QtGui.QColor(self.color_inputs_color)) + + def alpha_changed(self, value): + self.color_view.set_alpha(value) + if self.alpha_slider.value() != value: + self.alpha_slider.setValue(value) + + if self.alpha_inputs.alpha_value != value: + self.alpha_inputs.set_alpha(value) + + def _on_alpha_inputs_changed(self, value): + self.alpha_changed(value) + + def _on_alpha_slider_change(self, value): + self.alpha_changed(value) From 3245e97884c724037de5a14c1ba130f5d818857e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 12:29:37 +0200 Subject: [PATCH 49/68] checkerboard can be drawn out of ColorViewer --- openpype/widgets/color_widgets/color_view.py | 52 +++++++++++--------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/openpype/widgets/color_widgets/color_view.py b/openpype/widgets/color_widgets/color_view.py index a4393a6625..d6d7f0a666 100644 --- a/openpype/widgets/color_widgets/color_view.py +++ b/openpype/widgets/color_widgets/color_view.py @@ -1,6 +1,34 @@ from Qt import QtWidgets, QtCore, QtGui +def draw_checkerboard_tile(piece_size=None, color_1=None, color_2=None): + if piece_size is None: + piece_size = 7 + + if color_1 is None: + color_1 = QtGui.QColor(188, 188, 188) + + if color_2 is None: + color_2 = QtGui.QColor(90, 90, 90) + + pix = QtGui.QPixmap(piece_size * 2, piece_size * 2) + pix_painter = QtGui.QPainter(pix) + + rect = QtCore.QRect( + 0, 0, piece_size, piece_size + ) + pix_painter.fillRect(rect, color_1) + rect.moveTo(piece_size, piece_size) + pix_painter.fillRect(rect, color_1) + rect.moveTo(piece_size, 0) + pix_painter.fillRect(rect, color_2) + rect.moveTo(0, piece_size) + pix_painter.fillRect(rect, color_2) + pix_painter.end() + + return pix + + class ColorViewer(QtWidgets.QWidget): def __init__(self, parent=None): super(ColorViewer, self).__init__(parent) @@ -14,29 +42,7 @@ class ColorViewer(QtWidgets.QWidget): def checkerboard(self): if not self._checkerboard: - checkboard_piece_size = 10 - color_1 = QtGui.QColor(188, 188, 188) - color_2 = QtGui.QColor(90, 90, 90) - - pix = QtGui.QPixmap( - checkboard_piece_size * 2, - checkboard_piece_size * 2 - ) - pix_painter = QtGui.QPainter(pix) - - rect = QtCore.QRect( - 0, 0, checkboard_piece_size, checkboard_piece_size - ) - pix_painter.fillRect(rect, color_1) - rect.moveTo(checkboard_piece_size, checkboard_piece_size) - pix_painter.fillRect(rect, color_1) - rect.moveTo(checkboard_piece_size, 0) - pix_painter.fillRect(rect, color_2) - rect.moveTo(0, checkboard_piece_size) - pix_painter.fillRect(rect, color_2) - pix_painter.end() - self._checkerboard = pix - + self._checkerboard = draw_checkerboard_tile() return self._checkerboard def color(self): From c4ec9cb7f1f0d5a2798fe1dc9acd902e2ca84095 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 13:10:49 +0200 Subject: [PATCH 50/68] use draw_checkerboard_tile to draw checkerboard --- .../tools/settings/settings/color_widget.py | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/openpype/tools/settings/settings/color_widget.py b/openpype/tools/settings/settings/color_widget.py index 7b534b749c..b84c8dd9cc 100644 --- a/openpype/tools/settings/settings/color_widget.py +++ b/openpype/tools/settings/settings/color_widget.py @@ -2,7 +2,10 @@ from Qt import QtWidgets, QtCore, QtGui from .item_widgets import InputWidget -from openpype.widgets.color_widgets import ColorPickerWidget +from openpype.widgets.color_widgets import ( + ColorPickerWidget, + draw_checkerboard_tile +) class ColorWidget(InputWidget): @@ -77,29 +80,7 @@ class ColorViewer(QtWidgets.QWidget): def checkerboard(self): if not self._checkerboard: - checkboard_piece_size = 10 - color_1 = QtGui.QColor(188, 188, 188) - color_2 = QtGui.QColor(90, 90, 90) - - pix = QtGui.QPixmap( - checkboard_piece_size * 2, - checkboard_piece_size * 2 - ) - pix_painter = QtGui.QPainter(pix) - - rect = QtCore.QRect( - 0, 0, checkboard_piece_size, checkboard_piece_size - ) - pix_painter.fillRect(rect, color_1) - rect.moveTo(checkboard_piece_size, checkboard_piece_size) - pix_painter.fillRect(rect, color_1) - rect.moveTo(checkboard_piece_size, 0) - pix_painter.fillRect(rect, color_2) - rect.moveTo(0, checkboard_piece_size) - pix_painter.fillRect(rect, color_2) - pix_painter.end() - self._checkerboard = pix - + self._checkerboard = draw_checkerboard_tile() return self._checkerboard def color(self): From 5680f96a499a59844d84b591d39c3b52cb431242 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 13:13:10 +0200 Subject: [PATCH 51/68] changed how triangle size is set --- openpype/widgets/color_widgets/color_picker_widget.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 7158a426a4..725d0b374e 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -107,14 +107,21 @@ class ColorPickerWidget(QtWidgets.QWidget): self.alpha_inputs = alpha_inputs self.btn_pick_color = btn_pick_color + self._minimum_size_set = False + if color: self.set_color(color) self.alpha_changed(color.alpha()) def showEvent(self, event): super(ColorPickerWidget, self).showEvent(event) - triangle_width = int(self.height() / 5 * 4) - self.color_triangle.setMinimumWidth(triangle_width) + if self._minimum_size_set: + return + + triangle_size = max(int(self.width() / 5 * 3), 180) + self.color_triangle.setMinimumWidth(triangle_size) + self.color_triangle.setMinimumHeight(triangle_size) + self._minimum_size_set = True def color(self): return self.color_view.color() From 20c119f3555ee267c5784ce1e8d4e7db8d65ae9a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 13:13:20 +0200 Subject: [PATCH 52/68] added draw_checkerboard_tile to init file --- openpype/widgets/color_widgets/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/widgets/color_widgets/__init__.py b/openpype/widgets/color_widgets/__init__.py index 3423e26cf8..324b23543d 100644 --- a/openpype/widgets/color_widgets/__init__.py +++ b/openpype/widgets/color_widgets/__init__.py @@ -1,6 +1,14 @@ -from .color_picker_widget import ColorPickerWidget +from .color_picker_widget import ( + ColorPickerWidget +) + +from .color_view import ( + draw_checkerboard_tile +) __all__ = ( "ColorPickerWidget", + + "draw_checkerboard_tile" ) From 4bb432284fe4dea23bb61e2272d84d952e8ff4a3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 13:13:39 +0200 Subject: [PATCH 53/68] added stretch at the end of gridlayout --- openpype/widgets/color_widgets/color_picker_widget.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 725d0b374e..3b1eb1aca0 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -82,6 +82,9 @@ class ColorPickerWidget(QtWidgets.QWidget): right_layout.addWidget(QtWidgets.QLabel("Alpha", right_side), row, 0) right_layout.addWidget(alpha_inputs, row, 1) + row += 1 + right_layout.setRowStretch(row, 1) + layout = QtWidgets.QHBoxLayout(self) layout.setSpacing(5) layout.addWidget(left_side, 1) From a688a1510e8dcb439267308112a72e81e61c546c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 13:13:50 +0200 Subject: [PATCH 54/68] alpha slider has proxy --- openpype/widgets/color_widgets/color_picker_widget.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 3b1eb1aca0..117e1c9aef 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -28,7 +28,12 @@ class ColorPickerWidget(QtWidgets.QWidget): # Color triangle color_triangle = QtColorTriangle(left_side) - alpha_slider = AlphaSlider(QtCore.Qt.Horizontal, left_side) + alpha_slider_proxy = QtWidgets.QWidget(left_side) + alpha_slider = AlphaSlider(QtCore.Qt.Horizontal, alpha_slider_proxy) + + alpha_slider_layout = QtWidgets.QHBoxLayout(alpha_slider_proxy) + alpha_slider_layout.setContentsMargins(5, 5, 5, 5) + alpha_slider_layout.addWidget(alpha_slider, 1) left_layout = QtWidgets.QVBoxLayout(left_side) left_layout.setContentsMargins(0, 0, 0, 0) From e40847f61ff49b1ae47bce8b471f87ec98352852 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 13:14:05 +0200 Subject: [PATCH 55/68] added stretched of triangle and alpha slider --- openpype/widgets/color_widgets/color_picker_widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 117e1c9aef..23c06929c0 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -37,8 +37,8 @@ class ColorPickerWidget(QtWidgets.QWidget): left_layout = QtWidgets.QVBoxLayout(left_side) left_layout.setContentsMargins(0, 0, 0, 0) - left_layout.addWidget(color_triangle) - left_layout.addWidget(alpha_slider) + left_layout.addWidget(color_triangle, 1) + left_layout.addWidget(alpha_slider_proxy, 0) right_side = QtWidgets.QWidget(top_part) From 97c1a14de48b48713bee78c1248573bf0f969e7e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 13:14:17 +0200 Subject: [PATCH 56/68] added spacing between left and right widget --- openpype/widgets/color_widgets/color_picker_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 23c06929c0..40e20496f6 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -91,7 +91,7 @@ class ColorPickerWidget(QtWidgets.QWidget): right_layout.setRowStretch(row, 1) layout = QtWidgets.QHBoxLayout(self) - layout.setSpacing(5) + layout.setSpacing(20) layout.addWidget(left_side, 1) layout.addWidget(right_side, 0) From 0579cb2983d32205deed83d778bd4fbbac3c44e7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 15:33:43 +0200 Subject: [PATCH 57/68] nicer alpha slider --- .../widgets/color_widgets/color_inputs.py | 90 ++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index ddb832c655..d3c801bc0f 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -1,6 +1,8 @@ import re from Qt import QtWidgets, QtCore, QtGui +from .color_view import draw_checkerboard_tile + slide_style = """ QSlider::groove:horizontal { @@ -40,7 +42,14 @@ class AlphaSlider(QtWidgets.QSlider): self.setMaximum(255) self.setValue(255) - self.setStyleSheet(slide_style) + self._checkerboard = None + + def checkerboard(self): + if self._checkerboard is None: + self._checkerboard = draw_checkerboard_tile( + 3, QtGui.QColor(255, 255, 255), QtGui.QColor(27, 27, 27) + ) + return self._checkerboard def mousePressEvent(self, event): self._mouse_clicked = True @@ -64,6 +73,85 @@ class AlphaSlider(QtWidgets.QSlider): self._mouse_clicked = True super(AlphaSlider, self).mouseReleaseEvent(event) + def paintEvent(self, event): + painter = QtGui.QPainter(self) + opt = QtWidgets.QStyleOptionSlider() + self.initStyleOption(opt) + + painter.fillRect(event.rect(), QtCore.Qt.transparent) + + painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform) + rect = self.style().subControlRect( + QtWidgets.QStyle.CC_Slider, + opt, + QtWidgets.QStyle.SC_SliderGroove, + self + ) + final_height = 9 + offset_top = 0 + if rect.height() > final_height: + offset_top = int((rect.height() - final_height) / 2) + rect = QtCore.QRect( + rect.x(), + offset_top, + rect.width(), + final_height + ) + + pix_rect = QtCore.QRect(event.rect()) + pix_rect.setX(rect.x()) + pix_rect.setWidth(rect.width() - (2 * rect.x())) + pix = QtGui.QPixmap(pix_rect.width(), pix_rect.height()) + pix_painter = QtGui.QPainter(pix) + pix_painter.drawTiledPixmap(pix_rect, self.checkerboard()) + gradient = QtGui.QLinearGradient(rect.topLeft(), rect.bottomRight()) + gradient.setColorAt(0, QtCore.Qt.transparent) + gradient.setColorAt(1, QtCore.Qt.white) + pix_painter.fillRect(pix_rect, gradient) + pix_painter.end() + + brush = QtGui.QBrush(pix) + painter.save() + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(brush) + ratio = rect.height() / 2 + painter.drawRoundedRect(rect, ratio, ratio) + painter.restore() + + _handle_rect = self.style().subControlRect( + QtWidgets.QStyle.CC_Slider, + opt, + QtWidgets.QStyle.SC_SliderHandle, + self + ) + + handle_rect = QtCore.QRect(rect) + if offset_top > 1: + height = handle_rect.height() + handle_rect.setY(handle_rect.y() - 1) + handle_rect.setHeight(height + 2) + handle_rect.setX(_handle_rect.x()) + handle_rect.setWidth(handle_rect.height()) + + painter.save() + + gradient = QtGui.QRadialGradient() + radius = handle_rect.height() / 2 + center_x = handle_rect.width() / 2 + handle_rect.x() + center_y = handle_rect.height() + gradient.setCenter(center_x, center_y) + gradient.setCenterRadius(radius) + gradient.setFocalPoint(center_x, center_y) + + gradient.setColorAt(0.9, QtGui.QColor(127, 127, 127)) + gradient.setColorAt(1, QtCore.Qt.transparent) + + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(gradient) + painter.drawEllipse(handle_rect) + + painter.restore() + class AlphaInputs(QtWidgets.QWidget): alpha_changed = QtCore.Signal(int) From dbf70d34ea039a31ebd3b22dfbdc90d48a8ed61c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 15:38:03 +0200 Subject: [PATCH 58/68] darker white --- openpype/widgets/color_widgets/color_inputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index d3c801bc0f..eda8c618f1 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -47,7 +47,7 @@ class AlphaSlider(QtWidgets.QSlider): def checkerboard(self): if self._checkerboard is None: self._checkerboard = draw_checkerboard_tile( - 3, QtGui.QColor(255, 255, 255), QtGui.QColor(27, 27, 27) + 3, QtGui.QColor(173, 173, 173), QtGui.QColor(27, 27, 27) ) return self._checkerboard From faa6fdcd08065e9de928a68f3a82b093f3318795 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 15:39:19 +0200 Subject: [PATCH 59/68] removed unused imports --- openpype/widgets/color_widgets/color_picker_widget.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 40e20496f6..fd0f31e342 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -5,14 +5,12 @@ from .color_triangle import QtColorTriangle from .color_view import ColorViewer from .color_screen_pick import PickScreenColorWidget from .color_inputs import ( - ColorInputsWidget, AlphaSlider, AlphaInputs, HEXInputs, RGBInputs, HSLInputs, - HSVInputs, - CMYKInputs + HSVInputs ) From a32624f22bbf75d964d9b235a05a903ea5e336f3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 17:10:46 +0200 Subject: [PATCH 60/68] fake blender addons from blender user scripts --- openpype/hosts/blender/api/lib.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index 6aa1cb46ac..e4d063a583 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -3,6 +3,7 @@ import traceback import importlib import bpy +import addon_utils def load_scripts(paths): @@ -74,6 +75,23 @@ def load_scripts(paths): for mod in bpy.utils.modules_from_path(path, loaded_modules): test_register(mod) + addons_paths = [] + for base_path in paths: + addons_path = os.path.join(base_path, "addons") + if os.path.exists(addons_path): + addons_paths.append(addons_path) + + if addons_paths: + # Fake addons + origin_paths = addon_utils.paths + + def new_paths(): + paths = origin_paths() + addons_paths + return paths + + addon_utils.paths = new_paths + addon_utils.modules_refresh() + # load template (if set) if any(bpy.utils.app_template_paths()): import bl_app_template_utils From df5c4aebd8cd2adc31144eaecb3819c9561421ee Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 17:13:23 +0200 Subject: [PATCH 61/68] also prepend modules to sys path --- openpype/hosts/blender/api/lib.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index e4d063a583..fe5d3f93e9 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -78,8 +78,12 @@ def load_scripts(paths): addons_paths = [] for base_path in paths: addons_path = os.path.join(base_path, "addons") - if os.path.exists(addons_path): - addons_paths.append(addons_path) + if not os.path.exists(addons_path): + continue + addons_paths.append(addons_path) + addons_module_path = os.path.join(addons_path, "modules") + if os.path.exists(addons_module_path): + bpy.utils._sys_path_ensure_prepend(addons_module_path) if addons_paths: # Fake addons From c496386b47ef126bda0a6623f0411dc4920ac79c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 19:59:54 +0200 Subject: [PATCH 62/68] ok btn has same width as cancel btn --- openpype/tools/settings/settings/color_widget.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/tools/settings/settings/color_widget.py b/openpype/tools/settings/settings/color_widget.py index b84c8dd9cc..7aa3a4bba3 100644 --- a/openpype/tools/settings/settings/color_widget.py +++ b/openpype/tools/settings/settings/color_widget.py @@ -141,9 +141,18 @@ class ColorDialog(QtWidgets.QDialog): cancel_btn.clicked.connect(self.on_cancel_clicked) self.picker_widget = picker_widget + self.ok_btn = ok_btn + self.cancel_btn = cancel_btn self._result = None + def showEvent(self, event): + super(ColorDialog, self).showEvent(event) + + btns_width = max(self.ok_btn.width(), self.cancel_btn.width()) + self.ok_btn.setFixedWidth(btns_width) + self.cancel_btn.setFixedWidth(btns_width) + def on_ok_clicked(self): self._result = self.picker_widget.color() self.close() From 69ed30f15c5fd8c5f7c19c2a8e34541a56a390e3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 20:23:26 +0200 Subject: [PATCH 63/68] colorpicker widget is one big grid --- .../color_widgets/color_picker_widget.py | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index fd0f31e342..27b9f8fc82 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -21,31 +21,23 @@ class ColorPickerWidget(QtWidgets.QWidget): super(ColorPickerWidget, self).__init__(parent) top_part = QtWidgets.QWidget(self) - left_side = QtWidgets.QWidget(top_part) # Color triangle - color_triangle = QtColorTriangle(left_side) + color_triangle = QtColorTriangle(self) - alpha_slider_proxy = QtWidgets.QWidget(left_side) + alpha_slider_proxy = QtWidgets.QWidget(self) alpha_slider = AlphaSlider(QtCore.Qt.Horizontal, alpha_slider_proxy) alpha_slider_layout = QtWidgets.QHBoxLayout(alpha_slider_proxy) alpha_slider_layout.setContentsMargins(5, 5, 5, 5) alpha_slider_layout.addWidget(alpha_slider, 1) - left_layout = QtWidgets.QVBoxLayout(left_side) - left_layout.setContentsMargins(0, 0, 0, 0) - left_layout.addWidget(color_triangle, 1) - left_layout.addWidget(alpha_slider_proxy, 0) - - right_side = QtWidgets.QWidget(top_part) - # Eye picked widget pick_widget = PickScreenColorWidget() pick_widget.setMaximumHeight(50) # Color pick button - btn_pick_color = QtWidgets.QPushButton(right_side) + btn_pick_color = QtWidgets.QPushButton(self) icon_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "eyedropper.png" @@ -54,44 +46,52 @@ class ColorPickerWidget(QtWidgets.QWidget): btn_pick_color.setToolTip("Pick a color") # Color preview - color_view = ColorViewer(right_side) + color_view = ColorViewer(self) color_view.setMaximumHeight(50) - row = 0 - right_layout = QtWidgets.QGridLayout(right_side) - right_layout.setContentsMargins(0, 0, 0, 0) - right_layout.addWidget(btn_pick_color, row, 0) - right_layout.addWidget(color_view, row, 1) + alpha_inputs = AlphaInputs(self) color_inputs_color = QtGui.QColor() col_inputs_by_label = [ - ("HEX", HEXInputs(color_inputs_color, right_side)), - ("RGB", RGBInputs(color_inputs_color, right_side)), - ("HSL", HSLInputs(color_inputs_color, right_side)), - ("HSV", HSVInputs(color_inputs_color, right_side)) + ("HEX", HEXInputs(color_inputs_color, self)), + ("RGB", RGBInputs(color_inputs_color, self)), + ("HSL", HSLInputs(color_inputs_color, self)), + ("HSV", HSVInputs(color_inputs_color, self)) ] + + layout = QtWidgets.QGridLayout(self) + empty_col = 1 + label_col = empty_col + 1 + input_col = label_col + 1 + empty_widget = QtWidgets.QWidget(self) + empty_widget.setFixedWidth(10) + layout.addWidget(empty_widget, 0, empty_col) + + row = 0 + layout.addWidget(btn_pick_color, row, label_col) + layout.addWidget(color_view, row, input_col) + row += 1 + color_input_fields = [] for label, input_field in col_inputs_by_label: - row += 1 - right_layout.addWidget(QtWidgets.QLabel(label, right_side), row, 0) - right_layout.addWidget(input_field, row, 1) + layout.addWidget(QtWidgets.QLabel(label, self), row, label_col) + layout.addWidget(input_field, row, input_col) input_field.value_changed.connect( self._on_color_input_value_change ) color_input_fields.append(input_field) + row += 1 + layout.addWidget(color_triangle, 0, 0, row + 1, 1) + layout.setRowStretch(row, 1) row += 1 - alpha_inputs = AlphaInputs(right_side) - right_layout.addWidget(QtWidgets.QLabel("Alpha", right_side), row, 0) - right_layout.addWidget(alpha_inputs, row, 1) + layout.addWidget(alpha_slider_proxy, row, 0) + + layout.addWidget(QtWidgets.QLabel("Alpha", self), row, label_col) + layout.addWidget(alpha_inputs, row, input_col) row += 1 - right_layout.setRowStretch(row, 1) - - layout = QtWidgets.QHBoxLayout(self) - layout.setSpacing(20) - layout.addWidget(left_side, 1) - layout.addWidget(right_side, 0) + layout.setRowStretch(row, 1) color_view.set_color(color_triangle.cur_color) From c77094aeb6bd5666d791c4486834123f06780362 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 20:52:44 +0200 Subject: [PATCH 64/68] nicer view of color in setting ui --- .../tools/settings/settings/color_widget.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/openpype/tools/settings/settings/color_widget.py b/openpype/tools/settings/settings/color_widget.py index 7aa3a4bba3..fa0cd2c989 100644 --- a/openpype/tools/settings/settings/color_widget.py +++ b/openpype/tools/settings/settings/color_widget.py @@ -80,7 +80,7 @@ class ColorViewer(QtWidgets.QWidget): def checkerboard(self): if not self._checkerboard: - self._checkerboard = draw_checkerboard_tile() + self._checkerboard = draw_checkerboard_tile(self.height() / 4) return self._checkerboard def color(self): @@ -101,15 +101,21 @@ class ColorViewer(QtWidgets.QWidget): def paintEvent(self, event): rect = event.rect() - # Paint everything to pixmap as it has transparency - pix = QtGui.QPixmap(rect.width(), rect.height()) - pix_painter = QtGui.QPainter(pix) - pix_painter.drawTiledPixmap(rect, self.checkerboard()) - pix_painter.fillRect(rect, self.actual_color) - pix_painter.end() - painter = QtGui.QPainter(self) - painter.drawPixmap(rect, pix) + painter.setRenderHint(QtGui.QPainter.Antialiasing) + + radius = rect.height() / 2 + rounded_rect = QtGui.QPainterPath() + rounded_rect.addRoundedRect(QtCore.QRectF(rect), radius, radius) + painter.setClipPath(rounded_rect) + + pen = QtGui.QPen(QtGui.QColor(255, 255, 255, 67)) + pen.setWidth(1) + painter.setPen(pen) + painter.drawTiledPixmap(rect, self.checkerboard()) + painter.fillRect(rect, self.actual_color) + painter.drawPath(rounded_rect) + painter.end() From c4a7a3ac5013476cab7079c31b6d614422551bea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 21:16:46 +0200 Subject: [PATCH 65/68] added border to color viewer in the dialog --- openpype/widgets/color_widgets/color_view.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/openpype/widgets/color_widgets/color_view.py b/openpype/widgets/color_widgets/color_view.py index d6d7f0a666..8644281a1d 100644 --- a/openpype/widgets/color_widgets/color_view.py +++ b/openpype/widgets/color_widgets/color_view.py @@ -42,7 +42,7 @@ class ColorViewer(QtWidgets.QWidget): def checkerboard(self): if not self._checkerboard: - self._checkerboard = draw_checkerboard_tile() + self._checkerboard = draw_checkerboard_tile(4) return self._checkerboard def color(self): @@ -70,15 +70,14 @@ class ColorViewer(QtWidgets.QWidget): self.update() def paintEvent(self, event): - rect = event.rect() - - # Paint everything to pixmap as it has transparency - pix = QtGui.QPixmap(rect.width(), rect.height()) - pix_painter = QtGui.QPainter(pix) - pix_painter.drawTiledPixmap(rect, self.checkerboard()) - pix_painter.fillRect(rect, self.actual_color) - pix_painter.end() + clip_rect = event.rect() + rect = clip_rect.adjusted(0, 0, -1, -1) painter = QtGui.QPainter(self) - painter.drawPixmap(rect, pix) + painter.setClipRect(clip_rect) + painter.drawTiledPixmap(rect, self.checkerboard()) + painter.setBrush(self.actual_color) + pen = QtGui.QPen(QtGui.QColor(255, 255, 255, 67)) + painter.setPen(pen) + painter.drawRect(rect) painter.end() From 0d40c89f471f6abccfbe6ae58667bb5304086422 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 20 May 2021 21:18:16 +0200 Subject: [PATCH 66/68] removed unused variable --- openpype/widgets/color_widgets/color_picker_widget.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 27b9f8fc82..81ec1f87aa 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -20,8 +20,6 @@ class ColorPickerWidget(QtWidgets.QWidget): def __init__(self, color=None, parent=None): super(ColorPickerWidget, self).__init__(parent) - top_part = QtWidgets.QWidget(self) - # Color triangle color_triangle = QtColorTriangle(self) From 7efd2aeecea330ce985c819fbbb45ca6b689496d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 21 May 2021 13:02:44 +0200 Subject: [PATCH 67/68] enhance windows and add linux launcher --- tools/run_project_manager.ps1 | 42 ++++++++++++++ tools/run_projectmanager.sh | 103 ++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 tools/run_projectmanager.sh diff --git a/tools/run_project_manager.ps1 b/tools/run_project_manager.ps1 index 67c2d2eb5e..9886a80316 100644 --- a/tools/run_project_manager.ps1 +++ b/tools/run_project_manager.ps1 @@ -10,9 +10,51 @@ PS> .\run_project_manager.ps1 #> + +$art = @" + + . . .. . .. + _oOOP3OPP3Op_. . + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . + +"@ + +Write-Host $art -ForegroundColor DarkGreen + $current_dir = Get-Location $script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent $openpype_root = (Get-Item $script_dir).parent.FullName + +$env:_INSIDE_OPENPYPE_TOOL = "1" + +# make sure Poetry is in PATH +if (-not (Test-Path 'env:POETRY_HOME')) { + $env:POETRY_HOME = "$openpype_root\.poetry" +} +$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" + Set-Location -Path $openpype_root + +Write-Host ">>> " -NoNewline -ForegroundColor Green +Write-Host "Reading Poetry ... " -NoNewline +if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { + Write-Host "NOT FOUND" -ForegroundColor Yellow + Write-Host "*** " -NoNewline -ForegroundColor Yellow + Write-Host "We need to install Poetry create virtual env first ..." + & "$openpype_root\tools\create_env.ps1" +} else { + Write-Host "OK" -ForegroundColor Green +} + & poetry run python "$($openpype_root)\start.py" projectmanager Set-Location -Path $current_dir diff --git a/tools/run_projectmanager.sh b/tools/run_projectmanager.sh new file mode 100644 index 0000000000..312f321d67 --- /dev/null +++ b/tools/run_projectmanager.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash + +# Run OpenPype Settings GUI + + +art () { + cat <<-EOF + + . . .. . .. + _oOOP3OPP3Op_. . + .PPpo~· ·· ~2p. ·· ···· · · + ·Ppo · .pPO3Op.· · O:· · · · + .3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · · + ·~OP 3PO· .Op3 : · ·· _____ _____ _____ + ·P3O · oP3oP3O3P' · · · · / /·/ /·/ / + O3:· O3p~ · ·:· · ·/____/·/____/ /____/ + 'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · · + · ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · · + · '_ .. · . _OP3·· · ·https://openpype.io·· · + ~P3·OPPPO3OP~ · ·· · + · ' '· · ·· · · · ·· · + +EOF +} + +# Colors for terminal + +RST='\033[0m' # Text Reset + +# Regular Colors +Black='\033[0;30m' # Black +Red='\033[0;31m' # Red +Green='\033[0;32m' # Green +Yellow='\033[0;33m' # Yellow +Blue='\033[0;34m' # Blue +Purple='\033[0;35m' # Purple +Cyan='\033[0;36m' # Cyan +White='\033[0;37m' # White + +# Bold +BBlack='\033[1;30m' # Black +BRed='\033[1;31m' # Red +BGreen='\033[1;32m' # Green +BYellow='\033[1;33m' # Yellow +BBlue='\033[1;34m' # Blue +BPurple='\033[1;35m' # Purple +BCyan='\033[1;36m' # Cyan +BWhite='\033[1;37m' # White + +# Bold High Intensity +BIBlack='\033[1;90m' # Black +BIRed='\033[1;91m' # Red +BIGreen='\033[1;92m' # Green +BIYellow='\033[1;93m' # Yellow +BIBlue='\033[1;94m' # Blue +BIPurple='\033[1;95m' # Purple +BICyan='\033[1;96m' # Cyan +BIWhite='\033[1;97m' # White + + +############################################################################## +# Return absolute path +# Globals: +# None +# Arguments: +# Path to resolve +# Returns: +# None +############################################################################### +realpath () { + echo $(cd $(dirname "$1"); pwd)/$(basename "$1") +} + +# Main +main () { + + # Directories + openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) + + _inside_openpype_tool="1" + + # make sure Poetry is in PATH + if [[ -z $POETRY_HOME ]]; then + export POETRY_HOME="$openpype_root/.poetry" + fi + export PATH="$POETRY_HOME/bin:$PATH" + + pushd "$openpype_root" > /dev/null || return > /dev/null + + echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c" + if [ -f "$POETRY_HOME/bin/poetry" ]; then + echo -e "${BIGreen}OK${RST}" + else + echo -e "${BIYellow}NOT FOUND${RST}" + echo -e "${BIYellow}***${RST} We need to install Poetry and virtual env ..." + . "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; } + fi + + echo -e "${BIGreen}>>>${RST} Generating zip from current sources ..." + poetry run python "$openpype_root/start.py" projectmanager +} + +main From 85d3ce529e149b75321f7bfdb76081195be14610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 21 May 2021 13:04:18 +0200 Subject: [PATCH 68/68] set shell script executable --- tools/run_projectmanager.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 tools/run_projectmanager.sh diff --git a/tools/run_projectmanager.sh b/tools/run_projectmanager.sh old mode 100644 new mode 100755