Skip to content

Commit

Permalink
Merge pull request #3928 from BigRoy/fusion_event_system_thread
Browse files Browse the repository at this point in the history
  • Loading branch information
mkolar committed Oct 10, 2022
2 parents 7fa6597 + 3239953 commit 5a83c02
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 28 deletions.
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)


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:
self.no_response.emit()

self.msleep(interval)
Expand Down

0 comments on commit 5a83c02

Please sign in to comment.