From 6d6348f28a5f95817a426f42200dac2b825ff1b0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 28 Sep 2022 16:24:10 +0200 Subject: [PATCH 1/6] Fix logging handler to still print logs correctly when original "comp" is closed --- openpype/hosts/fusion/api/pipeline.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index c92d072ef7..4ddc8b0411 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -39,12 +39,13 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") -class CompLogHandler(logging.Handler): +class FusionLogHandler(logging.Handler): + # Keep a reference to fusion's Print function (Remote Object) + _print = getattr(sys.modules["__main__"], "fusion").Print + def emit(self, record): entry = self.format(record) - comp = get_current_comp() - if comp: - comp.Print(entry) + self._print(entry) def install(): @@ -67,7 +68,7 @@ def install(): # Attach default logging handler that prints to active comp logger = logging.getLogger() formatter = logging.Formatter(fmt="%(message)s\n") - handler = CompLogHandler() + handler = FusionLogHandler() handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(logging.DEBUG) From 6120d3b0fe09321ce21822d748527ff5ed785a55 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 28 Sep 2022 16:24:44 +0200 Subject: [PATCH 2/6] Remove unused import --- openpype/hosts/fusion/api/lib.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 4ef44dbb61..a55d25829e 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -3,8 +3,6 @@ import sys import re import contextlib -from Qt import QtGui - from openpype.lib import Logger from openpype.client import ( get_asset_by_name, From 0ebb6bd321f0c9bedb2095458a49c903bcab216a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 28 Sep 2022 16:25:04 +0200 Subject: [PATCH 3/6] Fix missing import --- openpype/hosts/fusion/api/pipeline.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 4ddc8b0411..3efaad91fc 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -2,6 +2,7 @@ Basic avalon integration """ import os +import sys import logging import pyblish.api From e9110d518d062ad34168b6abc59a9e9b9cf9e9b4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 28 Sep 2022 16:27:32 +0200 Subject: [PATCH 4/6] Add FusionEventHandler with background QThread --- openpype/hosts/fusion/api/menu.py | 5 + openpype/hosts/fusion/api/pipeline.py | 137 ++++++++++++++++++++++++-- 2 files changed, 135 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 7a6293807f..39126935e6 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -16,6 +16,7 @@ from openpype.hosts.fusion.api.lib import ( from openpype.pipeline import legacy_io from openpype.resources import get_openpype_icon_filepath +from .pipeline import FusionEventHandler from .pulse import FusionPulse self = sys.modules[__name__] @@ -119,6 +120,10 @@ class OpenPypeMenu(QtWidgets.QWidget): self._pulse = FusionPulse(parent=self) self._pulse.start() + # Detect Fusion events as OpenPype events + self._event_handler = FusionEventHandler(parent=self) + self._event_handler.start() + def on_task_changed(self): # Update current context label label = legacy_io.Session["AVALON_ASSET"] diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 3efaad91fc..2043fa290f 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -6,10 +6,12 @@ import sys import logging import pyblish.api +from Qt import QtCore from openpype.lib import ( Logger, - register_event_callback + register_event_callback, + emit_event ) from openpype.pipeline import ( register_loader_plugin_path, @@ -86,10 +88,10 @@ def install(): "instanceToggled", on_pyblish_instance_toggled ) - # Fusion integration currently does not attach to direct callbacks of - # the application. So we use workfile callbacks to allow similar behavior - # on save and open - register_event_callback("workfile.open.after", on_after_open) + # Register events + register_event_callback("open", on_after_open) + register_event_callback("save", on_save) + register_event_callback("new", on_new) def uninstall(): @@ -139,8 +141,18 @@ def on_pyblish_instance_toggled(instance, old_value, new_value): tool.SetAttrs({"TOOLB_PassThrough": passthrough}) -def on_after_open(_event): - comp = get_current_comp() +def on_new(event): + comp = event["Rets"]["comp"] + validate_comp_prefs(comp) + + +def on_save(event): + comp = event["sender"] + validate_comp_prefs(comp) + + +def on_after_open(event): + comp = event["sender"] validate_comp_prefs(comp) if any_outdated_containers(): @@ -256,3 +268,114 @@ def parse_container(tool): return container +class FusionEventThread(QtCore.QThread): + """QThread which will periodically ping Fusion app for any events. + + The fusion.UIManager must be set up to be notified of events before they'll + be reported by this thread, for example: + fusion.UIManager.AddNotify("Comp_Save", None) + + """ + + on_event = QtCore.Signal(dict) + + def run(self): + + app = getattr(sys.modules["__main__"], "app", None) + if app is None: + # No Fusion app found + return + + # As optimization store the GetEvent method directly because every + # getattr of UIManager.GetEvent tries to resolve the Remote Function + # through the PyRemoteObject + get_event = app.UIManager.GetEvent + delay = int(os.environ.get("OPENPYPE_FUSION_CALLBACK_INTERVAL", 1000)) + while True: + if self.isInterruptionRequested(): + return + + # Process all events that have been queued up until now + while True: + event = get_event(False) + if not event: + break + self.on_event.emit(event) + + # Wait some time before processing events again + # to not keep blocking the UI + self.msleep(delay) + + +class FusionEventHandler(QtCore.QObject): + """Emits OpenPype events based on Fusion events captured in a QThread. + + This will emit the following OpenPype events based on Fusion actions: + save: Comp_Save, Comp_SaveAs + open: Comp_Opened + new: Comp_New + + To use this you can attach it to you Qt UI so it runs in the background. + E.g. + >>> handler = FusionEventHandler(parent=window) + >>> handler.start() + + + """ + ACTION_IDS = [ + "Comp_Save", + "Comp_SaveAs", + "Comp_New", + "Comp_Opened" + ] + + def __init__(self, parent=None): + super(FusionEventHandler, self).__init__(parent=parent) + + # Set up Fusion event callbacks + fusion = getattr(sys.modules["__main__"], "fusion", None) + ui = fusion.UIManager + + # Add notifications for the ones we want to listen to + notifiers = [] + for action_id in self.ACTION_IDS: + notifier = ui.AddNotify(action_id, None) + notifiers.append(notifier) + + # TODO: Not entirely sure whether these must be kept to avoid + # garbage collection + self._notifiers = notifiers + + self._event_thread = FusionEventThread(parent=self) + self._event_thread.on_event.connect(self._on_event) + + def start(self): + self._event_thread.start() + + def stop(self): + self._event_thread.stop() + + def _on_event(self, event): + """Handle Fusion events to emit OpenPype events""" + if not event: + return + + what = event["what"] + + # Comp Save + if what in {"Comp_Save", "Comp_SaveAs"}: + if not event["Rets"].get("success"): + # If the Save action is cancelled it will still emit an + # event but with "success": False so we ignore those cases + return + # Comp was saved + emit_event("save", data=event) + return + + # Comp New + elif what in {"Comp_New"}: + emit_event("new", data=event) + + # Comp Opened + elif what in {"Comp_Opened"}: + emit_event("open", data=event) From fa256ad2a8ac153b24e81e156e6612c8538d4e65 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 28 Sep 2022 16:28:32 +0200 Subject: [PATCH 5/6] Force repair on new comp without asking the user --- openpype/hosts/fusion/api/lib.py | 28 ++++++++++++++++----------- openpype/hosts/fusion/api/pipeline.py | 2 +- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index a55d25829e..a33e5cf289 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -90,7 +90,7 @@ def set_asset_resolution(): }) -def validate_comp_prefs(comp=None): +def validate_comp_prefs(comp=None, force_repair=False): """Validate current comp defaults with asset settings. Validates fps, resolutionWidth, resolutionHeight, aspectRatio. @@ -133,21 +133,22 @@ def validate_comp_prefs(comp=None): asset_value = asset_data[key] comp_value = comp_frame_format_prefs.get(comp_key) if asset_value != comp_value: - # todo: Actually show dialog to user instead of just logging - log.warning( - "Comp {pref} {value} does not match asset " - "'{asset_name}' {pref} {asset_value}".format( - pref=label, - value=comp_value, - asset_name=asset_doc["name"], - asset_value=asset_value) - ) - invalid_msg = "{} {} should be {}".format(label, comp_value, asset_value) invalid.append(invalid_msg) + if not force_repair: + # Do not log warning if we force repair anyway + log.warning( + "Comp {pref} {value} does not match asset " + "'{asset_name}' {pref} {asset_value}".format( + pref=label, + value=comp_value, + asset_name=asset_doc["name"], + asset_value=asset_value) + ) + if invalid: def _on_repair(): @@ -158,6 +159,11 @@ def validate_comp_prefs(comp=None): attributes[comp_key_full] = value comp.SetPrefs(attributes) + if force_repair: + log.info("Applying default Comp preferences..") + _on_repair() + return + from . import menu from openpype.widgets import popup from openpype.style import load_stylesheet diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 2043fa290f..79928c0d96 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -143,7 +143,7 @@ def on_pyblish_instance_toggled(instance, old_value, new_value): def on_new(event): comp = event["Rets"]["comp"] - validate_comp_prefs(comp) + validate_comp_prefs(comp, force_repair=True) def on_save(event): From 323995369000e194575999a0b8460e412a3aee68 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 28 Sep 2022 16:45:21 +0200 Subject: [PATCH 6/6] Optimize Fusion pulse --- openpype/hosts/fusion/api/pulse.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/api/pulse.py b/openpype/hosts/fusion/api/pulse.py index 5b61f3bd63..eb7ef3785d 100644 --- a/openpype/hosts/fusion/api/pulse.py +++ b/openpype/hosts/fusion/api/pulse.py @@ -19,9 +19,12 @@ class PulseThread(QtCore.QThread): while True: if self.isInterruptionRequested(): return - try: - app.Test() - except Exception: + + # We don't need to call Test because PyRemoteObject of the app + # will actually fail to even resolve the Test function if it has + # gone down. So we can actually already just check by confirming + # the method is still getting resolved. (Optimization) + if app.Test is None: self.no_response.emit() self.msleep(interval)