From 341379e546807e1dfa6794402e9dffcea3931088 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 May 2020 18:20:52 +0200 Subject: [PATCH] feat(resolve): wip integration workio, pipeline and basic avalon methods, menu, --- pype/hooks/resolve/prelaunch.py | 13 +- pype/plugins/resolve/publish/collect_host.py | 17 ++ pype/resolve/__init__.py | 106 ++++------ pype/resolve/action.py | 53 +++++ pype/resolve/lib.py | 109 ----------- pype/resolve/menu.py | 133 +++++++++++++ pype/resolve/menu_style.qss | 34 ++++ pype/resolve/pipeline.py | 184 ++++++++++++++++++ pype/resolve/plugin.py | 75 +++++++ pype/resolve/preload_console.py | 29 +++ .../resolve_utility_scripts/Pype_menu.py | 34 ++++ .../resolve_utility_scripts/README.markdown | 1 + .../resolve_utility_scripts/__test_gui.py | 111 +++++++++++ .../resolve_utility_scripts/__test_pyblish.py | 57 ++++++ .../__test_subprocess.py | 36 ++++ .../python_get_resolve.py | 34 ---- .../resolve_utility_scripts/resolveapitest.py | 72 ------- pype/resolve/resolve_utility_scripts/test.py | 8 - .../utility/pre_python_console_script.py | 26 --- pype/resolve/utility/python_get_resolve.py | 34 ---- pype/resolve/utils.py | 114 +++++++++++ pype/resolve/workio.py | 88 +++++++++ 22 files changed, 1014 insertions(+), 354 deletions(-) create mode 100644 pype/plugins/resolve/publish/collect_host.py create mode 100644 pype/resolve/action.py create mode 100644 pype/resolve/menu.py create mode 100644 pype/resolve/menu_style.qss create mode 100644 pype/resolve/pipeline.py create mode 100644 pype/resolve/plugin.py create mode 100644 pype/resolve/preload_console.py create mode 100644 pype/resolve/resolve_utility_scripts/Pype_menu.py create mode 100644 pype/resolve/resolve_utility_scripts/README.markdown create mode 100644 pype/resolve/resolve_utility_scripts/__test_gui.py create mode 100644 pype/resolve/resolve_utility_scripts/__test_pyblish.py create mode 100644 pype/resolve/resolve_utility_scripts/__test_subprocess.py delete mode 100644 pype/resolve/resolve_utility_scripts/python_get_resolve.py delete mode 100644 pype/resolve/resolve_utility_scripts/resolveapitest.py delete mode 100644 pype/resolve/resolve_utility_scripts/test.py delete mode 100644 pype/resolve/utility/pre_python_console_script.py delete mode 100644 pype/resolve/utility/python_get_resolve.py create mode 100644 pype/resolve/utils.py create mode 100644 pype/resolve/workio.py diff --git a/pype/hooks/resolve/prelaunch.py b/pype/hooks/resolve/prelaunch.py index e3e1c83077..d0b7448a41 100644 --- a/pype/hooks/resolve/prelaunch.py +++ b/pype/hooks/resolve/prelaunch.py @@ -2,7 +2,7 @@ import os import traceback from pype.lib import PypeHook from pypeapp import Logger -from pype.resolve import lib as rlib +from pype.resolve import utils class ResolvePrelaunch(PypeHook): @@ -27,14 +27,15 @@ class ResolvePrelaunch(PypeHook): env = os.environ # making sure pyton 3.6 is installed at provided path - py36_dir = os.path.normpath(env.get("PYTHON36_RES", "")) + py36_dir = os.path.normpath(env.get("PYTHON36_RESOLVE", "")) assert os.path.isdir(py36_dir), ( "Python 3.6 is not installed at the provided folder path. Either " "make sure the `environments\resolve.json` is having correctly set " - "`PYTHON36_RES` or make sure Python 3.6 is installed in given path." - f"\nPYTHON36_RES: `{py36_dir}`" + "`PYTHON36_RESOLVE` or make sure Python 3.6 is installed in given path." + f"\nPYTHON36_RESOLVE: `{py36_dir}`" ) - env["PYTHON36_RES"] = py36_dir + self.log.info(f"Path to Resolve Python folder: `{py36_dir}`...") + env["PYTHON36_RESOLVE"] = py36_dir # setting utility scripts dir for scripts syncing us_dir = os.path.normpath(env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "")) @@ -59,6 +60,6 @@ class ResolvePrelaunch(PypeHook): else: # Resolve Setup integration - rlib.setup(env) + utils.setup(env) return True diff --git a/pype/plugins/resolve/publish/collect_host.py b/pype/plugins/resolve/publish/collect_host.py new file mode 100644 index 0000000000..9119ba1f4f --- /dev/null +++ b/pype/plugins/resolve/publish/collect_host.py @@ -0,0 +1,17 @@ +import pyblish.api +from python_get_resolve import GetResolve + + +class CollectProject(pyblish.api.ContextPlugin): + """Collect Project object""" + + order = pyblish.api.CollectorOrder - 0.1 + label = "Collect Project" + hosts = ["resolve"] + + def process(self, context): + resolve = GetResolve() + PM = resolve.GetProjectManager() + P = PM.GetCurrentProject() + + self.log.info(P.GetName()) diff --git a/pype/resolve/__init__.py b/pype/resolve/__init__.py index 0683cfa92f..966d7aef4c 100644 --- a/pype/resolve/__init__.py +++ b/pype/resolve/__init__.py @@ -1,71 +1,47 @@ -import os -from avalon import api as avalon -from pyblish import api as pyblish -from pypeapp import Logger - - -from .lib import ( - setup, - reload_pipeline, +from .pipeline import ( + install, + uninstall, ls, - # LOAD_PATH, - # CREATE_PATH, - PUBLISH_PATH + containerise, + reload_pipeline, + publish, + launch_workfiles_app ) +from .utils import ( + setup, + get_resolve_module +) + +from .workio import ( + open_file, + save_file, + current_file, + has_unsaved_changes, + file_extensions, + work_root +) + +# from .lib import ( +# +# ) + __all__ = [ - "setup", + "install", + "uninstall", + "ls", + "containerise", "reload_pipeline", - "ls" + "publish", + "launch_workfiles_app", + + "setup", + "get_resolve_module", + + "open_file", + "save_file", + "current_file", + "has_unsaved_changes", + "file_extensions", + "work_root" ] - -log = Logger().get_logger(__name__, "resolve") - - -def install(): - """Install resolve-specific functionality of avalon-core. - - This is where you install menus and register families, data - and loaders into resolve. - - It is called automatically when installing via `api.install(resolve)`. - - 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", - "mov" - ] - avalon.data["familiesStateDefault"] = False - avalon.data["familiesStateToggled"] = family_states - - log.info("pype.resolve installed") - - pyblish.register_host("resolve") - pyblish.register_plugin_path(PUBLISH_PATH) - log.info("Registering Premiera plug-ins..") - - # avalon.register_plugin_path(avalon.Loader, LOAD_PATH) - # avalon.register_plugin_path(avalon.Creator, CREATE_PATH) - - -def uninstall(): - """Uninstall all tha was installed - - This is where you undo everything that was done in `install()`. - That means, removing menus, deregistering families and data - and everything. It should be as though `install()` was never run, - because odds are calling this function means the user is interested - in re-installing shortly afterwards. If, for example, he has been - modifying the menu or registered families. - - """ - pyblish.deregister_host("resolve") - pyblish.deregister_plugin_path(PUBLISH_PATH) - log.info("Deregistering Premiera plug-ins..") - - # avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) - # avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) diff --git a/pype/resolve/action.py b/pype/resolve/action.py new file mode 100644 index 0000000000..94d0f5eb67 --- /dev/null +++ b/pype/resolve/action.py @@ -0,0 +1,53 @@ +# absolute_import is needed to counter the `module has no cmds error` in Maya +from __future__ import absolute_import + +import pyblish.api + + +from ..action import get_errored_instances_from_context + + +class SelectInvalidAction(pyblish.api.Action): + """Select invalid clips in Resolve timeline when plug-in failed. + + To retrieve the invalid nodes this assumes a static `get_invalid()` + method is available on the plugin. + + """ + label = "Select invalid" + on = "failed" # This action is only available on a failed plug-in + icon = "search" # Icon from Awesome Icon + + def process(self, context, plugin): + + try: + from pype.resolve.utils import get_resolve_module + resolve = get_resolve_module() + except ImportError: + raise ImportError("Current host is not Resolve") + + errored_instances = get_errored_instances_from_context(context) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + + # Get the invalid nodes for the plug-ins + self.log.info("Finding invalid clips..") + invalid = list() + for instance in instances: + invalid_nodes = plugin.get_invalid(instance) + if invalid_nodes: + if isinstance(invalid_nodes, (list, tuple)): + invalid.extend(invalid_nodes) + else: + self.log.warning("Plug-in returned to be invalid, " + "but has no selectable nodes.") + + # Ensure unique (process each node only once) + invalid = list(set(invalid)) + + if invalid: + self.log.info("Selecting invalid nodes: %s" % ", ".join(invalid)) + # TODO: select resolve timeline track items in current timeline + else: + self.log.info("No invalid nodes found.") diff --git a/pype/resolve/lib.py b/pype/resolve/lib.py index 95586b00eb..e69de29bb2 100644 --- a/pype/resolve/lib.py +++ b/pype/resolve/lib.py @@ -1,109 +0,0 @@ -import os -import sys -import shutil - -from avalon import api -from pype.widgets.message_window import message -from pypeapp import Logger - -log = Logger().get_logger(__name__, "resolve") - -self = sys.modules[__name__] - -AVALON_CONFIG = os.environ["AVALON_CONFIG"] -PARENT_DIR = os.path.dirname(__file__) -PACKAGE_DIR = os.path.dirname(PARENT_DIR) -PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") - -self.UTILITY_SCRIPTS = os.path.join(PARENT_DIR, "resolve_utility_scripts") - -self.PUBLISH_PATH = os.path.join( - PLUGINS_DIR, "resolve", "publish" -).replace("\\", "/") - -if os.getenv("PUBLISH_PATH", None): - if self.PUBLISH_PATH not in os.environ["PUBLISH_PATH"]: - os.environ["PUBLISH_PATH"] = os.pathsep.join( - os.environ["PUBLISH_PATH"].split(os.pathsep) + - [self.PUBLISH_PATH] - ) -else: - os.environ["PUBLISH_PATH"] = self.PUBLISH_PATH - - -def ls(): - pass - - -def sync_utility_scripts(env=None): - """ Synchronizing basic utlility scripts for resolve. - - To be able to run scripts from inside `Resolve/Workspace/Scripts` menu - all scripts has to be accessible from defined folder. - """ - if not env: - env = os.environ - - us_dir = env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "") - scripts = os.listdir(self.UTILITY_SCRIPTS) - - log.info(f"Utility Scripts Dir: `{self.UTILITY_SCRIPTS}`") - 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.join(us_dir, s) - log.info(f"Removing `{path}`...") - os.remove(path) - - # copy scripts into Resolve's utility scripts dir - for s in scripts: - src = os.path.join(self.UTILITY_SCRIPTS, s) - dst = os.path.join(us_dir, s) - log.info(f"Copying `{src}` to `{dst}`...") - shutil.copy2(src, dst) - - -def reload_pipeline(): - """Attempt to reload pipeline at run-time. - - CAUTION: This is primarily for development and debugging purposes. - - """ - - import importlib - import pype.resolve - - api.uninstall() - - for module in ("avalon.io", - "avalon.lib", - "avalon.pipeline", - "avalon.api", - "avalon.tools", - - "{}".format(AVALON_CONFIG), - "{}.resolve".format(AVALON_CONFIG), - "{}.resolve.lib".format(AVALON_CONFIG) - ): - log.info("Reloading module: {}...".format(module)) - try: - module = importlib.import_module(module) - importlib.reload(module) - except Exception as e: - log.warning("Cannot reload module: {}".format(e)) - - api.install(pype.resolve) - - -def setup(env=None): - """ Running wrapper - """ - if not env: - env = os.environ - - # synchronize resolve utility scripts - sync_utility_scripts(env) - - log.info("Resolve Pype wrapper has been installed") diff --git a/pype/resolve/menu.py b/pype/resolve/menu.py new file mode 100644 index 0000000000..9f45ec9b70 --- /dev/null +++ b/pype/resolve/menu.py @@ -0,0 +1,133 @@ +import os +import sys + +from Qt import QtWidgets, QtCore + + +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(self.__class__, self).__init__(*args, **kwargs) + + self.setFixedHeight(height) + + real_spacer = QtWidgets.QWidget(self) + real_spacer.setObjectName("Spacer") + real_spacer.setFixedHeight(int(height / 3)) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(real_spacer) + + self.setLayout(layout) + + +class PypeMenu(QtWidgets.QWidget): + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + + self.setObjectName("PypeMenu") + + self.setWindowFlags( + QtCore.Qt.Window + | QtCore.Qt.CustomizeWindowHint + | QtCore.Qt.WindowTitleHint + | QtCore.Qt.WindowCloseButtonHint + | QtCore.Qt.WindowStaysOnTopHint + ) + + self.setWindowTitle("Pype") + 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) + rename_btn = QtWidgets.QPushButton("Rename", self) + set_colorspace_btn = QtWidgets.QPushButton( + "Set colorspace from presets", self + ) + reset_resolution_btn = QtWidgets.QPushButton( + "Reset Resolution from peresets", self + ) + reload_pipeline_btn = QtWidgets.QPushButton("Reload pipeline", self) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(10, 10, 10, 10) + + layout.addWidget(workfiles_btn) + layout.addWidget(create_btn) + layout.addWidget(publish_btn) + layout.addWidget(load_btn) + layout.addWidget(inventory_btn) + + layout.addWidget(Spacer(20, self)) + + layout.addWidget(rename_btn) + layout.addWidget(set_colorspace_btn) + layout.addWidget(reset_resolution_btn) + + layout.addWidget(Spacer(20, self)) + + layout.addWidget(reload_pipeline_btn) + + self.setLayout(layout) + + workfiles_btn.clicked.connect(self.on_reload_pipeline_clicked) + 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) + rename_btn.clicked.connect(self.on_rename_clicked) + set_colorspace_btn.clicked.connect(self.on_set_colorspace_clicked) + reset_resolution_btn.clicked.connect(self.on_reset_resolution_clicked) + reload_pipeline_btn.clicked.connect(self.on_reload_pipeline_clicked) + + def on_workfile_clicked(self): + print("Clicked Workfile") + + def on_create_clicked(self): + print("Clicked Create") + + def on_publish_clicked(self): + print("Clicked Publish") + + def on_load_clicked(self): + print("Clicked Load") + + def on_inventory_clicked(self): + print("Clicked Inventory") + + def on_rename_clicked(self): + print("Clicked Rename") + + def on_set_colorspace_clicked(self): + print("Clicked Set Colorspace") + + def on_reset_resolution_clicked(self): + print("Clicked Reset Resolution") + + def on_reload_pipeline_clicked(self): + print("Clicked Reload Pipeline") + + +def launch_pype_menu(): + app = QtWidgets.QApplication(sys.argv) + + pype_menu = PypeMenu() + + stylesheet = load_stylesheet() + pype_menu.setStyleSheet(stylesheet) + + pype_menu.show() + + sys.exit(app.exec_()) diff --git a/pype/resolve/menu_style.qss b/pype/resolve/menu_style.qss new file mode 100644 index 0000000000..52672364db --- /dev/null +++ b/pype/resolve/menu_style.qss @@ -0,0 +1,34 @@ +QWidget { + background-color: #3a3939; + border-radius: 5; +} + +QPushButton { + border: 1px solid #6d6d6d; + background-color: #201f1f; + color: #6d6d6d; + padding: 5; +} + +QPushButton:focus { + background-color: "#272525"; +} + +QPushButton:pressed { + background-color: "#686464"; + color: #333333; +} + +QPushButton:hover { + color: #d0d0d0; + background-color: "#343232"; +} + +#PypeMenu { + border: 1px solid #333333; +} + +#Spacer { + padding: 10; + background-color: #464646; +} diff --git a/pype/resolve/pipeline.py b/pype/resolve/pipeline.py new file mode 100644 index 0000000000..b6cae307b0 --- /dev/null +++ b/pype/resolve/pipeline.py @@ -0,0 +1,184 @@ +""" +Basic avalon integration +""" +import os +import sys +from avalon.tools import workfiles +from avalon import api as avalon +from pyblish import api as pyblish +from pypeapp import Logger + +log = Logger().get_logger(__name__, "resolve") + +# self = sys.modules[__name__] + +AVALON_CONFIG = os.environ["AVALON_CONFIG"] +PARENT_DIR = os.path.dirname(__file__) +PACKAGE_DIR = os.path.dirname(PARENT_DIR) +PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") + +LOAD_PATH = os.path.join(PLUGINS_DIR, "resolve", "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "resolve", "create") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "resolve", "inventory") + +PUBLISH_PATH = os.path.join( + PLUGINS_DIR, "resolve", "publish" +).replace("\\", "/") + +AVALON_CONTAINERS = ":AVALON_CONTAINERS" +# IS_HEADLESS = not hasattr(cmds, "about") or cmds.about(batch=True) + + +def install(): + """Install resolve-specific functionality of avalon-core. + + This is where you install menus and register families, data + and loaders into resolve. + + It is called automatically when installing via `api.install(resolve)`. + + See the Maya equivalent for inspiration on how to implement this. + + """ + from .menu import launch_pype_menu + + # Disable all families except for the ones we explicitly want to see + family_states = [ + "imagesequence", + "mov" + ] + avalon.data["familiesStateDefault"] = False + avalon.data["familiesStateToggled"] = family_states + + log.info("pype.resolve installed") + + pyblish.register_host("resolve") + pyblish.register_plugin_path(PUBLISH_PATH) + log.info("Registering DaVinci Resovle 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) + + # opening menu + launch_pype_menu() + + +def uninstall(): + """Uninstall all tha was installed + + This is where you undo everything that was done in `install()`. + That means, removing menus, deregistering families and data + and everything. It should be as though `install()` was never run, + because odds are calling this function means the user is interested + in re-installing shortly afterwards. If, for example, he has been + modifying the menu or registered families. + + """ + pyblish.deregister_host("resolve") + pyblish.deregister_plugin_path(PUBLISH_PATH) + log.info("Deregistering DaVinci Resovle 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) + + +def containerise(obj, + name, + namespace, + context, + loader=None, + data=None): + """Bundle Resolve's object into an assembly and imprint it with metadata + + Containerisation enables a tracking of version, author and origin + for loaded assets. + + Arguments: + obj (obj): Resolve's object to imprint as container + name (str): Name of resulting assembly + namespace (str): Namespace under which to host container + context (dict): Asset information + loader (str, optional): Name of node used to produce this container. + + Returns: + obj (obj): containerised object + + """ + pass + + +def ls(): + """List available containers. + + This function is used by the Container Manager in Nuke. You'll + need to implement a for-loop that then *yields* one Container at + a time. + + See the `container.json` schema for details on how it should look, + and the Maya equivalent, which is in `avalon.maya.pipeline` + """ + pass + + +def parse_container(container): + """Return the container node's full container data. + + Args: + container (str): A container node name. + + Returns: + dict: The container schema data for this container node. + + """ + pass + + +def launch_workfiles_app(*args): + workdir = os.environ["AVALON_WORKDIR"] + workfiles.show(workdir) + + +def reload_pipeline(): + """Attempt to reload pipeline at run-time. + + CAUTION: This is primarily for development and debugging purposes. + + """ + + import importlib + import pype.resolve + + avalon.uninstall() + + # get avalon config name + config = os.getenv("AVALON_CONFIG", "pype") + + for module in ("avalon.io", + "avalon.lib", + "avalon.pipeline", + "avalon.api", + "avalon.tools", + + "{}".format(config), + "{}.resolve".format(config), + "{}.resolve.lib".format(config), + "{}.resolve.menu".format(config), + "{}.resolve.plugin".format(config), + "{}.resolve.pipeline".format(config) + ): + log.info("Reloading module: {}...".format(module)) + try: + module = importlib.import_module(module) + importlib.reload(module) + except Exception as e: + log.warning("Cannot reload module: {}".format(e)) + + avalon.install(pype.resolve) + + +def publish(parent): + """Shorthand to publish from within host""" + from avalon.tools import publish + return publish.show(parent) diff --git a/pype/resolve/plugin.py b/pype/resolve/plugin.py new file mode 100644 index 0000000000..a463495af3 --- /dev/null +++ b/pype/resolve/plugin.py @@ -0,0 +1,75 @@ +from avalon import api +from pype.resolve import lib as drlib +from avalon.vendor import qargparse + + +def get_reference_node_parents(ref): + """Return all parent reference nodes of reference node + + Args: + ref (str): reference node. + + Returns: + list: The upstream parent reference nodes. + + """ + parents = [] + return parents + + +class SequenceLoader(api.Loader): + """A basic SequenceLoader for Resolve + + This will implement the basic behavior for a loader to inherit from that + will containerize the reference and will implement the `remove` and + `update` logic. + + """ + + options = [ + qargparse.Toggle( + "handles", + label="Include handles", + default=0, + help="Load with handles or without?" + ), + qargparse.Choice( + "load_to", + label="Where to load clips", + items=[ + "Current timeline", + "New timeline" + ], + default=0, + help="Where do you want clips to be loaded?" + ), + qargparse.Choice( + "load_how", + label="How to load clips", + items=[ + "original timing", + "sequential in order" + ], + default=0, + help="Would you like to place it at orignal timing?" + ) + ] + + def load( + self, + context, + name=None, + namespace=None, + options=None + ): + pass + + def update(self, container, representation): + """Update an existing `container` + """ + pass + + def remove(self, container): + """Remove an existing `container` + """ + pass diff --git a/pype/resolve/preload_console.py b/pype/resolve/preload_console.py new file mode 100644 index 0000000000..7d602df339 --- /dev/null +++ b/pype/resolve/preload_console.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +import time +from pype.resolve.utils import get_resolve_module +from pypeapp import Logger + +log = Logger().get_logger(__name__, "resolve") + +wait_delay = 2.5 +wait = 0.00 +ready = None +while True: + try: + # Create project and set parameters: + resolve = get_resolve_module() + pm = resolve.GetProjectManager() + p = pm.GetCurrentProject() + if p.GetName() == "Untitled Project": + ready = None + else: + ready = True + except AttributeError: + pass + + if ready is None: + time.sleep(wait_delay) + log.info(f"Waiting {wait}s for Resolve to be open in project") + wait += wait_delay + else: + break diff --git a/pype/resolve/resolve_utility_scripts/Pype_menu.py b/pype/resolve/resolve_utility_scripts/Pype_menu.py new file mode 100644 index 0000000000..e9f5d68d70 --- /dev/null +++ b/pype/resolve/resolve_utility_scripts/Pype_menu.py @@ -0,0 +1,34 @@ +import os +import sys +import importlib +import avalon +import pype + +from pypeapp import Logger + +log = Logger().get_logger(__name__) + + +def main(env): + # Registers pype's Global pyblish plugins + pype.install() + + # Register Host (and it's pyblish plugins) + host_name = env["AVALON_APP"] + host_import_str = "pype.resolve" + + try: + host_module = importlib.import_module(host_import_str) + except ModuleNotFoundError: + log.error(( + f"Host \"{host_name}\" can't be imported." + f" Import string \"{host_import_str}\" failed." + )) + return False + + avalon.api.install(host_module) + + +if __name__ == "__main__": + result = main(os.environ) + sys.exit(not bool(result)) diff --git a/pype/resolve/resolve_utility_scripts/README.markdown b/pype/resolve/resolve_utility_scripts/README.markdown new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/pype/resolve/resolve_utility_scripts/README.markdown @@ -0,0 +1 @@ + diff --git a/pype/resolve/resolve_utility_scripts/__test_gui.py b/pype/resolve/resolve_utility_scripts/__test_gui.py new file mode 100644 index 0000000000..2b91732667 --- /dev/null +++ b/pype/resolve/resolve_utility_scripts/__test_gui.py @@ -0,0 +1,111 @@ +#! python3 +# -*- coding: utf-8 -*- + +# DaVinci Resolve scripting proof of concept. Resolve page external switcher. +# Local or TCP/IP control mode. +# Refer to Resolve V15 public beta 2 scripting API documentation for host setup. +# Copyright 2018 Igor Riđanović, www.hdhead.com +from Qt.QtGui import * +from Qt.QtWidgets import * +from Qt.QtCore import * + +import sys + +# If API module not found assume we"re working as a remote control +try: + import DaVinciResolveScript + # Instantiate Resolve object + resolve = DaVinciResolveScript.scriptapp("Resolve") + checkboxState = False +except ImportError: + print("Resolve API not found.") + checkboxState = True + +try: + _encoding = QApplication.UnicodeUTF8 + + def _translate(context, text, disambig): + return QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QApplication.translate(context, text, disambig) + + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(str("Resolve Page Switcher")) + Form.resize(561, 88) + Form.setStyleSheet(str(( + "background-color: #282828;" + "border-color: #555555;" + "color: #929292;" + "font-size: 13px;" + ))) + self.horizontalLayout = QHBoxLayout(Form) + self.horizontalLayout.setObjectName(str("horizontalLayout")) + self.mediaButton = QPushButton(Form) + self.mediaButton.setObjectName(str("mediaButton")) + self.horizontalLayout.addWidget(self.mediaButton) + self.editButton = QPushButton(Form) + self.editButton.setObjectName(str("editButton")) + self.horizontalLayout.addWidget(self.editButton) + self.fusionButton = QPushButton(Form) + self.fusionButton.setObjectName(str("fusionButton")) + self.horizontalLayout.addWidget(self.fusionButton) + self.colorButton = QPushButton(Form) + self.colorButton.setObjectName(str("colorButton")) + self.horizontalLayout.addWidget(self.colorButton) + self.fairlightButton = QPushButton(Form) + self.fairlightButton.setObjectName(str("fairlightButton")) + self.horizontalLayout.addWidget(self.fairlightButton) + self.deliverButton = QPushButton(Form) + self.deliverButton.setObjectName(str("deliverButton")) + self.horizontalLayout.addWidget(self.deliverButton) + + self.mediaButton.clicked.connect(lambda: self.pageswitch("media")) + self.editButton.clicked.connect(lambda: self.pageswitch("edit")) + self.fusionButton.clicked.connect(lambda: self.pageswitch("fusion")) + self.colorButton.clicked.connect(lambda: self.pageswitch("color")) + self.fairlightButton.clicked.connect( + lambda: self.pageswitch("fairlight")) + self.deliverButton.clicked.connect(lambda: self.pageswitch("deliver")) + + self.mediaButton.setStyleSheet(str("background-color: #181818;")) + self.editButton.setStyleSheet(str("background-color: #181818;")) + self.fusionButton.setStyleSheet( + str("background-color: #181818;")) + self.colorButton.setStyleSheet(str("background-color: #181818;")) + self.fairlightButton.setStyleSheet( + str("background-color: #181818;")) + self.deliverButton.setStyleSheet( + str("background-color: #181818;")) + + self.retranslateUi(Form) + QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(_translate("Resolve Page Switcher", + "Resolve Page Switcher", None)) + self.mediaButton.setText(_translate("Form", "Media", None)) + self.editButton.setText(_translate("Form", "Edit", None)) + self.fusionButton.setText(_translate("Form", "Fusion", None)) + self.colorButton.setText(_translate("Form", "Color", None)) + self.fairlightButton.setText(_translate("Form", "Fairlight", None)) + self.deliverButton.setText(_translate("Form", "Deliver", None)) + + def pageswitch(self, page): + # Send page name to server to switch remote Resolve"s page + try: + resolve.OpenPage(page) + print(f"Switched to {page}") + except NameError: + print("Resolve API not found. Run in remote mode instead?") + + +if __name__ == "__main__": + app = QApplication(sys.argv) + Form = QWidget() + ui = Ui_Form() + ui.setupUi(Form) + Form.show() + sys.exit(app.exec_()) diff --git a/pype/resolve/resolve_utility_scripts/__test_pyblish.py b/pype/resolve/resolve_utility_scripts/__test_pyblish.py new file mode 100644 index 0000000000..a6fe991025 --- /dev/null +++ b/pype/resolve/resolve_utility_scripts/__test_pyblish.py @@ -0,0 +1,57 @@ +import os +import sys +import pype +import importlib +import pyblish.api +import pyblish.util +import avalon.api +from avalon.tools import publish +from pypeapp import Logger + +log = Logger().get_logger(__name__) + + +def main(env): + # Registers pype's Global pyblish plugins + pype.install() + + # Register Host (and it's pyblish plugins) + host_name = env["AVALON_APP"] + # TODO not sure if use "pype." or "avalon." for host import + host_import_str = f"pype.{host_name}" + + try: + host_module = importlib.import_module(host_import_str) + except ModuleNotFoundError: + log.error(( + f"Host \"{host_name}\" can't be imported." + f" Import string \"{host_import_str}\" failed." + )) + return False + + avalon.api.install(host_module) + + # Register additional paths + addition_paths_str = env.get("PUBLISH_PATHS") or "" + addition_paths = addition_paths_str.split(os.pathsep) + for path in addition_paths: + path = os.path.normpath(path) + if not os.path.exists(path): + continue + + pyblish.api.register_plugin_path(path) + + # Register project specific plugins + project_name = os.environ["AVALON_PROJECT"] + project_plugins_paths = env.get("PYPE_PROJECT_PLUGINS") or "" + for path in project_plugins_paths.split(os.pathsep): + plugin_path = os.path.join(path, project_name, "plugins") + if os.path.exists(plugin_path): + pyblish.api.register_plugin_path(plugin_path) + + return publish.show() + + +if __name__ == "__main__": + result = main(os.environ) + sys.exit(not bool(result)) diff --git a/pype/resolve/resolve_utility_scripts/__test_subprocess.py b/pype/resolve/resolve_utility_scripts/__test_subprocess.py new file mode 100644 index 0000000000..438b1f716c --- /dev/null +++ b/pype/resolve/resolve_utility_scripts/__test_subprocess.py @@ -0,0 +1,36 @@ +#! python3 +# -*- coding: utf-8 -*- +import os +import sys +from pypeapp import execute, Logger +from pype.resolve.utils import get_resolve_module + +log = Logger().get_logger("Resolve") + +CURRENT_DIR = os.getenv("RESOLVE_UTILITY_SCRIPTS_DIR", "") +python_dir = os.getenv("PYTHON36_RESOLVE") +python_exe = os.path.normpath( + os.path.join(python_dir, "python.exe") +) + +resolve = get_resolve_module() +PM = resolve.GetProjectManager() +P = PM.GetCurrentProject() + +log.info(P.GetName()) + + +# ______________________________________________________ +# testing subprocessing Scripts +testing_py = os.path.join(CURRENT_DIR, "ResolvePageSwitcher.py") +testing_py = os.path.normpath(testing_py) +log.info(f"Testing path to script: `{testing_py}`") + +returncode = execute( + [python_exe, os.path.normpath(testing_py)], + env=dict(os.environ) +) + +# Check if output file exists +if returncode != 0: + log.error("Executing failed!") diff --git a/pype/resolve/resolve_utility_scripts/python_get_resolve.py b/pype/resolve/resolve_utility_scripts/python_get_resolve.py deleted file mode 100644 index 862a5bb758..0000000000 --- a/pype/resolve/resolve_utility_scripts/python_get_resolve.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python - -""" -This file serves to return a DaVinci Resolve object -""" - -import sys - -def GetResolve(): - try: - # The PYTHONPATH needs to be set correctly for this import statement to work. - # An alternative is to import the DaVinciResolveScript by specifying absolute path (see ExceptionHandler logic) - import DaVinciResolveScript as bmd - except ImportError: - if sys.platform.startswith("darwin"): - expectedPath="/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/Modules/" - elif sys.platform.startswith("win") or sys.platform.startswith("cygwin"): - import os - expectedPath=os.getenv('PROGRAMDATA') + "\\Blackmagic Design\\DaVinci Resolve\\Support\\Developer\\Scripting\\Modules\\" - elif sys.platform.startswith("linux"): - expectedPath="/opt/resolve/libs/Fusion/Modules/" - - # check if the default path has it... - print("Unable to find module DaVinciResolveScript from $PYTHONPATH - trying default locations") - try: - import imp - bmd = imp.load_source('DaVinciResolveScript', expectedPath+"DaVinciResolveScript.py") - except ImportError: - # No fallbacks ... report error: - print("Unable to find module DaVinciResolveScript - please ensure that the module DaVinciResolveScript is discoverable by python") - print("For a default DaVinci Resolve installation, the module is expected to be located in: "+expectedPath) - sys.exit() - - return bmd.scriptapp("Resolve") diff --git a/pype/resolve/resolve_utility_scripts/resolveapitest.py b/pype/resolve/resolve_utility_scripts/resolveapitest.py deleted file mode 100644 index e7cc32a864..0000000000 --- a/pype/resolve/resolve_utility_scripts/resolveapitest.py +++ /dev/null @@ -1,72 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# This script tests Resolve 15 scripting API on MacOS. -# We suspect an issue with import of fusionscript.so. -# To test launch Resolve Studio first and then run this script. -# The script will save a text report. -# igor@hdhead.com - -from datetime import datetime -import os -import sys -import imp - -eol = '\n' -pathLib = 'C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\fusionscript.dll' - -reportDir = "C:\\Users\\jezsc" - -# Create initial report file. It will overwrite existing! -reportName = 'Resolve_API_Report.txt' -reportPath = os.path.join(reportDir, reportName) -reportfile = open(reportPath, 'w') -reportfile.close() - - -def report(entry): - # Print to console - print entry - - # Write a report entry - reportfile = open(reportPath, 'a') - reportfile.write(entry) - reportfile.write(eol) - reportfile.close() - - -# These are the values we'll discover and save -report('Time: ' + str(datetime.now())) -report('Python Version: ' + sys.version) -report('Interpreter Path: ' + sys.executable) -report('___________________________________' + eol) - -report('If no lines follow we have likely experienced a Fatal Python Error.') - -try: - # Will the API library import? Does it exist? - smodule = imp.load_dynamic('fusionscript', pathLib) - report('Imported fusionscript.so') - - # It looks like the library imported. Can we create a resolve instance now? - try: - resolve = smodule.scriptapp('Resolve') - if 'None' in str(type(resolve)): - report('Resolve instance is created, but Resolve is not found.') - sys.exit() - if 'PyRemoteObject' in str(type(resolve)): - report('Resolve instance is created and Resolve is responsive.') - except Exception, e: - report(str(e)) - - # Let's go nuts and count how many projects are in the Project Manager - try: - projman = resolve.GetProjectManager() - projects = projman.GetProjectsInCurrentFolder() - report('Project Count: ' + str(len(projects))) - report('All is well!') - except Exception, e: - report(str(e)) - -except Exception, e: - report(str(e)) diff --git a/pype/resolve/resolve_utility_scripts/test.py b/pype/resolve/resolve_utility_scripts/test.py deleted file mode 100644 index 7d32aabfe5..0000000000 --- a/pype/resolve/resolve_utility_scripts/test.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python3.6 -from python_get_resolve import GetResolve - -resolve = GetResolve() -PM = resolve.GetProjectManager() -P = PM.GetCurrentProject() - -print(P.GetName()) diff --git a/pype/resolve/utility/pre_python_console_script.py b/pype/resolve/utility/pre_python_console_script.py deleted file mode 100644 index 1c1aceaddd..0000000000 --- a/pype/resolve/utility/pre_python_console_script.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python -import time -from python_get_resolve import GetResolve - -wait_delay = 2.5 -wait = 0.00 -ready = None -while True: - try: - # Create project and set parameters: - resolve = GetResolve() - PM = resolve.GetProjectManager() - P = PM.GetCurrentProject() - if P.GetName() == "Untitled Project": - ready = None - else: - ready = True - except AttributeError: - pass - - if ready is None: - time.sleep(wait_delay) - print(f"Waiting {wait}s for Resolve to be open inproject") - wait += wait_delay - else: - break diff --git a/pype/resolve/utility/python_get_resolve.py b/pype/resolve/utility/python_get_resolve.py deleted file mode 100644 index 862a5bb758..0000000000 --- a/pype/resolve/utility/python_get_resolve.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python - -""" -This file serves to return a DaVinci Resolve object -""" - -import sys - -def GetResolve(): - try: - # The PYTHONPATH needs to be set correctly for this import statement to work. - # An alternative is to import the DaVinciResolveScript by specifying absolute path (see ExceptionHandler logic) - import DaVinciResolveScript as bmd - except ImportError: - if sys.platform.startswith("darwin"): - expectedPath="/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/Modules/" - elif sys.platform.startswith("win") or sys.platform.startswith("cygwin"): - import os - expectedPath=os.getenv('PROGRAMDATA') + "\\Blackmagic Design\\DaVinci Resolve\\Support\\Developer\\Scripting\\Modules\\" - elif sys.platform.startswith("linux"): - expectedPath="/opt/resolve/libs/Fusion/Modules/" - - # check if the default path has it... - print("Unable to find module DaVinciResolveScript from $PYTHONPATH - trying default locations") - try: - import imp - bmd = imp.load_source('DaVinciResolveScript', expectedPath+"DaVinciResolveScript.py") - except ImportError: - # No fallbacks ... report error: - print("Unable to find module DaVinciResolveScript - please ensure that the module DaVinciResolveScript is discoverable by python") - print("For a default DaVinci Resolve installation, the module is expected to be located in: "+expectedPath) - sys.exit() - - return bmd.scriptapp("Resolve") diff --git a/pype/resolve/utils.py b/pype/resolve/utils.py new file mode 100644 index 0000000000..e421d02de5 --- /dev/null +++ b/pype/resolve/utils.py @@ -0,0 +1,114 @@ +#! python3 + +""" +Resolve's tools for setting environment +""" + +import sys +import os +import shutil + +from pypeapp import Logger + +log = Logger().get_logger(__name__, "resolve") + +UTILITY_SCRIPTS = os.path.join( + os.path.dirname(__file__), + "resolve_utility_scripts" +) + + +def get_resolve_module(): + try: + """ + The PYTHONPATH needs to be set correctly for this import + statement to work. An alternative is to import the + DaVinciResolveScript by specifying absolute path + (see ExceptionHandler logic) + """ + import DaVinciResolveScript as bmd + except ImportError: + if sys.platform.startswith("darwin"): + expected_path = ("/Library/Application Support/Blackmagic Design" + "/DaVinci Resolve/Developer/Scripting/Modules") + elif sys.platform.startswith("win") \ + or sys.platform.startswith("cygwin"): + expected_path = os.path.normpath( + os.getenv('PROGRAMDATA') + ( + "/Blackmagic Design/DaVinci Resolve/Support/Developer" + "/Scripting/Modules" + ) + ) + elif sys.platform.startswith("linux"): + expected_path = "/opt/resolve/libs/Fusion/Modules" + + # check if the default path has it... + print(("Unable to find module DaVinciResolveScript from " + "$PYTHONPATH - trying default locations")) + + module_path = os.path.normpath( + os.path.join( + expected_path, + "DaVinciResolveScript.py" + ) + ) + + try: + import imp + bmd = imp.load_source('DaVinciResolveScript', module_path) + except ImportError: + # No fallbacks ... report error: + log.error( + ("Unable to find module DaVinciResolveScript - please " + "ensure that the module DaVinciResolveScript is " + "discoverable by python") + ) + log.error( + ("For a default DaVinci Resolve installation, the " + f"module is expected to be located in: {expected_path}") + ) + sys.exit() + + return bmd.scriptapp("Resolve") + + +def _sync_utility_scripts(env=None): + """ Synchronizing basic utlility scripts for resolve. + + To be able to run scripts from inside `Resolve/Workspace/Scripts` menu + all scripts has to be accessible from defined folder. + """ + if not env: + env = os.environ + + us_dir = env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "") + scripts = os.listdir(UTILITY_SCRIPTS) + + log.info(f"Utility Scripts Dir: `{UTILITY_SCRIPTS}`") + 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.join(us_dir, s) + log.info(f"Removing `{path}`...") + os.remove(path) + + # copy scripts into Resolve's utility scripts dir + for s in scripts: + src = os.path.join(UTILITY_SCRIPTS, s) + dst = os.path.join(us_dir, s) + log.info(f"Copying `{src}` to `{dst}`...") + shutil.copy2(src, dst) + + +def setup(env=None): + """ Wrapper installer started from pype.hooks.resolve.ResolvePrelaunch() + """ + if not env: + env = os.environ + + # synchronize resolve utility scripts + _sync_utility_scripts(env) + + log.info("Resolve Pype wrapper has been installed") diff --git a/pype/resolve/workio.py b/pype/resolve/workio.py new file mode 100644 index 0000000000..49c027259b --- /dev/null +++ b/pype/resolve/workio.py @@ -0,0 +1,88 @@ +"""Host API required Work Files tool""" + +import os +import sys +from pypeapp import Logger +from .utils import get_resolve_module + +log = Logger().get_logger(__name__, "nukestudio") + +exported_projet_ext = ".drp" + +self = sys.modules[__name__] +self.pm = None + + +def get_project_manager(): + if not self.pm: + resolve = get_resolve_module() + self.pm = resolve.GetProjectManager() + return self.pm + + +def file_extensions(): + return [exported_projet_ext] + + +def has_unsaved_changes(): + get_project_manager().SaveProject() + return False + + +def save_file(filepath): + pm = get_project_manager() + file = os.path.basename(filepath) + fname, _ = os.path.splitext(file) + project = pm.GetCurrentProject() + name = project.GetName() + + if "Untitled Project" not in name: + log.info("Saving project: `{}` as '{}'".format(name, file)) + pm.ExportProject(name, filepath) + else: + log.info("Creating new project...") + pm.CreateProject(fname) + pm.ExportProject(name, filepath) + + +def open_file(filepath): + """ + Loading project + """ + pm = get_project_manager() + file = os.path.basename(filepath) + fname, _ = os.path.splitext(file) + + # deal with current project + project = pm.GetCurrentProject() + pm.SaveProject() + pm.CloseProject(project) + + try: + # load project from input path + project = pm.LoadProject(fname) + log.info(f"Project {project.GetName()} opened...") + return True + except NameError as E: + log.error(f"Project with name `{fname}` does not exist!\n\nError: {E}") + return False + + +def current_file(): + pm = get_project_manager() + current_dir = os.getenv("AVALON_WORKDIR") + project = pm.GetCurrentProject() + name = project.GetName() + fname = name + exported_projet_ext + current_file = os.path.join(current_dir, fname) + normalised = os.path.normpath(current_file) + + # Unsaved current file + if normalised == "": + return None + + return normalised + + +def work_root(session): + return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/")