Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/ynput/ayon-silhouette in…
Browse files Browse the repository at this point in the history
…to bugfix/extractor_support_layered_objects

# Conflicts:
#	client/ayon_silhouette/api/lib.py
  • Loading branch information
BigRoy committed Jan 28, 2025
2 parents 6a3d591 + 7f4b80a commit eaaa136
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 7 deletions.
50 changes: 48 additions & 2 deletions client/ayon_silhouette/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import logging
from typing import Optional

from qtpy import QtCore, QtWidgets
import fx
import tools.window

from ayon_core.lib import NumberDef
from ayon_core.pipeline.context_tools import get_current_task_entity

import fx
import tools.window

AYON_CONTAINERS = "AYON_CONTAINERS"
JSON_PREFIX = "JSON::"
Expand Down Expand Up @@ -251,6 +253,50 @@ def reset_session_settings(session=None, task_entity=None):
set_frame_range_from_entity(session, task_entity)


@contextlib.contextmanager
def capture_messageboxes(callback):
"""Capture messageboxes and call a callback with them.
This is a workaround for Silhouette not allowing the Python code to
suppress messageboxes and supply default answers to them. So instead we
capture the messageboxes and respond to them through a rapid QTimer.
"""
processed = set()
timer = QtCore.QTimer()

def on_timeout():
# Check for dialogs
widgets = QtWidgets.QApplication.instance().topLevelWidgets()
has_boxes = False
for widget in widgets:
if isinstance(widget, QtWidgets.QMessageBox):
has_boxes = True
if widget in processed:
continue
processed.add(widget)
callback(widget)

# Stop as soon as possible with our detections. Even with the
# QTimer repeating at interval of 0 we should have been able to
# capture all the UI events as they happen in the main thread for
# each dialog.
# Note: Technically this would mean that as soon as there is no
# active messagebox we directly stop the timer, and hence would stop
# finding messageboxes after. However, with the export methods in
# Silhouette this has not been a problem and all boxes were detected
# accordingly.
if not has_boxes:
timer.stop()

timer.setSingleShot(False) # Allow to capture multiple boxes
timer.timeout.connect(on_timeout)
timer.start()
try:
yield
finally:
timer.stop()


def iter_children(node, prefix=None):
"""Iterate over all children of a node recursively."""
children = node.children
Expand Down
36 changes: 34 additions & 2 deletions client/ayon_silhouette/plugins/publish/extract_shapes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os
import contextlib

from qtpy import QtWidgets
import fx

from ayon_core.pipeline import publish
Expand All @@ -16,6 +18,8 @@ class ExtractNukeShapes(publish.Extractor):
extension = "nk"
io_module = "Nuke 9+ Shapes"

capture_messageboxes = False

def process(self, instance):

# Define extract output file path
Expand All @@ -37,8 +41,12 @@ def process(self, instance):

with lib.maintained_selection():
fx.select(shapes)
self.log.debug(f"Exporting '{self.io_module}' to: {path}")
fx.io_modules[self.io_module].export(path)
with contextlib.ExitStack() as stack:
self.log.debug(f"Exporting '{self.io_module}' to: {path}")
if self.capture_messageboxes:
stack.enter_context(
lib.capture_messageboxes(self.on_captured_messagebox))
fx.io_modules[self.io_module].export(path)

