Skip to content
This repository has been archived by the owner on Sep 20, 2024. It is now read-only.

Fusion: Implement callbacks to Fusion's event system thread #3928

Merged
merged 6 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions openpype/hosts/fusion/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import re
import contextlib

from Qt import QtGui

from openpype.lib import Logger
from openpype.client import (
get_asset_by_name,
Expand Down Expand Up @@ -92,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.
Expand Down Expand Up @@ -135,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():
Expand All @@ -160,6 +159,11 @@ def _on_repair():
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
Expand Down
5 changes: 5 additions & 0 deletions openpype/hosts/fusion/api/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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__]
Expand Down Expand Up @@ -119,6 +120,10 @@ def __init__(self, *args, **kwargs):
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"]
Expand Down
149 changes: 137 additions & 12 deletions openpype/hosts/fusion/api/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
Basic avalon integration
"""
import os
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,
Expand Down Expand Up @@ -39,12 +42,13 @@
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)
Comment on lines +45 to +51
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mostly an optimization.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning: I've just found that this logic does not work in Fusion 17.2 (released around June 2021) but does work in Fusion 17.4 since the fusion instance does not have a Print method in the older Fusion.

We might need to introduce a fallback to support older fusion versions.



def install():
Expand All @@ -67,7 +71,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)
Expand All @@ -84,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():
Expand Down Expand Up @@ -137,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, force_repair=True)


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():
Expand Down Expand Up @@ -254,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)
9 changes: 6 additions & 3 deletions openpype/hosts/fusion/api/pulse.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ def run(self):
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:
Comment on lines +22 to +27
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As comment explains this will mostly result in less delays (since calling Test() actually called against the main fusion UI).

self.no_response.emit()

self.msleep(interval)
Expand Down