From 4fb92b6fcab6401b110ab3cdb1be8ab6440e0856 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Feb 2022 10:25:33 +0100 Subject: [PATCH 01/20] Default to "Main" subset name across all hosts - 'testhost' is not refactored to default to "Main" --- openpype/hosts/aftereffects/plugins/create/create_render.py | 1 + openpype/hosts/blender/api/plugin.py | 2 ++ openpype/hosts/fusion/plugins/create/create_exr_saver.py | 1 + openpype/hosts/harmony/api/plugin.py | 2 +- openpype/hosts/houdini/api/plugin.py | 2 ++ openpype/hosts/maya/api/plugin.py | 1 + openpype/hosts/maya/plugins/create/create_animation.py | 1 - openpype/hosts/maya/plugins/create/create_ass.py | 1 - openpype/hosts/maya/plugins/create/create_assembly.py | 1 - openpype/hosts/maya/plugins/create/create_camera.py | 2 -- openpype/hosts/maya/plugins/create/create_layout.py | 1 - openpype/hosts/maya/plugins/create/create_look.py | 1 - openpype/hosts/maya/plugins/create/create_mayaascii.py | 1 - openpype/hosts/maya/plugins/create/create_pointcache.py | 1 - openpype/hosts/maya/plugins/create/create_render.py | 1 - openpype/hosts/maya/plugins/create/create_review.py | 1 - openpype/hosts/maya/plugins/create/create_rig.py | 1 - openpype/hosts/maya/plugins/create/create_xgen.py | 1 - openpype/hosts/maya/plugins/create/create_yeti_cache.py | 1 - openpype/hosts/maya/plugins/create/create_yeti_rig.py | 1 - openpype/hosts/unreal/api/plugin.py | 2 +- 21 files changed, 9 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 5fe72d4f53..8dfc85cdc8 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -19,6 +19,7 @@ class CreateRender(openpype.api.Creator): name = "renderDefault" label = "Render on Farm" family = "render" + defaults = ["Main"] def process(self): stub = get_stub() # only after After Effects is up diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 1f0b142ef6..8c9ab9a27f 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -131,6 +131,8 @@ def deselect_all(): class Creator(PypeCreatorMixin, avalon.api.Creator): """Base class for Creator plug-ins.""" + defaults = ['Main'] + def process(self): collection = bpy.data.collections.new(name=self.data["subset"]) bpy.context.scene.collection.children.link(collection) diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_exr_saver.py index 077e77c059..2990ce23b8 100644 --- a/openpype/hosts/fusion/plugins/create/create_exr_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_exr_saver.py @@ -10,6 +10,7 @@ class CreateOpenEXRSaver(openpype.api.Creator): label = "Create OpenEXR Saver" hosts = ["fusion"] family = "render" + defaults = ["Main"] def process(self): diff --git a/openpype/hosts/harmony/api/plugin.py b/openpype/hosts/harmony/api/plugin.py index 7ac7fe510c..a50fbccc35 100644 --- a/openpype/hosts/harmony/api/plugin.py +++ b/openpype/hosts/harmony/api/plugin.py @@ -3,4 +3,4 @@ from openpype.api import PypeCreatorMixin class Creator(PypeCreatorMixin, harmony.Creator): - pass + defaults = ["Main"] diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 63d9bba470..e9e6d6c6d7 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -14,6 +14,8 @@ class OpenPypeCreatorError(CreatorError): class Creator(PypeCreatorMixin, houdini.Creator): + defaults = ['Main'] + def process(self): try: # re-raise as standard Python exception so diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index a5f03cd576..4c4d4afc2d 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -76,6 +76,7 @@ def get_reference_node_parents(ref): class Creator(PypeCreatorMixin, avalon.maya.Creator): + defaults = ['Main'] pass diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index 7ce96166f7..11a668cfc8 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -11,7 +11,6 @@ class CreateAnimation(plugin.Creator): label = "Animation" family = "animation" icon = "male" - defaults = ['Main'] def __init__(self, *args, **kwargs): super(CreateAnimation, self).__init__(*args, **kwargs) diff --git a/openpype/hosts/maya/plugins/create/create_ass.py b/openpype/hosts/maya/plugins/create/create_ass.py index 7f1cb55821..39f226900a 100644 --- a/openpype/hosts/maya/plugins/create/create_ass.py +++ b/openpype/hosts/maya/plugins/create/create_ass.py @@ -15,7 +15,6 @@ class CreateAss(plugin.Creator): label = "Ass StandIn" family = "ass" icon = "cube" - defaults = ['Main'] def __init__(self, *args, **kwargs): super(CreateAss, self).__init__(*args, **kwargs) diff --git a/openpype/hosts/maya/plugins/create/create_assembly.py b/openpype/hosts/maya/plugins/create/create_assembly.py index fa9d692792..ff5e1d45c4 100644 --- a/openpype/hosts/maya/plugins/create/create_assembly.py +++ b/openpype/hosts/maya/plugins/create/create_assembly.py @@ -8,4 +8,3 @@ class CreateAssembly(plugin.Creator): label = "Assembly" family = "assembly" icon = "cubes" - defaults = ['Main'] diff --git a/openpype/hosts/maya/plugins/create/create_camera.py b/openpype/hosts/maya/plugins/create/create_camera.py index 09f41914e4..8b2c881036 100644 --- a/openpype/hosts/maya/plugins/create/create_camera.py +++ b/openpype/hosts/maya/plugins/create/create_camera.py @@ -11,7 +11,6 @@ class CreateCamera(plugin.Creator): label = "Camera" family = "camera" icon = "video-camera" - defaults = ['Main'] def __init__(self, *args, **kwargs): super(CreateCamera, self).__init__(*args, **kwargs) @@ -33,4 +32,3 @@ class CreateCameraRig(plugin.Creator): label = "Camera Rig" family = "camerarig" icon = "video-camera" - defaults = ['Main'] diff --git a/openpype/hosts/maya/plugins/create/create_layout.py b/openpype/hosts/maya/plugins/create/create_layout.py index 8d825d23ba..6dc87430aa 100644 --- a/openpype/hosts/maya/plugins/create/create_layout.py +++ b/openpype/hosts/maya/plugins/create/create_layout.py @@ -8,4 +8,3 @@ class CreateLayout(plugin.Creator): label = "Layout" family = "layout" icon = "cubes" - defaults = ["Main"] diff --git a/openpype/hosts/maya/plugins/create/create_look.py b/openpype/hosts/maya/plugins/create/create_look.py index 36a3120a9a..56e2640919 100644 --- a/openpype/hosts/maya/plugins/create/create_look.py +++ b/openpype/hosts/maya/plugins/create/create_look.py @@ -11,7 +11,6 @@ class CreateLook(plugin.Creator): label = "Look" family = "look" icon = "paint-brush" - defaults = ['Main'] make_tx = True def __init__(self, *args, **kwargs): diff --git a/openpype/hosts/maya/plugins/create/create_mayaascii.py b/openpype/hosts/maya/plugins/create/create_mayaascii.py index 8bbdf107c6..f54f2df812 100644 --- a/openpype/hosts/maya/plugins/create/create_mayaascii.py +++ b/openpype/hosts/maya/plugins/create/create_mayaascii.py @@ -8,4 +8,3 @@ class CreateMayaScene(plugin.Creator): label = "Maya Scene" family = "mayaScene" icon = "file-archive-o" - defaults = ['Main'] diff --git a/openpype/hosts/maya/plugins/create/create_pointcache.py b/openpype/hosts/maya/plugins/create/create_pointcache.py index d8e5fd43a7..ede152f1ef 100644 --- a/openpype/hosts/maya/plugins/create/create_pointcache.py +++ b/openpype/hosts/maya/plugins/create/create_pointcache.py @@ -11,7 +11,6 @@ class CreatePointCache(plugin.Creator): label = "Point Cache" family = "pointcache" icon = "gears" - defaults = ['Main'] def __init__(self, *args, **kwargs): super(CreatePointCache, self).__init__(*args, **kwargs) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index fa5e73f3ed..b01d5a1184 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -64,7 +64,6 @@ class CreateRender(plugin.Creator): label = "Render" family = "rendering" icon = "eye" - defaults = ["Main"] _token = None _user = None diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index ae636ec691..14a21d28ca 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -12,7 +12,6 @@ class CreateReview(plugin.Creator): label = "Review" family = "review" icon = "video-camera" - defaults = ['Main'] keepImages = False isolate = False imagePlane = True diff --git a/openpype/hosts/maya/plugins/create/create_rig.py b/openpype/hosts/maya/plugins/create/create_rig.py index ae1670961f..8032e5fbbd 100644 --- a/openpype/hosts/maya/plugins/create/create_rig.py +++ b/openpype/hosts/maya/plugins/create/create_rig.py @@ -13,7 +13,6 @@ class CreateRig(plugin.Creator): label = "Rig" family = "rig" icon = "wheelchair" - defaults = ['Main'] def process(self): diff --git a/openpype/hosts/maya/plugins/create/create_xgen.py b/openpype/hosts/maya/plugins/create/create_xgen.py index 3953972952..8672c06a1e 100644 --- a/openpype/hosts/maya/plugins/create/create_xgen.py +++ b/openpype/hosts/maya/plugins/create/create_xgen.py @@ -8,4 +8,3 @@ class CreateXgen(plugin.Creator): label = "Xgen Interactive" family = "xgen" icon = "pagelines" - defaults = ['Main'] diff --git a/openpype/hosts/maya/plugins/create/create_yeti_cache.py b/openpype/hosts/maya/plugins/create/create_yeti_cache.py index a37d8082db..86e13b95b2 100644 --- a/openpype/hosts/maya/plugins/create/create_yeti_cache.py +++ b/openpype/hosts/maya/plugins/create/create_yeti_cache.py @@ -13,7 +13,6 @@ class CreateYetiCache(plugin.Creator): label = "Yeti Cache" family = "yeticache" icon = "pagelines" - defaults = ["Main"] def __init__(self, *args, **kwargs): super(CreateYetiCache, self).__init__(*args, **kwargs) diff --git a/openpype/hosts/maya/plugins/create/create_yeti_rig.py b/openpype/hosts/maya/plugins/create/create_yeti_rig.py index 2326958963..7abe2988cd 100644 --- a/openpype/hosts/maya/plugins/create/create_yeti_rig.py +++ b/openpype/hosts/maya/plugins/create/create_yeti_rig.py @@ -12,7 +12,6 @@ class CreateYetiRig(plugin.Creator): label = "Yeti Rig" family = "yetiRig" icon = "usb" - defaults = ["Main"] def process(self): diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index a69b12d6f5..5a6b236730 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -4,7 +4,7 @@ import openpype.api class Creator(openpype.api.Creator): """This serves as skeleton for future OpenPype specific functionality""" - pass + defaults = ['Main'] class Loader(api.Loader): From c6c73de82c94880053d62cd2755b8be132726530 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 14 Feb 2022 12:22:43 +0100 Subject: [PATCH 02/20] moved init utils directly to prelaunch hook --- openpype/hosts/fusion/api/__init__.py | 7 -- openpype/hosts/fusion/api/utils.py | 86 ------------------- .../hosts/fusion/hooks/pre_fusion_setup.py | 77 ++++++++++++++--- 3 files changed, 63 insertions(+), 107 deletions(-) delete mode 100644 openpype/hosts/fusion/api/utils.py diff --git a/openpype/hosts/fusion/api/__init__.py b/openpype/hosts/fusion/api/__init__.py index 5aee978c15..4ec9177af7 100644 --- a/openpype/hosts/fusion/api/__init__.py +++ b/openpype/hosts/fusion/api/__init__.py @@ -3,11 +3,6 @@ from .pipeline import ( uninstall ) -from .utils import ( - setup -) - - from .lib import ( get_additional_data, update_frame_range @@ -21,8 +16,6 @@ __all__ = [ "install", "uninstall", - # utils - "setup", # lib "get_additional_data", diff --git a/openpype/hosts/fusion/api/utils.py b/openpype/hosts/fusion/api/utils.py deleted file mode 100644 index 5605323b1e..0000000000 --- a/openpype/hosts/fusion/api/utils.py +++ /dev/null @@ -1,86 +0,0 @@ -#! python3 - -""" -Fusion tools for setting environment -""" - -import os -import shutil - -from openpype.api import Logger -import openpype.hosts.fusion - -log = Logger().get_logger(__name__) - - -def _sync_utility_scripts(env=None): - """ Synchronizing basic utlility scripts for resolve. - - To be able to run scripts from inside `Fusion/Workspace/Scripts` menu - all scripts has to be accessible from defined folder. - """ - if not env: - env = os.environ - - # initiate inputs - scripts = {} - us_env = env.get("FUSION_UTILITY_SCRIPTS_SOURCE_DIR") - us_dir = env.get("FUSION_UTILITY_SCRIPTS_DIR", "") - us_paths = [os.path.join( - os.path.dirname(os.path.abspath(openpype.hosts.fusion.__file__)), - "utility_scripts" - )] - - # collect script dirs - if us_env: - log.info(f"Utility Scripts Env: `{us_env}`") - us_paths = us_env.split( - os.pathsep) + us_paths - - # collect scripts from dirs - for path in us_paths: - scripts.update({path: os.listdir(path)}) - - log.info(f"Utility Scripts Dir: `{us_paths}`") - log.info(f"Utility Scripts: `{scripts}`") - - # make sure no script file is in folder - if next((s for s in os.listdir(us_dir)), None): - for s in os.listdir(us_dir): - path = os.path.normpath( - os.path.join(us_dir, s)) - log.info(f"Removing `{path}`...") - - # remove file or directory if not in our folders - if not os.path.isdir(path): - os.remove(path) - else: - shutil.rmtree(path) - - # copy scripts into Resolve's utility scripts dir - for d, sl in scripts.items(): - # directory and scripts list - for s in sl: - # script in script list - src = os.path.normpath(os.path.join(d, s)) - dst = os.path.normpath(os.path.join(us_dir, s)) - - log.info(f"Copying `{src}` to `{dst}`...") - - # copy file or directory from our folders to fusion's folder - if not os.path.isdir(src): - shutil.copy2(src, dst) - else: - shutil.copytree(src, dst) - - -def setup(env=None): - """ Wrapper installer started from pype.hooks.fusion.FusionPrelaunch() - """ - if not env: - env = os.environ - - # synchronize resolve utility scripts - _sync_utility_scripts(env) - - log.info("Fusion Pype wrapper has been installed") diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index 4a9dfaec15..b104b3c081 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -1,7 +1,8 @@ import os -import importlib +import shutil + +import openpype.hosts.fusion from openpype.lib import PreLaunchHook, ApplicationLaunchFailed -from openpype.hosts.fusion.api import utils class FusionPrelaunch(PreLaunchHook): @@ -36,17 +37,65 @@ class FusionPrelaunch(PreLaunchHook): f"Fusion.\n\nFUSION_UTILITY_SCRIPTS_DIR: '{us_dir}'" ) - try: - __import__("avalon.fusion") - __import__("pyblish") + self._sync_utility_scripts(self.launch_context.env) + self.log.info("Fusion Pype wrapper has been installed") - except ImportError: - self.log.warning( - "pyblish: Could not load Fusion integration.", - exc_info=True - ) + def _sync_utility_scripts(self, env): + """ Synchronizing basic utlility scripts for resolve. - else: - # Resolve Setup integration - importlib.reload(utils) - utils.setup(self.launch_context.env) + To be able to run scripts from inside `Fusion/Workspace/Scripts` menu + all scripts has to be accessible from defined folder. + """ + if not env: + env = {k: v for k, v in os.environ.items()} + + # initiate inputs + scripts = {} + us_env = env.get("FUSION_UTILITY_SCRIPTS_SOURCE_DIR") + us_dir = env.get("FUSION_UTILITY_SCRIPTS_DIR", "") + us_paths = [os.path.join( + os.path.dirname(os.path.abspath(openpype.hosts.fusion.__file__)), + "utility_scripts" + )] + + # collect script dirs + if us_env: + self.log.info(f"Utility Scripts Env: `{us_env}`") + us_paths = us_env.split( + os.pathsep) + us_paths + + # collect scripts from dirs + for path in us_paths: + scripts.update({path: os.listdir(path)}) + + self.log.info(f"Utility Scripts Dir: `{us_paths}`") + self.log.info(f"Utility Scripts: `{scripts}`") + + # make sure no script file is in folder + if next((s for s in os.listdir(us_dir)), None): + for s in os.listdir(us_dir): + path = os.path.normpath( + os.path.join(us_dir, s)) + self.log.info(f"Removing `{path}`...") + + # remove file or directory if not in our folders + if not os.path.isdir(path): + os.remove(path) + else: + shutil.rmtree(path) + + # copy scripts into Resolve's utility scripts dir + for d, sl in scripts.items(): + # directory and scripts list + for s in sl: + # script in script list + src = os.path.normpath(os.path.join(d, s)) + dst = os.path.normpath(os.path.join(us_dir, s)) + + self.log.info(f"Copying `{src}` to `{dst}`...") + + # copy file or directory from our folders to fusion's folder + if not os.path.isdir(src): + shutil.copy2(src, dst) + else: + shutil.copytree(src, dst) From c70d0c25ec9f8b93852398e9f1cd2a5e1d1b042f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 14 Feb 2022 12:23:36 +0100 Subject: [PATCH 03/20] moved fusion implementation from avalon to openpype --- openpype/hosts/fusion/api/__init__.py | 35 +++- openpype/hosts/fusion/api/lib.py | 59 ++++++- openpype/hosts/fusion/api/menu.py | 2 +- openpype/hosts/fusion/api/pipeline.py | 165 ++++++++++++++++-- openpype/hosts/fusion/api/workio.py | 45 +++++ .../fusion/plugins/create/create_exr_saver.py | 9 +- .../plugins/inventory/select_containers.py | 10 +- .../plugins/inventory/set_tool_color.py | 12 +- .../fusion/plugins/load/load_sequence.py | 23 +-- .../fusion/plugins/publish/collect_comp.py | 4 +- .../plugins/publish/collect_instances.py | 2 +- .../fusion/plugins/publish/render_local.py | 8 +- .../fusion/plugins/publish/submit_deadline.py | 5 +- .../fusion/scripts/duplicate_with_inputs.py | 9 +- .../fusion/scripts/fusion_switch_shot.py | 30 ++-- .../hosts/fusion/scripts/set_rendermode.py | 6 +- .../32bit/backgrounds_selected_to32bit.py | 9 +- .../32bit/backgrounds_to32bit.py | 8 +- .../32bit/loaders_selected_to32bit.py | 8 +- .../utility_scripts/32bit/loaders_to32bit.py | 8 +- .../utility_scripts/__OpenPype_Menu__.py | 6 +- .../hosts/fusion/utility_scripts/switch_ui.py | 19 +- .../utility_scripts/update_loader_ranges.py | 9 +- 23 files changed, 382 insertions(+), 109 deletions(-) create mode 100644 openpype/hosts/fusion/api/workio.py diff --git a/openpype/hosts/fusion/api/__init__.py b/openpype/hosts/fusion/api/__init__.py index 4ec9177af7..19d1e092fe 100644 --- a/openpype/hosts/fusion/api/__init__.py +++ b/openpype/hosts/fusion/api/__init__.py @@ -1,9 +1,27 @@ from .pipeline import ( install, - uninstall + uninstall, + + ls, + + imprint_container, + parse_container, + + get_current_comp, + comp_lock_and_undo_chunk +) + +from .workio import ( + open_file, + save_file, + current_file, + has_unsaved_changes, + file_extensions, + work_root ) from .lib import ( + maintained_selection, get_additional_data, update_frame_range ) @@ -15,9 +33,24 @@ __all__ = [ # pipeline "install", "uninstall", + "ls", + "imprint_container", + "parse_container", + + "get_current_comp", + "comp_lock_and_undo_chunk", + + # workio + "open_file", + "save_file", + "current_file", + "has_unsaved_changes", + "file_extensions", + "work_root", # lib + "maintained_selection", "get_additional_data", "update_frame_range", diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 7afcdd82ea..5d97f83032 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -1,8 +1,13 @@ +import os import sys +import re +import contextlib from Qt import QtGui -import avalon.fusion + +import avalon.api from avalon import io +from .pipeline import get_current_comp, comp_lock_and_undo_chunk self = sys.modules[__name__] self._project = None @@ -24,7 +29,7 @@ def update_frame_range(start, end, comp=None, set_render_range=True): """ if not comp: - comp = avalon.fusion.get_current_comp() + comp = get_current_comp() attrs = { "COMPN_GlobalStart": start, @@ -37,7 +42,7 @@ def update_frame_range(start, end, comp=None, set_render_range=True): "COMPN_RenderEnd": end }) - with avalon.fusion.comp_lock_and_undo_chunk(comp): + with comp_lock_and_undo_chunk(comp): comp.SetAttrs(attrs) @@ -140,3 +145,51 @@ def switch_item(container, avalon.api.switch(container, representation) return representation + + +@contextlib.contextmanager +def maintained_selection(): + comp = get_current_comp() + previous_selection = comp.GetToolList(True).values() + try: + yield + finally: + flow = comp.CurrentFrame.FlowView + flow.Select() # No args equals clearing selection + if previous_selection: + for tool in previous_selection: + flow.Select(tool, True) + + +def get_frame_path(path): + """Get filename for the Fusion Saver with padded number as '#' + + >>> get_frame_path("C:/test.exr") + ('C:/test', 4, '.exr') + + >>> get_frame_path("filename.00.tif") + ('filename.', 2, '.tif') + + >>> get_frame_path("foobar35.tif") + ('foobar', 2, '.tif') + + Args: + path (str): The path to render to. + + Returns: + tuple: head, padding, tail (extension) + + """ + filename, ext = os.path.splitext(path) + + # Find a final number group + match = re.match('.*?([0-9]+)$', filename) + if match: + padding = len(match.group(1)) + # remove number from end since fusion + # will swap it with the frame number + filename = filename[:-padding] + else: + padding = 4 # default Fusion padding + + return filename, padding, ext diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index d1c01163b1..11b6c2e273 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -3,6 +3,7 @@ import sys from Qt import QtWidgets, QtCore +from openpype import style from openpype.tools.utils import host_tools from openpype.hosts.fusion.scripts import ( @@ -128,7 +129,6 @@ class OpenPypeMenu(QtWidgets.QWidget): host_tools.show_library_loader() def on_rendernode_clicked(self): - from avalon import style print("Clicked Set Render Mode") if self.render_mode_widget is None: window = set_rendermode.SetRenderMode() diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 6b16339e53..70aac69b44 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -2,9 +2,14 @@ Basic avalon integration """ import os +import sys +import logging +import contextlib + +import pyblish.api +import avalon.api +from avalon.pipeline import AVALON_CONTAINER_ID -from avalon import api as avalon -from pyblish import api as pyblish from openpype.api import Logger import openpype.hosts.fusion @@ -19,6 +24,14 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") +class CompLogHandler(logging.Handler): + def emit(self, record): + entry = self.format(record) + comp = get_current_comp() + if comp: + comp.Print(entry) + + def install(): """Install fusion-specific functionality of avalon-core. @@ -30,25 +43,39 @@ def install(): See the Maya equivalent for inspiration on how to implement this. """ + # Remove all handlers associated with the root logger object, because + # that one sometimes logs as "warnings" incorrectly. + for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) + + # Attach default logging handler that prints to active comp + logger = logging.getLogger() + formatter = logging.Formatter(fmt="%(message)s\n") + handler = CompLogHandler() + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(logging.DEBUG) # Disable all families except for the ones we explicitly want to see family_states = ["imagesequence", "camera", "pointcache"] - avalon.data["familiesStateDefault"] = False - avalon.data["familiesStateToggled"] = family_states + avalon.api.data["familiesStateDefault"] = False + avalon.api.data["familiesStateToggled"] = family_states log.info("openpype.hosts.fusion installed") - pyblish.register_host("fusion") - pyblish.register_plugin_path(PUBLISH_PATH) + pyblish.api.register_host("fusion") + pyblish.api.register_plugin_path(PUBLISH_PATH) log.info("Registering Fusion plug-ins..") - avalon.register_plugin_path(avalon.Loader, LOAD_PATH) - avalon.register_plugin_path(avalon.Creator, CREATE_PATH) - avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) + avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) + avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH) - pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) + pyblish.api.register_callback( + "instanceToggled", on_pyblish_instance_toggled + ) def uninstall(): @@ -62,22 +89,19 @@ def uninstall(): modifying the menu or registered families. """ - pyblish.deregister_host("fusion") - pyblish.deregister_plugin_path(PUBLISH_PATH) + pyblish.api.deregister_host("fusion") + pyblish.api.deregister_plugin_path(PUBLISH_PATH) log.info("Deregistering Fusion plug-ins..") - avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) - avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) - avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH) + avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) + avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH) - pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) + pyblish.api.deregister_callback("instanceToggled", on_pyblish_instance_toggled) def on_pyblish_instance_toggled(instance, new_value, old_value): """Toggle saver tool passthrough states on instance toggles.""" - - from avalon.fusion import comp_lock_and_undo_chunk - comp = instance.context.data.get("currentComp") if not comp: return @@ -97,3 +121,106 @@ def on_pyblish_instance_toggled(instance, new_value, old_value): current = attrs["TOOLB_PassThrough"] if current != passthrough: tool.SetAttrs({"TOOLB_PassThrough": passthrough}) + + +def ls(): + """List containers from active Fusion scene + + This is the host-equivalent of api.ls(), but instead of listing + assets on disk, it lists assets already loaded in Fusion; once loaded + they are called 'containers' + + Yields: + dict: container + + """ + + comp = get_current_comp() + tools = comp.GetToolList(False, "Loader").values() + + for tool in tools: + container = parse_container(tool) + if container: + yield container + + +def imprint_container(tool, + name, + namespace, + context, + loader=None): + """Imprint a Loader with metadata + + Containerisation enables a tracking of version, author and origin + for loaded assets. + + Arguments: + tool (object): The node in Fusion to imprint as container, usually a + Loader. + name (str): Name of resulting assembly + namespace (str): Namespace under which to host container + context (dict): Asset information + loader (str, optional): Name of loader used to produce this container. + + Returns: + None + + """ + + data = [ + ("schema", "openpype:container-2.0"), + ("id", AVALON_CONTAINER_ID), + ("name", str(name)), + ("namespace", str(namespace)), + ("loader", str(loader)), + ("representation", str(context["representation"]["_id"])), + ] + + for key, value in data: + tool.SetData("avalon.{}".format(key), value) + + +def parse_container(tool): + """Returns imprinted container data of a tool + + This reads the imprinted data from `imprint_container`. + + """ + + data = tool.GetData('avalon') + if not isinstance(data, dict): + return + + # If not all required data return the empty container + required = ['schema', 'id', 'name', + 'namespace', 'loader', 'representation'] + if not all(key in data for key in required): + return + + container = {key: data[key] for key in required} + + # Store the tool's name + container["objectName"] = tool.Name + + # Store reference to the tool object + container["_tool"] = tool + + return container + + +def get_current_comp(): + """Hack to get current comp in this session""" + fusion = getattr(sys.modules["__main__"], "fusion", None) + return fusion.CurrentComp if fusion else None + + +@contextlib.contextmanager +def comp_lock_and_undo_chunk(comp, undo_queue_name="Script CMD"): + """Lock comp and open an undo chunk during the context""" + try: + comp.Lock() + comp.StartUndo(undo_queue_name) + yield + finally: + comp.Unlock() + comp.EndUndo() diff --git a/openpype/hosts/fusion/api/workio.py b/openpype/hosts/fusion/api/workio.py new file mode 100644 index 0000000000..ec9ac7481a --- /dev/null +++ b/openpype/hosts/fusion/api/workio.py @@ -0,0 +1,45 @@ +"""Host API required Work Files tool""" +import sys +import os +from avalon import api +from .pipeline import get_current_comp + + +def file_extensions(): + return api.HOST_WORKFILE_EXTENSIONS["fusion"] + + +def has_unsaved_changes(): + comp = get_current_comp() + return comp.GetAttrs()["COMPB_Modified"] + + +def save_file(filepath): + comp = get_current_comp() + comp.Save(filepath) + + +def open_file(filepath): + # Hack to get fusion, see + # openpype.hosts.fusion.api.pipeline.get_current_comp() + fusion = getattr(sys.modules["__main__"], "fusion", None) + + return fusion.LoadComp(filepath) + + +def current_file(): + comp = get_current_comp() + current_filepath = comp.GetAttrs()["COMPS_FileName"] + if not current_filepath: + return None + + return current_filepath + + +def work_root(session): + work_dir = session["AVALON_WORKDIR"] + scene_dir = session.get("AVALON_SCENEDIR") + if scene_dir: + return os.path.join(work_dir, scene_dir) + else: + return work_dir diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_exr_saver.py index 077e77c059..253dac773e 100644 --- a/openpype/hosts/fusion/plugins/create/create_exr_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_exr_saver.py @@ -1,7 +1,10 @@ import os import openpype.api -from avalon import fusion +from openpype.hosts.fusion.api import ( + get_current_comp, + comp_lock_and_undo_chunk +) class CreateOpenEXRSaver(openpype.api.Creator): @@ -15,7 +18,7 @@ class CreateOpenEXRSaver(openpype.api.Creator): file_format = "OpenEXRFormat" - comp = fusion.get_current_comp() + comp = get_current_comp() # todo: improve method of getting current environment # todo: pref avalon.Session over os.environ @@ -25,7 +28,7 @@ class CreateOpenEXRSaver(openpype.api.Creator): filename = "{}..tiff".format(self.name) filepath = os.path.join(workdir, "render", filename) - with fusion.comp_lock_and_undo_chunk(comp): + with comp_lock_and_undo_chunk(comp): args = (-32768, -32768) # Magical position numbers saver = comp.AddTool("Saver", *args) saver.SetAttrs({"TOOLS_Name": self.name}) diff --git a/openpype/hosts/fusion/plugins/inventory/select_containers.py b/openpype/hosts/fusion/plugins/inventory/select_containers.py index 2f7b3e5809..294c134505 100644 --- a/openpype/hosts/fusion/plugins/inventory/select_containers.py +++ b/openpype/hosts/fusion/plugins/inventory/select_containers.py @@ -8,15 +8,17 @@ class FusionSelectContainers(api.InventoryAction): color = "#d8d8d8" def process(self, containers): - - import avalon.fusion + from openpype.hosts.fusion.api import ( + get_current_comp, + comp_lock_and_undo_chunk + ) tools = [i["_tool"] for i in containers] - comp = avalon.fusion.get_current_comp() + comp = get_current_comp() flow = comp.CurrentFrame.FlowView - with avalon.fusion.comp_lock_and_undo_chunk(comp, self.label): + with comp_lock_and_undo_chunk(comp, self.label): # Clear selection flow.Select() diff --git a/openpype/hosts/fusion/plugins/inventory/set_tool_color.py b/openpype/hosts/fusion/plugins/inventory/set_tool_color.py index 9fc7012db7..2f5ae4d241 100644 --- a/openpype/hosts/fusion/plugins/inventory/set_tool_color.py +++ b/openpype/hosts/fusion/plugins/inventory/set_tool_color.py @@ -1,7 +1,11 @@ -from avalon import api, style +from avalon import api from Qt import QtGui, QtWidgets -import avalon.fusion +from openpype import style +from openpype.hosts.fusion.api import ( + get_current_comp, + comp_lock_and_undo_chunk +) class FusionSetToolColor(api.InventoryAction): @@ -16,7 +20,7 @@ class FusionSetToolColor(api.InventoryAction): """Color all selected tools the selected colors""" result = [] - comp = avalon.fusion.get_current_comp() + comp = get_current_comp() # Get tool color first = containers[0] @@ -33,7 +37,7 @@ class FusionSetToolColor(api.InventoryAction): if not picked_color: return - with avalon.fusion.comp_lock_and_undo_chunk(comp): + with comp_lock_and_undo_chunk(comp): for container in containers: # Convert color to RGB 0-1 floats rgb_f = picked_color.getRgbF() diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index 8f5be75484..667d50ed45 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -1,12 +1,15 @@ import os import contextlib -from avalon import api -import avalon.io as io +from avalon import api, io -from avalon import fusion +from openpype.hosts.fusion.api import ( + imprint_container, + get_current_comp, + comp_lock_and_undo_chunk +) -comp = fusion.get_current_comp() +comp = get_current_comp() @contextlib.contextmanager @@ -126,13 +129,6 @@ class FusionLoadSequence(api.Loader): color = "orange" def load(self, context, name, namespace, data): - - from avalon.fusion import ( - imprint_container, - get_current_comp, - comp_lock_and_undo_chunk - ) - # Fallback to asset name when namespace is None if namespace is None: namespace = context['asset']['name'] @@ -204,8 +200,6 @@ class FusionLoadSequence(api.Loader): """ - from avalon.fusion import comp_lock_and_undo_chunk - tool = container["_tool"] assert tool.ID == "Loader", "Must be Loader" comp = tool.Comp() @@ -247,9 +241,6 @@ class FusionLoadSequence(api.Loader): tool.SetData("avalon.representation", str(representation["_id"])) def remove(self, container): - - from avalon.fusion import comp_lock_and_undo_chunk - tool = container["_tool"] assert tool.ID == "Loader", "Must be Loader" comp = tool.Comp() diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp.py b/openpype/hosts/fusion/plugins/publish/collect_comp.py index 1cf182c8ca..dfa540fa7f 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp.py @@ -2,7 +2,7 @@ import os import pyblish.api -from avalon import fusion +from openpype.hosts.fusion.api import get_current_comp class CollectCurrentCompFusion(pyblish.api.ContextPlugin): @@ -15,7 +15,7 @@ class CollectCurrentCompFusion(pyblish.api.ContextPlugin): def process(self, context): """Collect all image sequence tools""" - current_comp = fusion.get_current_comp() + current_comp = get_current_comp() assert current_comp, "Must have active Fusion composition" context.data["currentComp"] = current_comp diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 3c7224e65c..b2192d1dd9 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -34,7 +34,7 @@ class CollectInstances(pyblish.api.ContextPlugin): def process(self, context): """Collect all image sequence tools""" - from avalon.fusion.lib import get_frame_path + from openpype.hosts.fusion.api.lib import get_frame_path comp = context.data["currentComp"] diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index 19449ead67..601c2ffccf 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -1,9 +1,9 @@ import os -import pyblish.api - -import avalon.fusion as fusion from pprint import pformat +import pyblish.api +from openpype.hosts.fusion.api import comp_lock_and_undo_chunk + class Fusionlocal(pyblish.api.InstancePlugin): """Render the current Fusion composition locally. @@ -39,7 +39,7 @@ class Fusionlocal(pyblish.api.InstancePlugin): self.log.info("Start frame: {}".format(frame_start)) self.log.info("End frame: {}".format(frame_end)) - with fusion.comp_lock_and_undo_chunk(current_comp): + with comp_lock_and_undo_chunk(current_comp): result = current_comp.Render() if "representations" not in instance.data: diff --git a/openpype/hosts/fusion/plugins/publish/submit_deadline.py b/openpype/hosts/fusion/plugins/publish/submit_deadline.py index 28671295ab..9da99dd9e2 100644 --- a/openpype/hosts/fusion/plugins/publish/submit_deadline.py +++ b/openpype/hosts/fusion/plugins/publish/submit_deadline.py @@ -2,8 +2,9 @@ import os import json import getpass +import requests + from avalon import api -from avalon.vendor import requests import pyblish.api @@ -30,7 +31,7 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): else: context.data[key] = True - from avalon.fusion.lib import get_frame_path + from openpype.hosts.fusion.api.lib import get_frame_path deadline_url = ( context.data["system_settings"] diff --git a/openpype/hosts/fusion/scripts/duplicate_with_inputs.py b/openpype/hosts/fusion/scripts/duplicate_with_inputs.py index 992dd2cd2d..21da6c24d8 100644 --- a/openpype/hosts/fusion/scripts/duplicate_with_inputs.py +++ b/openpype/hosts/fusion/scripts/duplicate_with_inputs.py @@ -1,4 +1,7 @@ -from avalon import fusion +from openpype.hosts.fusion.api import ( + comp_lock_and_undo_chunk, + get_current_comp +) def is_connected(input): @@ -9,12 +12,12 @@ def is_connected(input): def duplicate_with_input_connections(): """Duplicate selected tools with incoming connections.""" - comp = fusion.get_current_comp() + comp = get_current_comp() original_tools = comp.GetToolList(True).values() if not original_tools: return # nothing selected - with fusion.comp_lock_and_undo_chunk( + with comp_lock_and_undo_chunk( comp, "Duplicate With Input Connections"): # Generate duplicates diff --git a/openpype/hosts/fusion/scripts/fusion_switch_shot.py b/openpype/hosts/fusion/scripts/fusion_switch_shot.py index efb3cad800..041b53f6c9 100644 --- a/openpype/hosts/fusion/scripts/fusion_switch_shot.py +++ b/openpype/hosts/fusion/scripts/fusion_switch_shot.py @@ -4,12 +4,12 @@ import sys import logging # Pipeline imports -from avalon import api, io, pipeline -import avalon.fusion +import avalon.api +from avalon import io, pipeline -# Config imports -import openpype.lib as pype -import openpype.hosts.fusion.api.lib as fusion_lib +from openpype.lib import version_up +from openpype.hosts.fusion import api +from openpype.hosts.fusion.api import lib log = logging.getLogger("Update Slap Comp") @@ -87,7 +87,7 @@ def _format_filepath(session): # Create new unique filepath if os.path.exists(new_filepath): - new_filepath = pype.version_up(new_filepath) + new_filepath = version_up(new_filepath) return new_filepath @@ -119,7 +119,7 @@ def _update_savers(comp, session): comp.Print("New renders to: %s\n" % renders) - with avalon.fusion.comp_lock_and_undo_chunk(comp): + with api.comp_lock_and_undo_chunk(comp): savers = comp.GetToolList(False, "Saver").values() for saver in savers: filepath = saver.GetAttrs("TOOLST_Clip_Name")[1.0] @@ -185,7 +185,7 @@ def update_frame_range(comp, representations): start = min(v["data"]["frameStart"] for v in versions) end = max(v["data"]["frameEnd"] for v in versions) - fusion_lib.update_frame_range(start, end, comp=comp) + lib.update_frame_range(start, end, comp=comp) def switch(asset_name, filepath=None, new=True): @@ -215,11 +215,11 @@ def switch(asset_name, filepath=None, new=True): # Get current project self._project = io.find_one({"type": "project", - "name": api.Session["AVALON_PROJECT"]}) + "name": avalon.api.Session["AVALON_PROJECT"]}) # Go to comp if not filepath: - current_comp = avalon.fusion.get_current_comp() + current_comp = api.get_current_comp() assert current_comp is not None, "Could not find current comp" else: fusion = _get_fusion_instance() @@ -227,14 +227,14 @@ def switch(asset_name, filepath=None, new=True): assert current_comp is not None, ( "Fusion could not load '{}'").format(filepath) - host = api.registered_host() + host = avalon.api.registered_host() containers = list(host.ls()) assert containers, "Nothing to update" representations = [] for container in containers: try: - representation = fusion_lib.switch_item( + representation = lib.switch_item( container, asset_name=asset_name) representations.append(representation) @@ -246,7 +246,7 @@ def switch(asset_name, filepath=None, new=True): current_comp.Print(message) # Build the session to switch to - switch_to_session = api.Session.copy() + switch_to_session = avalon.api.Session.copy() switch_to_session["AVALON_ASSET"] = asset['name'] if new: @@ -255,7 +255,7 @@ def switch(asset_name, filepath=None, new=True): # Update savers output based on new session _update_savers(current_comp, switch_to_session) else: - comp_path = pype.version_up(filepath) + comp_path = version_up(filepath) current_comp.Print(comp_path) @@ -288,7 +288,7 @@ if __name__ == '__main__': args, unknown = parser.parse_args() - api.install(avalon.fusion) + avalon.api.install(api) switch(args.asset_name, args.file_path) sys.exit(0) diff --git a/openpype/hosts/fusion/scripts/set_rendermode.py b/openpype/hosts/fusion/scripts/set_rendermode.py index 73eec528a2..77a2d8e945 100644 --- a/openpype/hosts/fusion/scripts/set_rendermode.py +++ b/openpype/hosts/fusion/scripts/set_rendermode.py @@ -1,6 +1,6 @@ from Qt import QtWidgets from avalon.vendor import qtawesome -import avalon.fusion as avalon +from openpype.hosts.fusion.api import get_current_comp _help = {"local": "Render the comp on your own machine and publish " @@ -14,7 +14,7 @@ class SetRenderMode(QtWidgets.QWidget): def __init__(self, parent=None): QtWidgets.QWidget.__init__(self, parent) - self._comp = avalon.get_current_comp() + self._comp = get_current_comp() self._comp_name = self._get_comp_name() self.setWindowTitle("Set Render Mode") @@ -79,7 +79,7 @@ class SetRenderMode(QtWidgets.QWidget): def update(self): """Update all information in the UI""" - self._comp = avalon.get_current_comp() + self._comp = get_current_comp() self._comp_name = self._get_comp_name() self.comp_information.setText(self._comp_name) diff --git a/openpype/hosts/fusion/utility_scripts/32bit/backgrounds_selected_to32bit.py b/openpype/hosts/fusion/utility_scripts/32bit/backgrounds_selected_to32bit.py index 90e08c4edb..1a0a9911ea 100644 --- a/openpype/hosts/fusion/utility_scripts/32bit/backgrounds_selected_to32bit.py +++ b/openpype/hosts/fusion/utility_scripts/32bit/backgrounds_selected_to32bit.py @@ -1,10 +1,11 @@ -from avalon.fusion import comp_lock_and_undo_chunk - -from avalon import fusion -comp = fusion.get_current_comp() +from openpype.hosts.fusion.api import ( + comp_lock_and_undo_chunk, + get_current_comp +) def main(): + comp = get_current_comp() """Set all selected backgrounds to 32 bit""" with comp_lock_and_undo_chunk(comp, 'Selected Backgrounds to 32bit'): tools = comp.GetToolList(True, "Background").values() diff --git a/openpype/hosts/fusion/utility_scripts/32bit/backgrounds_to32bit.py b/openpype/hosts/fusion/utility_scripts/32bit/backgrounds_to32bit.py index 30ce36fee7..c2eea505e5 100644 --- a/openpype/hosts/fusion/utility_scripts/32bit/backgrounds_to32bit.py +++ b/openpype/hosts/fusion/utility_scripts/32bit/backgrounds_to32bit.py @@ -1,9 +1,11 @@ -from avalon.fusion import comp_lock_and_undo_chunk -from avalon import fusion -comp = fusion.get_current_comp() +from openpype.hosts.fusion.api import ( + comp_lock_and_undo_chunk, + get_current_comp +) def main(): + comp = get_current_comp() """Set all backgrounds to 32 bit""" with comp_lock_and_undo_chunk(comp, 'Backgrounds to 32bit'): tools = comp.GetToolList(False, "Background").values() diff --git a/openpype/hosts/fusion/utility_scripts/32bit/loaders_selected_to32bit.py b/openpype/hosts/fusion/utility_scripts/32bit/loaders_selected_to32bit.py index 403febf19b..2118767f4d 100644 --- a/openpype/hosts/fusion/utility_scripts/32bit/loaders_selected_to32bit.py +++ b/openpype/hosts/fusion/utility_scripts/32bit/loaders_selected_to32bit.py @@ -1,9 +1,11 @@ -from avalon.fusion import comp_lock_and_undo_chunk -from avalon import fusion -comp = fusion.get_current_comp() +from openpype.hosts.fusion.api import ( + comp_lock_and_undo_chunk, + get_current_comp +) def main(): + comp = get_current_comp() """Set all selected loaders to 32 bit""" with comp_lock_and_undo_chunk(comp, 'Selected Loaders to 32bit'): tools = comp.GetToolList(True, "Loader").values() diff --git a/openpype/hosts/fusion/utility_scripts/32bit/loaders_to32bit.py b/openpype/hosts/fusion/utility_scripts/32bit/loaders_to32bit.py index e5670fe41b..7dd1f66a5e 100644 --- a/openpype/hosts/fusion/utility_scripts/32bit/loaders_to32bit.py +++ b/openpype/hosts/fusion/utility_scripts/32bit/loaders_to32bit.py @@ -1,9 +1,11 @@ -from avalon.fusion import comp_lock_and_undo_chunk -from avalon import fusion -comp = fusion.get_current_comp() +from openpype.hosts.fusion.api import ( + comp_lock_and_undo_chunk, + get_current_comp +) def main(): + comp = get_current_comp() """Set all loaders to 32 bit""" with comp_lock_and_undo_chunk(comp, 'Loaders to 32bit'): tools = comp.GetToolList(False, "Loader").values() diff --git a/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py b/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py index 4f804f9bce..4b5e8f91a0 100644 --- a/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py +++ b/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py @@ -8,13 +8,15 @@ log = Logger().get_logger(__name__) def main(env): + import avalon.api + from openpype.hosts.fusion import api from openpype.hosts.fusion.api import menu - import avalon.fusion + # Registers pype's Global pyblish plugins openpype.install() # activate resolve from pype - avalon.api.install(avalon.fusion) + avalon.api.install(api) log.info(f"Avalon registered hosts: {avalon.api.registered_host()}") diff --git a/openpype/hosts/fusion/utility_scripts/switch_ui.py b/openpype/hosts/fusion/utility_scripts/switch_ui.py index 2be91af32a..fe324d9a41 100644 --- a/openpype/hosts/fusion/utility_scripts/switch_ui.py +++ b/openpype/hosts/fusion/utility_scripts/switch_ui.py @@ -4,13 +4,12 @@ import logging from Qt import QtWidgets, QtCore -import avalon.io as io -import avalon.api as api -import avalon.pipeline as pipeline -import avalon.fusion -import avalon.style as style +import avalon.api +from avalon import io, pipeline from avalon.vendor import qtawesome as qta +from openpype import style +from openpype.hosts.fusion import api log = logging.getLogger("Fusion Switch Shot") @@ -150,7 +149,7 @@ class App(QtWidgets.QWidget): if not self._use_current.isChecked(): file_name = self._comps.itemData(self._comps.currentIndex()) else: - comp = avalon.fusion.get_current_comp() + comp = api.get_current_comp() file_name = comp.GetAttrs("COMPS_FileName") asset = self._assets.currentText() @@ -161,11 +160,11 @@ class App(QtWidgets.QWidget): def _get_context_directory(self): project = io.find_one({"type": "project", - "name": api.Session["AVALON_PROJECT"]}, + "name": avalon.api.Session["AVALON_PROJECT"]}, projection={"config": True}) template = project["config"]["template"]["work"] - dir = pipeline._format_work_template(template, api.Session) + dir = pipeline._format_work_template(template, avalon.api.Session) return dir @@ -174,7 +173,7 @@ class App(QtWidgets.QWidget): return items def collect_assets(self): - return list(io.find({"type": "asset", "silo": "film"})) + return list(io.find({"type": "asset"}, {"name": True})) def populate_comp_box(self, files): """Ensure we display the filename only but the path is stored as well @@ -193,7 +192,7 @@ class App(QtWidgets.QWidget): if __name__ == '__main__': import sys - api.install(avalon.fusion) + avalon.api.install(api) app = QtWidgets.QApplication(sys.argv) window = App() diff --git a/openpype/hosts/fusion/utility_scripts/update_loader_ranges.py b/openpype/hosts/fusion/utility_scripts/update_loader_ranges.py index 9ddf1e6dc6..3d2d1ecfa6 100644 --- a/openpype/hosts/fusion/utility_scripts/update_loader_ranges.py +++ b/openpype/hosts/fusion/utility_scripts/update_loader_ranges.py @@ -5,12 +5,15 @@ Warning: settings of the Loader. So use this at your own risk. """ -from avalon import fusion +from openpype.hosts.fusion.api.pipeline import ( + get_current_comp, + comp_lock_and_undo_chunk +) def update_loader_ranges(): - comp = fusion.get_current_comp() - with fusion.comp_lock_and_undo_chunk(comp, "Reload clip time ranges"): + comp = get_current_comp() + with comp_lock_and_undo_chunk(comp, "Reload clip time ranges"): tools = comp.GetToolList(True, "Loader").values() for tool in tools: From 8dca5c9ccb429b39216c2de40d1697945ed71976 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 14 Feb 2022 14:26:30 +0100 Subject: [PATCH 04/20] Allow to add new app versions for Fusion in Setting --- .../host_settings/schema_fusion.json | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_fusion.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_fusion.json index 58f37fa99b..f62b439536 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_fusion.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_fusion.json @@ -20,24 +20,20 @@ "type": "raw-json" }, { - "type": "dict", + "type": "dict-modifiable", "key": "variants", - "children": [ - { - "type": "schema_template", - "name": "template_host_variant", - "template_data": [ - { - "app_variant_label": "16", - "app_variant": "16" - }, - { - "app_variant_label": "9", - "app_variant": "9" - } - ] - } - ] + "collapsible_key": true, + "use_label_wrap": false, + "object_type": { + "type": "dict", + "collapsible": true, + "children": [ + { + "type": "schema_template", + "name": "template_host_variant_items" + } + ] + } } ] -} +} \ No newline at end of file From 4ae8d298a6af68e9a3a8a9bed86dc8bd8e28c5dd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 14 Feb 2022 14:31:44 +0100 Subject: [PATCH 05/20] Add Fusion 17 to defaults --- .../system_settings/applications.json | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 317ed061b4..bfa1bd5cdc 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -690,15 +690,31 @@ "OPENPYPE_LOG_NO_COLORS": "Yes" }, "variants": { - "16": { - "enabled": true, - "variant_label": "16", + "17": { "use_python_2": false, "executables": { + "windows": [ + "C:\\Program Files\\Blackmagic Design\\Fusion 17\\Fusion.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { "windows": [], "darwin": [], "linux": [] }, + "environment": {} + }, + "16": { + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Blackmagic Design\\Fusion 16\\Fusion.exe" + ], + "darwin": [], + "linux": [] + }, "arguments": { "windows": [], "darwin": [], @@ -707,8 +723,6 @@ "environment": {} }, "9": { - "enabled": true, - "variant_label": "9", "use_python_2": false, "executables": { "windows": [ From 846082f50211f3ffe1b4a8245b27174d42dd7e71 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 14 Feb 2022 14:32:07 +0100 Subject: [PATCH 06/20] hound fixes --- openpype/hosts/fusion/api/pipeline.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 70aac69b44..ec68707401 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -95,9 +95,13 @@ def uninstall(): avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) - avalon.api.deregister_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH) + avalon.api.deregister_plugin_path( + avalon.api.InventoryAction, INVENTORY_PATH + ) - pyblish.api.deregister_callback("instanceToggled", on_pyblish_instance_toggled) + pyblish.api.deregister_callback( + "instanceToggled", on_pyblish_instance_toggled + ) def on_pyblish_instance_toggled(instance, new_value, old_value): From 00e1a042f4ac821ef840312f4ea10570c69130bb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 14 Feb 2022 14:52:41 +0100 Subject: [PATCH 07/20] Add "render" family so loaders and actions work on rendered output --- openpype/hosts/fusion/plugins/load/actions.py | 6 ++++-- openpype/hosts/fusion/plugins/load/load_sequence.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/load/actions.py b/openpype/hosts/fusion/plugins/load/actions.py index e1cdc6a41b..6af99e4c56 100644 --- a/openpype/hosts/fusion/plugins/load/actions.py +++ b/openpype/hosts/fusion/plugins/load/actions.py @@ -12,7 +12,8 @@ class FusionSetFrameRangeLoader(api.Loader): "camera", "imagesequence", "yeticache", - "pointcache"] + "pointcache", + "render"] representations = ["*"] label = "Set frame range" @@ -45,7 +46,8 @@ class FusionSetFrameRangeWithHandlesLoader(api.Loader): "camera", "imagesequence", "yeticache", - "pointcache"] + "pointcache", + "render"] representations = ["*"] label = "Set frame range (with handles)" diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index 667d50ed45..5d9cad61bb 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -120,7 +120,7 @@ def loader_shift(loader, frame, relative=True): class FusionLoadSequence(api.Loader): """Load image sequence into Fusion""" - families = ["imagesequence", "review"] + families = ["imagesequence", "review", "render"] representations = ["*"] label = "Load sequence" From 4f9699fa000ceaba0679e5491e26419cac65d8f4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 14 Feb 2022 15:16:53 +0100 Subject: [PATCH 08/20] Fix loader update --- openpype/hosts/fusion/plugins/load/load_sequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index 5d9cad61bb..ea118585bf 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -204,7 +204,7 @@ class FusionLoadSequence(api.Loader): assert tool.ID == "Loader", "Must be Loader" comp = tool.Comp() - root = api.get_representation_path(representation) + root = os.path.dirname(api.get_representation_path(representation)) path = self._get_first_image(root) # Get start frame from version data From 379b93e47d83343fc2bc75deed50cc1b4e6ceeb3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 14 Feb 2022 15:20:55 +0100 Subject: [PATCH 09/20] Rename "Inventory..." to "Manage..." to align with other hosts --- openpype/hosts/fusion/api/menu.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 11b6c2e273..6234322d7f 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -59,7 +59,7 @@ class OpenPypeMenu(QtWidgets.QWidget): create_btn = QtWidgets.QPushButton("Create...", self) publish_btn = QtWidgets.QPushButton("Publish...", self) load_btn = QtWidgets.QPushButton("Load...", self) - inventory_btn = QtWidgets.QPushButton("Inventory...", self) + manager_btn = QtWidgets.QPushButton("Manage...", self) libload_btn = QtWidgets.QPushButton("Library...", self) rendermode_btn = QtWidgets.QPushButton("Set render mode...", self) duplicate_with_inputs_btn = QtWidgets.QPushButton( @@ -76,7 +76,7 @@ class OpenPypeMenu(QtWidgets.QWidget): layout.addWidget(create_btn) layout.addWidget(publish_btn) layout.addWidget(load_btn) - layout.addWidget(inventory_btn) + layout.addWidget(manager_btn) layout.addWidget(Spacer(15, self)) @@ -97,7 +97,7 @@ class OpenPypeMenu(QtWidgets.QWidget): create_btn.clicked.connect(self.on_create_clicked) publish_btn.clicked.connect(self.on_publish_clicked) load_btn.clicked.connect(self.on_load_clicked) - inventory_btn.clicked.connect(self.on_inventory_clicked) + manager_btn.clicked.connect(self.on_manager_clicked) libload_btn.clicked.connect(self.on_libload_clicked) rendermode_btn.clicked.connect(self.on_rendernode_clicked) duplicate_with_inputs_btn.clicked.connect( @@ -120,8 +120,8 @@ class OpenPypeMenu(QtWidgets.QWidget): print("Clicked Load") host_tools.show_loader(use_context=True) - def on_inventory_clicked(self): - print("Clicked Inventory") + def on_manager_clicked(self): + print("Clicked Manager") host_tools.show_scene_inventory() def on_libload_clicked(self): From bb88f5e8049aa0d34d474d54b673efb1f791ba61 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 14 Feb 2022 17:41:52 +0100 Subject: [PATCH 10/20] removed 'use_python_2' from fusion settings schema --- .../system_settings/applications.json | 23 ++++++++++++------- .../host_settings/schema_fusion.json | 5 ++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index bfa1bd5cdc..593fab7034 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -691,7 +691,6 @@ }, "variants": { "17": { - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\Blackmagic Design\\Fusion 17\\Fusion.exe" @@ -707,7 +706,6 @@ "environment": {} }, "16": { - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\Blackmagic Design\\Fusion 16\\Fusion.exe" @@ -723,7 +721,6 @@ "environment": {} }, "9": { - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\Blackmagic Design\\Fusion 9\\Fusion.exe" @@ -952,8 +949,12 @@ "enabled": true, "variant_label": "21", "executables": { - "windows": ["c:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 21 Premium\\win64\\bin\\HarmonyPremium.exe"], - "darwin": ["/Applications/Toon Boom Harmony 21 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium"], + "windows": [ + "c:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 21 Premium\\win64\\bin\\HarmonyPremium.exe" + ], + "darwin": [ + "/Applications/Toon Boom Harmony 21 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium" + ], "linux": [] }, "arguments": { @@ -967,8 +968,12 @@ "enabled": true, "variant_label": "20", "executables": { - "windows": ["c:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 20 Premium\\win64\\bin\\HarmonyPremium.exe"], - "darwin": ["/Applications/Toon Boom Harmony 20 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium"], + "windows": [ + "c:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 20 Premium\\win64\\bin\\HarmonyPremium.exe" + ], + "darwin": [ + "/Applications/Toon Boom Harmony 20 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium" + ], "linux": [] }, "arguments": { @@ -982,7 +987,9 @@ "enabled": true, "variant_label": "17", "executables": { - "windows": ["c:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 17 Premium\\win64\\bin\\HarmonyPremium.exe"], + "windows": [ + "c:\\Program Files (x86)\\Toon Boom Animation\\Toon Boom Harmony 17 Premium\\win64\\bin\\HarmonyPremium.exe" + ], "darwin": [ "/Applications/Toon Boom Harmony 17 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium" ], diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_fusion.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_fusion.json index f62b439536..5960da7774 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_fusion.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_fusion.json @@ -30,10 +30,11 @@ "children": [ { "type": "schema_template", - "name": "template_host_variant_items" + "name": "template_host_variant_items", + "skip_paths": ["use_python_2"] } ] } } ] -} \ No newline at end of file +} From 9873b46faa9cc49691a7823ffa6231b8878665f8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 14 Feb 2022 19:01:05 +0100 Subject: [PATCH 11/20] check env values properly --- .../hosts/fusion/hooks/pre_fusion_setup.py | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index b104b3c081..e635a0ea74 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -14,27 +14,40 @@ class FusionPrelaunch(PreLaunchHook): def execute(self): # making sure python 3.6 is installed at provided path - py36_dir = os.path.normpath(self.launch_context.env.get("PYTHON36", "")) + py36_dir = self.launch_context.env.get("PYTHON36") + if not py36_dir: + raise ApplicationLaunchFailed( + "Required environment variable \"PYTHON36\" is not set." + "\n\nFusion implementation requires to have" + " installed Python 3.6" + ) + + py36_dir = os.path.normpath(py36_dir) if not os.path.isdir(py36_dir): raise ApplicationLaunchFailed( "Python 3.6 is not installed at the provided path.\n" - "Either make sure the 'environments/fusion.json' has " - "'PYTHON36' set corectly or make sure Python 3.6 is installed " - f"in the given path.\n\nPYTHON36: {py36_dir}" + "Either make sure the environments in fusion settings has" + " 'PYTHON36' set corectly or make sure Python 3.6 is installed" + f" in the given path.\n\nPYTHON36: {py36_dir}" ) self.log.info(f"Path to Fusion Python folder: '{py36_dir}'...") self.launch_context.env["PYTHON36"] = py36_dir + utility_dir = self.launch_context.env.get("FUSION_UTILITY_SCRIPTS_DIR") + if not utility_dir: + raise ApplicationLaunchFailed( + "Required Fusion utility script dir environment variable" + " \"FUSION_UTILITY_SCRIPTS_DIR\" is not set." + ) + # setting utility scripts dir for scripts syncing - us_dir = os.path.normpath( - self.launch_context.env.get("FUSION_UTILITY_SCRIPTS_DIR", "") - ) - if not os.path.isdir(us_dir): + utility_dir = os.path.normpath(utility_dir) + if not os.path.isdir(utility_dir): raise ApplicationLaunchFailed( "Fusion utility script dir does not exist. Either make sure " - "the 'environments/fusion.json' has " - "'FUSION_UTILITY_SCRIPTS_DIR' set correctly or reinstall " - f"Fusion.\n\nFUSION_UTILITY_SCRIPTS_DIR: '{us_dir}'" + "the environments in fusion settings has" + " 'FUSION_UTILITY_SCRIPTS_DIR' set correctly or reinstall " + f"Fusion.\n\nFUSION_UTILITY_SCRIPTS_DIR: '{utility_dir}'" ) self._sync_utility_scripts(self.launch_context.env) From d5092340667fa08c674bb8e8877cbfeb14e54ed4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Feb 2022 15:29:40 +0100 Subject: [PATCH 12/20] OP-2646 - fix for proper parsing of ffmpeg output arguments Previous implementation was too simple, values that contained flags anywhere inside were triggering it. --- openpype/plugins/publish/extract_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index d223c31291..5f286a53e6 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -694,13 +694,13 @@ class ExtractReview(pyblish.api.InstancePlugin): audio_args_dentifiers = ["-af", "-filter:a"] for arg in tuple(output_args): for identifier in video_args_dentifiers: - if identifier in arg: + if arg.startswith("{} ".format(identifier)): output_args.remove(arg) arg = arg.replace(identifier, "").strip() video_filters.append(arg) for identifier in audio_args_dentifiers: - if identifier in arg: + if arg.startswith("{} ".format(identifier)): output_args.remove(arg) arg = arg.replace(identifier, "").strip() audio_filters.append(arg) From b292438e2b8d36852b459a834740fa8d2056c335 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Feb 2022 16:17:11 +0100 Subject: [PATCH 13/20] OP-2646 - added barebone unit test for ffmpeg_full_args_filters --- .../plugins/publish/test_extract_review.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/unit/openpype/plugins/publish/test_extract_review.py diff --git a/tests/unit/openpype/plugins/publish/test_extract_review.py b/tests/unit/openpype/plugins/publish/test_extract_review.py new file mode 100644 index 0000000000..0a48d3d059 --- /dev/null +++ b/tests/unit/openpype/plugins/publish/test_extract_review.py @@ -0,0 +1,30 @@ +from openpype.plugins.publish.extract_review import ExtractReview + + +def test_fix_ffmpeg_full_args_filters(): + """Tests because of wrong resolution of audio filters.""" + plugin = ExtractReview() + output_arg = "c:/test-afbdc" + ret = plugin.ffmpeg_full_args([], [], [], [output_arg]) + assert len(ret) == 2, "Parsed wrong" + assert ret[-1] == output_arg + + ret = plugin.ffmpeg_full_args([], [], ["adeclick"], [output_arg]) + assert len(ret) == 4, "Parsed wrong" + assert ret[-1] == output_arg + assert ret[-2] == '"adeclick"' + assert ret[-3] == "-filter:a" + + ret = plugin.ffmpeg_full_args([], [], [], [output_arg, "-af adeclick"]) + assert len(ret) == 4, "Parsed wrong" + assert ret[-1] == output_arg + assert ret[-2] == '"adeclick"' + assert ret[-3] == "-filter:a" + + ret = plugin.ffmpeg_full_args([], [], ["adeclick"], + [output_arg, "-af adeclick"]) + assert len(ret) == 4, "Parsed wrong" + assert ret[-1] == output_arg + assert ret[-2] == '"adeclick,addeclick"' # TODO fix this duplication + assert ret[-3] == "-filter:a" + From 21784799c318cf339fbbd8fea7ed1cd4dae3a921 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Feb 2022 16:19:00 +0100 Subject: [PATCH 14/20] OP-2646 - modified readme --- tests/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/README.md b/tests/README.md index d0578f8059..6fb86cd027 100644 --- a/tests/README.md +++ b/tests/README.md @@ -12,8 +12,6 @@ Structure: How to run: ---------- -- single test class could be run by PyCharm and its pytest runner directly -- OR - use Openpype command 'runtests' from command line (`.venv` in ${OPENPYPE_ROOT} must be activated to use configured Python!) -- `${OPENPYPE_ROOT}/python start.py runtests` From 260257089c86cfd457e4564b1ae647b670194798 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Feb 2022 16:26:17 +0100 Subject: [PATCH 15/20] OP-2646 - Hound --- tests/unit/openpype/plugins/publish/test_extract_review.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/openpype/plugins/publish/test_extract_review.py b/tests/unit/openpype/plugins/publish/test_extract_review.py index 0a48d3d059..48317f0eb9 100644 --- a/tests/unit/openpype/plugins/publish/test_extract_review.py +++ b/tests/unit/openpype/plugins/publish/test_extract_review.py @@ -25,6 +25,5 @@ def test_fix_ffmpeg_full_args_filters(): [output_arg, "-af adeclick"]) assert len(ret) == 4, "Parsed wrong" assert ret[-1] == output_arg - assert ret[-2] == '"adeclick,addeclick"' # TODO fix this duplication + assert ret[-2] == '"adeclick,adeclick"' # TODO fix this duplication assert ret[-3] == "-filter:a" - From 30b8908d6bf105fbd77133087cb0273f3d0c279b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 15 Feb 2022 18:13:41 +0100 Subject: [PATCH 16/20] fix selected items --- openpype/tools/loader/app.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 1d5dee21f3..2443489f3c 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -1,4 +1,5 @@ import sys +import collections from Qt import QtWidgets, QtCore from avalon import api, io, pipeline @@ -378,15 +379,19 @@ class LoaderWindow(QtWidgets.QDialog): version_docs = [] if rows: + items = collections.deque() for index in rows: if not index or not index.isValid(): continue item = index.data(subsets.model.ItemRole) - if item is None: - continue + if item is not None: + items.append(item) + + while items: + item = items.popleft() if item.get("isGroup") or item.get("isMerged"): for child in item.children(): - version_docs.append(child["version_document"]) + items.append(child) else: version_docs.append(item["version_document"]) From 83b19949f960112fcbb4f676a7d2c2e635a8dfe6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 15 Feb 2022 18:14:19 +0100 Subject: [PATCH 17/20] added ability to get selected items under group item --- openpype/tools/loader/lib.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/openpype/tools/loader/lib.py b/openpype/tools/loader/lib.py index 8f18c01913..857b40faf7 100644 --- a/openpype/tools/loader/lib.py +++ b/openpype/tools/loader/lib.py @@ -1,4 +1,5 @@ import inspect +import collections from Qt import QtGui from avalon.vendor import qtawesome @@ -19,23 +20,21 @@ def change_visibility(model, view, column_name, visible): def get_selected_items(rows, item_role): - items = [] + output = [] + items = collections.deque() for row_index in rows: item = row_index.data(item_role) - if item.get("isGroup"): - continue - - elif item.get("isMerged"): - for idx in range(row_index.model().rowCount(row_index)): - child_index = row_index.child(idx, 0) - item = child_index.data(item_role) - if item not in items: - items.append(item) + items.append(item) + while items: + item = items.popleft() + if item.get("isGroup") or item.get("isMerged"): + for child in item.children(): + items.append(child) else: - if item not in items: - items.append(item) - return items + if item not in output: + output.append(item) + return output def get_options(action, loader, parent, repre_contexts): From cd6dc59f1440104868deb09908d1796f19752515 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 15 Feb 2022 18:14:48 +0100 Subject: [PATCH 18/20] asset underline colors can be cleared again --- openpype/tools/loader/app.py | 5 ++--- openpype/tools/loader/model.py | 15 ++++++--------- openpype/tools/utils/assets_widget.py | 13 ++++++++----- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 2443489f3c..619dcbcb06 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -288,9 +288,7 @@ class LoaderWindow(QtWidgets.QDialog): on selection change so they match current selection. """ # TODO do not touch inner attributes of asset widget - last_asset_ids = self.data["state"]["assetIds"] - if last_asset_ids: - self._assets_widget.clear_underlines() + self._assets_widget.clear_underlines() def _assetschanged(self): """Selected assets have changed""" @@ -329,6 +327,7 @@ class LoaderWindow(QtWidgets.QDialog): asset_ids = self.data["state"]["assetIds"] # Skip setting colors if not asset multiselection if not asset_ids or len(asset_ids) < 2: + self.clear_assets_underlines() self._versionschanged() return diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 96a52fce97..1f9223f596 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -524,9 +524,13 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): return self._fill_subset_items( - asset_docs_by_id, subset_docs_by_id, last_versions_by_subset_id, + asset_docs_by_id, + subset_docs_by_id, + last_versions_by_subset_id, repre_info_by_version_id ) + self.endResetModel() + self.refreshed.emit(True) def create_multiasset_group( self, subset_name, asset_ids, subset_counter, parent_item=None @@ -538,7 +542,6 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): merge_group.update({ "subset": "{} ({})".format(subset_name, len(asset_ids)), "isMerged": True, - "childRow": 0, "subsetColor": subset_color, "assetIds": list(asset_ids), "icon": qtawesome.icon( @@ -547,7 +550,6 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): ) }) - subset_counter += 1 self.add_child(merge_group, parent_item) return merge_group @@ -567,8 +569,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): group_item = Item() group_item.update({ "subset": group_name, - "isGroup": True, - "childRow": 0 + "isGroup": True }) group_item.update(group_data) @@ -666,9 +667,6 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): index = self.index(item.row(), 0, parent_index) self.set_version(index, last_version) - self.endResetModel() - self.refreshed.emit(True) - def data(self, index, role): if not index.isValid(): return @@ -1139,7 +1137,6 @@ class RepresentationModel(TreeModel, BaseRepresentationModel): "_id": doc["_id"], "name": doc["name"], "isMerged": True, - "childRow": 0, "active_site_name": self.active_site, "remote_site_name": self.remote_site, "icon": qtawesome.icon( diff --git a/openpype/tools/utils/assets_widget.py b/openpype/tools/utils/assets_widget.py index 55e34285fc..be9f442d10 100644 --- a/openpype/tools/utils/assets_widget.py +++ b/openpype/tools/utils/assets_widget.py @@ -303,7 +303,7 @@ class AssetModel(QtGui.QStandardItemModel): self._doc_fetched.connect(self._on_docs_fetched) - self._items_with_color_by_id = {} + self._item_ids_with_color = set() self._items_by_asset_id = {} self._last_project_name = None @@ -382,9 +382,11 @@ class AssetModel(QtGui.QStandardItemModel): self._stop_fetch_thread() def clear_underlines(self): - for asset_id in tuple(self._items_with_color_by_id.keys()): - item = self._items_with_color_by_id.pop(asset_id) - item.setData(None, ASSET_UNDERLINE_COLORS_ROLE) + for asset_id in set(self._item_ids_with_color): + self._item_ids_with_color.remove(asset_id) + item = self._items_by_asset_id.get(asset_id) + if item is not None: + item.setData(None, ASSET_UNDERLINE_COLORS_ROLE) def set_underline_colors(self, colors_by_asset_id): self.clear_underlines() @@ -394,12 +396,13 @@ class AssetModel(QtGui.QStandardItemModel): if item is None: continue item.setData(colors, ASSET_UNDERLINE_COLORS_ROLE) + self._item_ids_with_color.add(asset_id) def _clear_items(self): root_item = self.invisibleRootItem() root_item.removeRows(0, root_item.rowCount()) self._items_by_asset_id = {} - self._items_with_color_by_id = {} + self._item_ids_with_color = set() def _on_docs_fetched(self): # Make sure refreshing did not change From 19bfb53ad9455a8276e6ddeea3b29a3145e94982 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 16 Feb 2022 10:38:05 +0100 Subject: [PATCH 19/20] don't use ItemRole in loader --- openpype/tools/libraryloader/app.py | 39 +++--------- openpype/tools/loader/app.py | 56 +++--------------- openpype/tools/loader/lib.py | 18 ------ openpype/tools/loader/model.py | 19 +++++- openpype/tools/loader/widgets.py | 92 ++++++++++++++++++----------- 5 files changed, 92 insertions(+), 132 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index d030aa903d..9f8845f30f 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -396,9 +396,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self._versionschanged() return - selected_subsets = self._subsets_widget.selected_subsets( - _merged=True, _other=False - ) + selected_subsets = self._subsets_widget.get_selected_merge_items() asset_colors = {} asset_ids = [] @@ -423,35 +421,14 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self._versionschanged() def _versionschanged(self): - selection = self._subsets_widget.view.selectionModel() - - # Active must be in the selected rows otherwise we - # assume it's not actually an "active" current index. - version_docs = None + items = self._subsets_widget.get_selected_subsets() version_doc = None - active = selection.currentIndex() - rows = selection.selectedRows(column=active.column()) - if active and active in rows: - item = active.data(self._subsets_widget.model.ItemRole) - if ( - item is not None - and not (item.get("isGroup") or item.get("isMerged")) - ): - version_doc = item["version_document"] - - if rows: - version_docs = [] - for index in rows: - if not index or not index.isValid(): - continue - item = index.data(self._subsets_widget.model.ItemRole) - if ( - item is None - or item.get("isGroup") - or item.get("isMerged") - ): - continue - version_docs.append(item["version_document"]) + version_docs = [] + for item in items: + doc = item["version_document"] + version_docs.append(doc) + if version_doc is None: + version_doc = doc self._version_info_widget.set_version(version_doc) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 619dcbcb06..6f67f0c641 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -331,9 +331,7 @@ class LoaderWindow(QtWidgets.QDialog): self._versionschanged() return - selected_subsets = self._subsets_widget.selected_subsets( - _merged=True, _other=False - ) + selected_subsets = self._subsets_widget.get_selected_merge_items() asset_colors = {} asset_ids = [] @@ -358,41 +356,16 @@ class LoaderWindow(QtWidgets.QDialog): self._versionschanged() def _versionschanged(self): - subsets = self._subsets_widget - selection = subsets.view.selectionModel() - - # Active must be in the selected rows otherwise we - # assume it's not actually an "active" current index. + items = self._subsets_widget.get_selected_subsets() version_doc = None - active = selection.currentIndex() - rows = selection.selectedRows(column=active.column()) - if active: - if active in rows: - item = active.data(subsets.model.ItemRole) - if ( - item is not None and - not (item.get("isGroup") or item.get("isMerged")) - ): - version_doc = item["version_document"] - self._version_info_widget.set_version(version_doc) - version_docs = [] - if rows: - items = collections.deque() - for index in rows: - if not index or not index.isValid(): - continue - item = index.data(subsets.model.ItemRole) - if item is not None: - items.append(item) + for item in items: + doc = item["version_document"] + version_docs.append(doc) + if version_doc is None: + version_doc = doc - while items: - item = items.popleft() - if item.get("isGroup") or item.get("isMerged"): - for child in item.children(): - items.append(child) - else: - version_docs.append(item["version_document"]) + self._version_info_widget.set_version(version_doc) thumbnail_src_ids = [ version_doc["_id"] @@ -484,18 +457,7 @@ class LoaderWindow(QtWidgets.QDialog): self.echo("Grouping not enabled.") return - selected = [] - merged_items = [] - for item in subsets.selected_subsets(_merged=True): - if item.get("isMerged"): - merged_items.append(item) - else: - selected.append(item) - - for merged_item in merged_items: - for child_item in merged_item.children(): - selected.append(child_item) - + selected = self._subsets_widget.get_selected_subsets() if not selected: self.echo("No selected subset.") return diff --git a/openpype/tools/loader/lib.py b/openpype/tools/loader/lib.py index 857b40faf7..60e7672340 100644 --- a/openpype/tools/loader/lib.py +++ b/openpype/tools/loader/lib.py @@ -19,24 +19,6 @@ def change_visibility(model, view, column_name, visible): view.setColumnHidden(index, not visible) -def get_selected_items(rows, item_role): - output = [] - items = collections.deque() - for row_index in rows: - item = row_index.data(item_role) - items.append(item) - - while items: - item = items.popleft() - if item.get("isGroup") or item.get("isMerged"): - for child in item.children(): - items.append(child) - else: - if item not in output: - output.append(item) - return output - - def get_options(action, loader, parent, repre_contexts): """Provides dialog to select value from loader provided options. diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 1f9223f596..f34eb157ab 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -1,6 +1,7 @@ import copy import re import math +from uuid import uuid4 from avalon import ( style, @@ -22,6 +23,8 @@ from openpype.tools.utils.constants import ( REMOTE_AVAILABILITY_ROLE ) +ITEM_ID_ROLE = QtCore.Qt.UserRole + 90 + def is_filtering_recursible(): """Does Qt binding support recursive filtering for QSortFilterProxyModel? @@ -179,6 +182,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): self._icons = { "subset": qtawesome.icon("fa.file-o", color=style.colors.default) } + self._items_by_id = {} self._doc_fetching_thread = None self._doc_fetching_stop = False @@ -188,6 +192,15 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): self.refresh() + def get_item_by_id(self, item_id): + return self._items_by_id.get(item_id) + + def add_child(self, new_item, *args, **kwargs): + item_id = str(uuid4()) + new_item["id"] = item_id + self._items_by_id[item_id] = new_item + super(SubsetsModel, self).add_child(new_item, *args, **kwargs) + def set_assets(self, asset_ids): self._asset_ids = asset_ids self.refresh() @@ -486,7 +499,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): def refresh(self): self.stop_fetch_thread() self.clear() - + self._items_by_id = {} self.reset_sync_server() if not self._asset_ids: @@ -497,6 +510,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): def on_doc_fetched(self): self.clear() + self._items_by_id = {} self.beginResetModel() asset_docs_by_id = self._doc_payload.get( @@ -672,6 +686,9 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): return item = index.internalPointer() + if role == ITEM_ID_ROLE: + return item["id"] + if role == self.SortDescendingRole: if item.get("isGroup"): # Ensure groups be on top when sorting by descending order diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index ed130f765c..f60341af2f 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -34,7 +34,8 @@ from .model import ( SubsetFilterProxyModel, FamiliesFilterProxyModel, RepresentationModel, - RepresentationSortProxyModel + RepresentationSortProxyModel, + ITEM_ID_ROLE ) from . import lib @@ -351,6 +352,59 @@ class SubsetWidget(QtWidgets.QWidget): lib.change_visibility(self.model, self.view, "repre_info", enabled) + def get_selected_items(self): + selection_model = self.view.selectionModel() + indexes = selection_model.selectedIndexes() + + item_ids = set() + for index in indexes: + item_id = index.data(ITEM_ID_ROLE) + if item_id is not None: + item_ids.add(item_id) + + output = [] + for item_id in item_ids: + item = self.model.get_item_by_id(item_id) + if item is not None: + output.append(item) + return output + + def get_selected_merge_items(self): + output = [] + items = collections.deque(self.get_selected_items()) + + item_ids = set() + while items: + item = items.popleft() + if item.get("isGroup"): + for child in item.children(): + items.appendleft(child) + + elif item.get("isMerged"): + item_id = item["id"] + if item_id not in item_ids: + item_ids.add(item_id) + output.append(item) + + return output + + def get_selected_subsets(self): + output = [] + items = collections.deque(self.get_selected_items()) + + item_ids = set() + while items: + item = items.popleft() + if item.get("isGroup") or item.get("isMerged"): + for child in item.children(): + items.appendleft(child) + else: + item_id = item["id"] + if item_id not in item_ids: + item_ids.add(item_id) + output.append(item) + return output + def on_context_menu(self, point): """Shows menu with loader actions on Right-click. @@ -367,10 +421,7 @@ class SubsetWidget(QtWidgets.QWidget): return # Get selected subsets without groups - selection = self.view.selectionModel() - rows = selection.selectedRows(column=0) - - items = lib.get_selected_items(rows, self.model.ItemRole) + items = self.get_selected_subsets() # Get all representation->loader combinations available for the # index under the cursor, so we can list the user the options. @@ -539,35 +590,6 @@ class SubsetWidget(QtWidgets.QWidget): box = LoadErrorMessageBox(error_info, self) box.show() - def selected_subsets(self, _groups=False, _merged=False, _other=True): - selection = self.view.selectionModel() - rows = selection.selectedRows(column=0) - - subsets = list() - if not any([_groups, _merged, _other]): - self.echo(( - "This is a BUG: Selected_subsets args must contain" - " at least one value set to True" - )) - return subsets - - for row in rows: - item = row.data(self.model.ItemRole) - if item.get("isGroup"): - if not _groups: - continue - - elif item.get("isMerged"): - if not _merged: - continue - else: - if not _other: - continue - - subsets.append(item) - - return subsets - def group_subsets(self, name, asset_ids, items): field = "data.subsetGroup" @@ -1279,7 +1301,7 @@ class RepresentationWidget(QtWidgets.QWidget): selection = self.tree_view.selectionModel() rows = selection.selectedRows(column=0) - items = lib.get_selected_items(rows, self.model.ItemRole) + items = self.get_selected_subsets() selected_side = self._get_selected_side(point_index, rows) From 4748e111a2a15d88b68ac23b613555a9570e1dde Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 16 Feb 2022 10:40:03 +0100 Subject: [PATCH 20/20] hound fixes --- openpype/tools/loader/app.py | 1 - openpype/tools/loader/lib.py | 1 - 2 files changed, 2 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 6f67f0c641..aa743b05fe 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -1,5 +1,4 @@ import sys -import collections from Qt import QtWidgets, QtCore from avalon import api, io, pipeline diff --git a/openpype/tools/loader/lib.py b/openpype/tools/loader/lib.py index 60e7672340..180dee3eb5 100644 --- a/openpype/tools/loader/lib.py +++ b/openpype/tools/loader/lib.py @@ -1,5 +1,4 @@ import inspect -import collections from Qt import QtGui from avalon.vendor import qtawesome