representation = {
"name": self.extension,
Expand All @@ -50,6 +58,9 @@ def process(self, instance):

self.log.debug(f"Extracted instance '{instance.name}' to: {path}")

def on_captured_messagebox(self, messagebox: QtWidgets.QMessageBox):
pass


class ExtractFusionShapes(ExtractNukeShapes):
"""Extract node as Fusion Shapes."""
Expand All @@ -59,6 +70,27 @@ class ExtractFusionShapes(ExtractNukeShapes):
extension = "setting"
io_module = "Fusion Shapes"

capture_messageboxes = True

def on_captured_messagebox(self, messagebox):
# Suppress pop-up dialogs
self.log.debug(f"Detected messagebox: {messagebox.text()}")

def click(messagebox: QtWidgets.QMessageBox, text: str):
"""Click QMessageBox button with matching text."""
self.log.debug(f"Accepting messagebox with '{text}'")
button = next(
button for button in messagebox.buttons()
if button.text() == text
)
button.click()

messagebox_text = messagebox.text()
if messagebox_text == "Output Fusion Groups?":
click(messagebox, "&Yes")
elif messagebox_text == "Link Shapes?":
click(messagebox, "&Yes")


class ExtractSilhouetteShapes(ExtractNukeShapes):
"""Extract node as Silhouette Shapes."""
Expand Down
46 changes: 43 additions & 3 deletions client/ayon_silhouette/plugins/publish/extract_track.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import contextlib
import os

from qtpy import QtWidgets

import fx

from ayon_core.pipeline import publish
Expand All @@ -15,6 +18,8 @@ class SilhouetteExtractAfterEffectsTrack(publish.Extractor):
extension = "txt"
io_module = "After Effects"

capture_messageboxes = True

def process(self, instance):

# Define extract output file path
Expand All @@ -25,7 +30,6 @@ def process(self, instance):
# Node should be a node that contains 'tracker' children
node = instance.data["transientData"]["instance_node"]


# Use selection, if any specified, otherwise use all children shapes
tracker_ids = instance.data.get(
"creator_attributes", {}).get("trackers")
Expand All @@ -41,8 +45,12 @@ def process(self, instance):

with lib.maintained_selection():
fx.select(trackers)
self.log.debug(f"Exporting '{self.io_module}' to: {path}")
fx.io_modules[self.io_module].export(path)
with contextlib.ExitStack() as stack:
self.log.debug(f"Exporting '{self.io_module}' to: {path}")
if self.capture_messageboxes:
stack.enter_context(
lib.capture_messageboxes(self.on_captured_messagebox))
fx.io_modules[self.io_module].export(path)

representation = {
"name": self.extension,
Expand All @@ -54,6 +62,25 @@ def process(self, instance):

self.log.debug(f"Extracted instance '{instance.name}' to: {path}")

def on_captured_messagebox(self, messagebox: QtWidgets.QMessageBox):
self.log.debug(f"Detected messagebox: {messagebox.text()}")
button_texts = [button.text() for button in messagebox.buttons()]
self.log.debug(f"Buttons: {button_texts}")
# Continue if messagebox is just confirmation dialog about After
# Effects being unable to keyframe Match Size, Search Offset and Search
# Size.
if "After Effects cannot keyframe" in messagebox.text():
self.click(messagebox, "&Yes")

def click(self, messagebox: QtWidgets.QMessageBox, text: str):
"""Click QMessageBox button with matching text."""
self.log.debug(f"Accepting messagebox with '{text}'")
button = next(
button for button in messagebox.buttons()
if button.text() == text
)
button.click()


class SilhouetteExtractNuke5Track(SilhouetteExtractAfterEffectsTrack):
"""Extract Nuke 5 .nk trackers from Silhouette."""
Expand All @@ -63,3 +90,16 @@ class SilhouetteExtractNuke5Track(SilhouetteExtractAfterEffectsTrack):

extension = "nk"
io_module = "Nuke 5"

# Whether or not to merge up to four trackers in a single Nuke Tracker node
# or otherwise export as multiple single point tracker nodes
merge_up_to_four = True

def on_captured_messagebox(self, messagebox: QtWidgets.QMessageBox):
self.log.debug(f"Detected messagebox: {messagebox.text()}")
button_texts = [button.text() for button in messagebox.buttons()]
self.log.debug(f"Buttons: {button_texts}")
# Merge up to four tracker
if "Select Yes to merge." in messagebox.text():
button = "&Yes" if self.merge_up_to_four else "&No"
self.click(messagebox, button)

0 comments on commit eaaa136

Please sign in to comment.