diff --git a/openpype/hosts/fusion/__init__.py b/openpype/hosts/fusion/__init__.py index e69de29bb2d..02befa76e27 100644 --- a/openpype/hosts/fusion/__init__.py +++ b/openpype/hosts/fusion/__init__.py @@ -0,0 +1,5 @@ +import os + +HOST_DIR = os.path.dirname( + os.path.abspath(__file__) +) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 5d2efb49112..a4c3db95010 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -3,25 +3,16 @@ from Qt import QtWidgets, QtCore +from avalon import api from openpype.tools.utils import host_tools +from openpype.style import load_stylesheet from openpype.hosts.fusion.scripts import ( set_rendermode, duplicate_with_inputs ) -def load_stylesheet(): - path = os.path.join(os.path.dirname(__file__), "menu_style.qss") - if not os.path.exists(path): - print("Unable to load stylesheet, file not found in resources") - return "" - - with open(path, "r") as file_stream: - stylesheet = file_stream.read() - return stylesheet - - class Spacer(QtWidgets.QWidget): def __init__(self, height, *args, **kwargs): super(Spacer, self).__init__(*args, **kwargs) @@ -54,13 +45,22 @@ def __init__(self, *args, **kwargs): ) self.render_mode_widget = None self.setWindowTitle("OpenPype") - workfiles_btn = QtWidgets.QPushButton("Workfiles ...", self) - create_btn = QtWidgets.QPushButton("Create ...", self) - publish_btn = QtWidgets.QPushButton("Publish ...", self) - load_btn = QtWidgets.QPushButton("Load ...", self) - inventory_btn = QtWidgets.QPushButton("Inventory ...", self) - libload_btn = QtWidgets.QPushButton("Library ...", self) - rendermode_btn = QtWidgets.QPushButton("Set render mode ...", self) + + asset_label = QtWidgets.QLabel("Context", self) + asset_label.setStyleSheet("""QLabel { + font-size: 14px; + font-weight: 600; + color: #5f9fb8; + }""") + asset_label.setAlignment(QtCore.Qt.AlignHCenter) + + workfiles_btn = QtWidgets.QPushButton("Work Files", self) + create_btn = QtWidgets.QPushButton("Create...", self) + load_btn = QtWidgets.QPushButton("Load...", self) + publish_btn = QtWidgets.QPushButton("Publish...", self) + inventory_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( "Duplicate with input connections", self ) @@ -71,10 +71,17 @@ def __init__(self, *args, **kwargs): layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(10, 20, 10, 20) + layout.addWidget(asset_label) + + layout.addWidget(Spacer(15, self)) + layout.addWidget(workfiles_btn) + + layout.addWidget(Spacer(15, self)) + layout.addWidget(create_btn) - layout.addWidget(publish_btn) layout.addWidget(load_btn) + layout.addWidget(publish_btn) layout.addWidget(inventory_btn) layout.addWidget(Spacer(15, self)) @@ -92,6 +99,9 @@ def __init__(self, *args, **kwargs): self.setLayout(layout) + # Store reference so we can update the label + self.asset_label = asset_label + workfiles_btn.clicked.connect(self.on_workfile_clicked) create_btn.clicked.connect(self.on_create_clicked) publish_btn.clicked.connect(self.on_publish_clicked) @@ -103,6 +113,26 @@ def __init__(self, *args, **kwargs): self.on_duplicate_with_inputs_clicked) reset_resolution_btn.clicked.connect(self.on_reset_resolution_clicked) + self._callbacks = [] + self.register_callback("taskChanged", self.on_task_changed) + self.on_task_changed() + + def on_task_changed(self): + # Update current context label + label = api.Session["AVALON_ASSET"] + self.asset_label.setText(label) + + def register_callback(self, name, fn): + + # Create a wrapper callback that we only store + # for as long as we want it to persist as callback + callback = lambda *args: fn() + self._callbacks.append(callback) + api.on(name, callback) + + def deregister_all_callbacks(self): + self._callbacks[:] = [] + def on_workfile_clicked(self): print("Clicked Workfile") host_tools.show_workfiles() diff --git a/openpype/hosts/fusion/api/menu_style.qss b/openpype/hosts/fusion/api/menu_style.qss deleted file mode 100644 index 12c474b070d..00000000000 --- a/openpype/hosts/fusion/api/menu_style.qss +++ /dev/null @@ -1,29 +0,0 @@ -QWidget { - background-color: #282828; - border-radius: 3; -} - -QPushButton { - border: 1px solid #090909; - background-color: #201f1f; - color: #ffffff; - padding: 5; -} - -QPushButton:focus { - background-color: "#171717"; - color: #d0d0d0; -} - -QPushButton:hover { - background-color: "#171717"; - color: #e64b3d; -} - -#OpenPypeMenu { - border: 1px solid #fef9ef; -} - -#Spacer { - background-color: #282828; -} diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 6b16339e53a..9e57d249d5f 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -30,14 +30,6 @@ def install(): See the Maya equivalent for inspiration on how to implement this. """ - - # 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 - log.info("openpype.hosts.fusion installed") pyblish.register_host("fusion") @@ -48,6 +40,8 @@ def install(): avalon.register_plugin_path(avalon.Creator, CREATE_PATH) avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) + print(LOAD_PATH) + pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) diff --git a/openpype/hosts/fusion/api/utils.py b/openpype/hosts/fusion/api/utils.py index 5605323b1e8..6b7c6ef78f8 100644 --- a/openpype/hosts/fusion/api/utils.py +++ b/openpype/hosts/fusion/api/utils.py @@ -13,74 +13,12 @@ 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) + # todo(roy): This currently does nothing. Remove? log.info("Fusion Pype wrapper has been installed") diff --git a/openpype/hosts/fusion/deploy/Config/openpype_menu.fu b/openpype/hosts/fusion/deploy/Config/openpype_menu.fu new file mode 100644 index 00000000000..8b8d448259a --- /dev/null +++ b/openpype/hosts/fusion/deploy/Config/openpype_menu.fu @@ -0,0 +1,60 @@ +{ + Action + { + ID = "OpenPype_Menu", + Category = "OpenPype", + Name = "OpenPype Menu", + + Targets = + { + Composition = + { + Execute = _Lua [=[ + local scriptPath = app:MapPath("OpenPype:MenuScripts/openpype_menu.py") + if bmd.fileexists(scriptPath) == false then + print("[OpenPype Error] Can't run file: " .. scriptPath) + else + target:RunScript(scriptPath) + end + ]=], + }, + }, + }, + Action + { + ID = "OpenPype_Install_PySide2", + Category = "OpenPype", + Name = "Install PySide2", + + Targets = + { + Composition = + { + Execute = _Lua [=[ + local scriptPath = app:MapPath("OpenPype:MenuScripts/install_pyside2.py") + if bmd.fileexists(scriptPath) == false then + print("[OpenPype Error] Can't run file: " .. scriptPath) + else + target:RunScript(scriptPath) + end + ]=], + }, + }, + }, + Menus + { + Target = "ChildFrame", + + Before "Help" + { + Sub "OpenPype" + { + "OpenPype_Menu{}", + "_", + Sub "Admin" { + "OpenPype_Install_PySide2{}" + } + } + }, + }, +} diff --git a/openpype/hosts/fusion/deploy/MenuScripts/README.md b/openpype/hosts/fusion/deploy/MenuScripts/README.md new file mode 100644 index 00000000000..f87eaea4a29 --- /dev/null +++ b/openpype/hosts/fusion/deploy/MenuScripts/README.md @@ -0,0 +1,6 @@ +### OpenPype deploy MenuScripts + +Note that this `MenuScripts` is not an official Fusion folder. +OpenPype only uses this folder in `{fusion}/deploy/` to trigger the OpenPype menu actions. + +They are used in the actions defined in `.fu` files in `{fusion}/deploy/Config`. \ No newline at end of file diff --git a/openpype/hosts/fusion/deploy/MenuScripts/install_pyside2.py b/openpype/hosts/fusion/deploy/MenuScripts/install_pyside2.py new file mode 100644 index 00000000000..5419e4445a3 --- /dev/null +++ b/openpype/hosts/fusion/deploy/MenuScripts/install_pyside2.py @@ -0,0 +1,14 @@ +# This is just a quick hack for users running Py3 locally but having no +# Qt library installed +import os + +try: + print("Qt library found, nothing to do.") + from Qt import QtWidgets +except ImportError as exc: + print("Assuming no Qt library is installed..") + print('Installing PySide2 for Python 3.6: ' + f'{os.environ["FUSION16_PYTHON36_HOME"]}') + + import subprocess + subprocess.Popen(["pip", "install", "PySide2"]) diff --git a/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py b/openpype/hosts/fusion/deploy/MenuScripts/openpype_menu.py similarity index 53% rename from openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py rename to openpype/hosts/fusion/deploy/MenuScripts/openpype_menu.py index 4f804f9bce6..672f9b2406f 100644 --- a/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py +++ b/openpype/hosts/fusion/deploy/MenuScripts/openpype_menu.py @@ -8,6 +8,11 @@ def main(env): + # This script working directory starts in Fusion application folder. + # However the contents of that folder can conflict with Qt library dlls + # so we make sure to move out of it to avoid DLL Load Failed errors. + os.chdir("..") + from openpype.hosts.fusion.api import menu import avalon.fusion # Registers pype's Global pyblish plugins @@ -20,6 +25,11 @@ def main(env): menu.launch_openpype_menu() + # Initiate a QTimer to check if Fusion is still alive every X interval + # If Fusion is not found - kill itself + # todo(roy): Implement timer that ensures UI doesn't remain when e.g. + # Fusion closes down + if __name__ == "__main__": result = main(os.environ) diff --git a/openpype/hosts/fusion/deploy/fusion_shared.prefs b/openpype/hosts/fusion/deploy/fusion_shared.prefs new file mode 100644 index 00000000000..998c6a6d663 --- /dev/null +++ b/openpype/hosts/fusion/deploy/fusion_shared.prefs @@ -0,0 +1,19 @@ +{ +Locked = true, +Global = { + Paths = { + Map = { + ["OpenPype:"] = "$(OPENPYPE_FUSION)/deploy", + ["Reactor:"] = "$(REACTOR)", + + ["Config:"] = "UserPaths:Config;OpenPype:Config", + ["Scripts:"] = "UserPaths:Scripts;Reactor:System/Scripts;OpenPype:Scripts", + ["UserPaths:"] = "UserData:;AllData:;Fusion:;Reactor:Deploy" + }, + }, + Script = { + PythonVersion = 3, + Python3Forced = true + }, + }, +} \ No newline at end of file diff --git a/openpype/hosts/fusion/utility_scripts/32bit/backgrounds_selected_to32bit.py b/openpype/hosts/fusion/deploy/scripts/Comp/32bit/backgrounds_selected_to32bit.py similarity index 100% rename from openpype/hosts/fusion/utility_scripts/32bit/backgrounds_selected_to32bit.py rename to openpype/hosts/fusion/deploy/scripts/Comp/32bit/backgrounds_selected_to32bit.py diff --git a/openpype/hosts/fusion/utility_scripts/32bit/backgrounds_to32bit.py b/openpype/hosts/fusion/deploy/scripts/Comp/32bit/backgrounds_to32bit.py similarity index 100% rename from openpype/hosts/fusion/utility_scripts/32bit/backgrounds_to32bit.py rename to openpype/hosts/fusion/deploy/scripts/Comp/32bit/backgrounds_to32bit.py diff --git a/openpype/hosts/fusion/utility_scripts/32bit/loaders_selected_to32bit.py b/openpype/hosts/fusion/deploy/scripts/Comp/32bit/loaders_selected_to32bit.py similarity index 100% rename from openpype/hosts/fusion/utility_scripts/32bit/loaders_selected_to32bit.py rename to openpype/hosts/fusion/deploy/scripts/Comp/32bit/loaders_selected_to32bit.py diff --git a/openpype/hosts/fusion/utility_scripts/32bit/loaders_to32bit.py b/openpype/hosts/fusion/deploy/scripts/Comp/32bit/loaders_to32bit.py similarity index 100% rename from openpype/hosts/fusion/utility_scripts/32bit/loaders_to32bit.py rename to openpype/hosts/fusion/deploy/scripts/Comp/32bit/loaders_to32bit.py diff --git a/openpype/hosts/fusion/utility_scripts/switch_ui.py b/openpype/hosts/fusion/deploy/scripts/Comp/switch_ui.py similarity index 100% rename from openpype/hosts/fusion/utility_scripts/switch_ui.py rename to openpype/hosts/fusion/deploy/scripts/Comp/switch_ui.py diff --git a/openpype/hosts/fusion/utility_scripts/update_loader_ranges.py b/openpype/hosts/fusion/deploy/scripts/Comp/update_loader_ranges.py similarity index 100% rename from openpype/hosts/fusion/utility_scripts/update_loader_ranges.py rename to openpype/hosts/fusion/deploy/scripts/Comp/update_loader_ranges.py diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index 4a9dfaec15b..2054b263d90 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -2,6 +2,7 @@ import importlib from openpype.lib import PreLaunchHook, ApplicationLaunchFailed from openpype.hosts.fusion.api import utils +from openpype.hosts.fusion import HOST_DIR class FusionPrelaunch(PreLaunchHook): @@ -13,29 +14,37 @@ 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", "")) - if not os.path.isdir(py36_dir): + py36_var = "FUSION16_PYTHON36_HOME" + fusion_python36_home = self.launch_context.env.get(py36_var, "") + + self.log.info(f"Looking for Python 3.6 in: {fusion_python36_home}") + for path in fusion_python36_home.split(os.pathsep): + # Allow defining multiple paths to allow "fallback" to other + # path. But make to set only a single path as final variable. + py36_dir = os.path.normpath(path) + if os.path.isdir(py36_dir): + break + else: raise ApplicationLaunchFailed( - "Python 3.6 is not installed at the provided path.\n" + "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}" - ) - self.log.info(f"Path to Fusion Python folder: '{py36_dir}'...") - self.launch_context.env["PYTHON36"] = py36_dir - - # 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): - 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}'" + "'PYTHON36' set correctly or make sure Python 3.6 is installed " + f"in the given path.\n\nPYTHON36: {fusion_python36_home}" ) + self.log.info(f"Setting {py36_var}: '{py36_dir}'...") + self.launch_context.env[py36_var] = py36_dir + + # Add our Fusion Master Prefs which is the only way to customize + # Fusion to define where it can read custom scripts and tools from + self.log.info(f"Setting OPENPYPE_FUSION: {HOST_DIR}") + self.launch_context.env["OPENPYPE_FUSION"] = HOST_DIR + + pref_var = "FUSION16_MasterPrefs" # used by both Fu16 and Fu17 + prefs = os.path.join(HOST_DIR, "deploy", "fusion_shared.prefs") + self.log.info(f"Setting {pref_var}: {prefs}") + self.launch_context.env[pref_var] = prefs + try: __import__("avalon.fusion") __import__("pyblish") diff --git a/openpype/hosts/fusion/plugins/load/actions.py b/openpype/hosts/fusion/plugins/load/actions.py index e1cdc6a41b4..f92d572a05e 100644 --- a/openpype/hosts/fusion/plugins/load/actions.py +++ b/openpype/hosts/fusion/plugins/load/actions.py @@ -11,6 +11,7 @@ class FusionSetFrameRangeLoader(api.Loader): families = ["animation", "camera", "imagesequence", + "render" "yeticache", "pointcache"] representations = ["*"] @@ -44,6 +45,7 @@ class FusionSetFrameRangeWithHandlesLoader(api.Loader): families = ["animation", "camera", "imagesequence", + "render" "yeticache", "pointcache"] representations = ["*"] diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index 8f5be754840..5b5504de429 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -117,7 +117,9 @@ def loader_shift(loader, frame, relative=True): class FusionLoadSequence(api.Loader): """Load image sequence into Fusion""" - families = ["imagesequence", "review"] + families = ["imagesequence", + "render", + "review"] representations = ["*"] label = "Load sequence"