From 49a0ccc0d5506f49335c886ca5f56dc9ca2de7ec Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 18 Jul 2024 23:28:17 +0200 Subject: [PATCH 01/21] Allow passing CollectSceneVersion but with a logged error so validators can catch a nicer error report instead --- client/ayon_core/plugins/publish/collect_scene_version.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_scene_version.py b/client/ayon_core/plugins/publish/collect_scene_version.py index ea4823d62a..8d643062bc 100644 --- a/client/ayon_core/plugins/publish/collect_scene_version.py +++ b/client/ayon_core/plugins/publish/collect_scene_version.py @@ -47,8 +47,9 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): return if not context.data.get('currentFile'): - raise KnownPublishError("Cannot get current workfile path. " - "Make sure your scene is saved.") + self.log.error("Cannot get current workfile path. " + "Make sure your scene is saved.") + return filename = os.path.basename(context.data.get('currentFile')) From 412cc595cce900d1697146e30fdc3fb3203881f3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 18 Jul 2024 23:31:29 +0200 Subject: [PATCH 02/21] Use regular `dict.get()` access on `instance.data.get("version")` for less confusion --- .../ayon_core/plugins/publish/collect_anatomy_instance_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py index b6636696c1..97bc47dc11 100644 --- a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py +++ b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py @@ -321,7 +321,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): use_context_version = instance.data["followWorkfileVersion"] if use_context_version: - version_number = context.data("version") + version_number = context.data.get("version") # Even if 'follow_workfile_version' is enabled, it may not be set # because workfile version was not collected to 'context.data' From 30c32b8fa7a0dce477206ade54ecfa06940b8da4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 19 Jul 2024 00:23:12 +0200 Subject: [PATCH 03/21] Improve Validate File Saved report and provide repair actions --- .../plugins/publish/validate_file_saved.py | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/validate_file_saved.py b/client/ayon_core/plugins/publish/validate_file_saved.py index d459ba7ed4..976f78aa2f 100644 --- a/client/ayon_core/plugins/publish/validate_file_saved.py +++ b/client/ayon_core/plugins/publish/validate_file_saved.py @@ -1,6 +1,30 @@ +import inspect + import pyblish.api from ayon_core.pipeline.publish import PublishValidationError +from ayon_core.tools.utils.host_tools import show_workfiles +from ayon_core.pipeline.context_tools import version_up_current_workfile + + +class SaveByVersionUpAction(pyblish.api.Action): + """Save Workfile.""" + label = "Save Workfile" + on = "failed" + icon = "save" + + def process(self, context, plugin): + version_up_current_workfile() + + +class ShowWorkfilesAction(pyblish.api.Action): + """Save Workfile.""" + label = "Show Workfiles Tool..." + on = "failed" + icon = "files-o" + + def process(self, context, plugin): + show_workfiles() class ValidateCurrentSaveFile(pyblish.api.ContextPlugin): @@ -8,10 +32,26 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin): label = "Validate File Saved" order = pyblish.api.ValidatorOrder - 0.1 - hosts = ["maya", "houdini", "nuke"] + hosts = ["maya", "houdini", "nuke", "fusion"] + actions = [SaveByVersionUpAction, ShowWorkfilesAction] def process(self, context): current_file = context.data["currentFile"] if not current_file: - raise PublishValidationError("File not saved") + raise PublishValidationError( + "File not saved", + title="File not saved", + description=self.get_description()) + + def get_description(self): + return inspect.cleandoc(""" + ### File not saved + + Your workfile must be saved to continue publishing. + + Please save your scene. + + The **Save Workfile** action will save it for you with the first + available workfile version number in your current context. + """) From 6b7ece95e69e753dcefe46061b558b0ab960a606 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 19 Jul 2024 00:25:19 +0200 Subject: [PATCH 04/21] Improve Validate File Saved report and provide repair actions --- client/ayon_core/plugins/publish/validate_file_saved.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/validate_file_saved.py b/client/ayon_core/plugins/publish/validate_file_saved.py index 976f78aa2f..bff835fd33 100644 --- a/client/ayon_core/plugins/publish/validate_file_saved.py +++ b/client/ayon_core/plugins/publish/validate_file_saved.py @@ -40,7 +40,7 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin): current_file = context.data["currentFile"] if not current_file: raise PublishValidationError( - "File not saved", + "Workfile is not saved. Please save your scene to continue.", title="File not saved", description=self.get_description()) @@ -50,8 +50,6 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin): Your workfile must be saved to continue publishing. - Please save your scene. - The **Save Workfile** action will save it for you with the first available workfile version number in your current context. """) From 86b1b4d208261bf987668b7d3655e46c832efaac Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 19 Jul 2024 00:31:12 +0200 Subject: [PATCH 05/21] Improve docstring --- client/ayon_core/plugins/publish/validate_file_saved.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/validate_file_saved.py b/client/ayon_core/plugins/publish/validate_file_saved.py index bff835fd33..e4f009615a 100644 --- a/client/ayon_core/plugins/publish/validate_file_saved.py +++ b/client/ayon_core/plugins/publish/validate_file_saved.py @@ -28,7 +28,11 @@ class ShowWorkfilesAction(pyblish.api.Action): class ValidateCurrentSaveFile(pyblish.api.ContextPlugin): - """File must be saved before publishing""" + """File must be saved before publishing + + This does not validate for unsaved changes. It only validates whether + the current context was able to identify any 'currentFile'. + """ label = "Validate File Saved" order = pyblish.api.ValidatorOrder - 0.1 From cc74f25e7a182363a82f319a97f4a24a61b80c02 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 14 Aug 2024 14:12:24 +0200 Subject: [PATCH 06/21] Provided unique file prefix Limits overwrites from multiple instances --- .../plugins/publish/extract_otio_review.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index be365520c7..8c7719ceb6 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -49,7 +49,6 @@ class ExtractOTIOReview(publish.Extractor): hosts = ["resolve", "hiero", "flame"] # plugin default attributes - temp_file_head = "tempFile." to_width = 1280 to_height = 720 output_ext = ".jpg" @@ -62,6 +61,8 @@ class ExtractOTIOReview(publish.Extractor): make_sequence_collection ) + self.temp_file_head = self._get_unique_file_prefix(instance) + # TODO: convert resulting image sequence to mp4 # get otio clip and other time info from instance clip @@ -491,3 +492,19 @@ class ExtractOTIOReview(publish.Extractor): out_frame_start = self.used_frames[-1] return output_path, out_frame_start + + def _get_unique_file_prefix(self, instance): + """Creates unique human readable file prefix to differentiate. + + Multiple instances might share same temp folder, this will provide + unique prefix for intermediate file for burnins. + """ + folder_path = instance.data["folderPath"] + folder_name = folder_path.split("/")[-1] + folder_path = folder_path.replace("/", "_").lstrip("_") + + file_prefix = f"{folder_path}_{folder_name}." + self.log.debug(f"file_prefix::{file_prefix}") + + return file_prefix + From b84904ec43379763bb6ee9528a762a645a635f87 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 26 Aug 2024 17:12:45 +0200 Subject: [PATCH 07/21] screen capture happens only on one screen --- .../publisher/widgets/screenshot_widget.py | 478 ++++++++++++------ 1 file changed, 316 insertions(+), 162 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/screenshot_widget.py b/client/ayon_core/tools/publisher/widgets/screenshot_widget.py index 08a0a790b7..9037883c04 100644 --- a/client/ayon_core/tools/publisher/widgets/screenshot_widget.py +++ b/client/ayon_core/tools/publisher/widgets/screenshot_widget.py @@ -1,10 +1,169 @@ import os import tempfile +import uuid from qtpy import QtCore, QtGui, QtWidgets -class ScreenMarquee(QtWidgets.QDialog): +class ScreenMarqueeDialog(QtWidgets.QDialog): + mouse_moved = QtCore.Signal() + mouse_pressed = QtCore.Signal(QtCore.QPoint, str) + mouse_released = QtCore.Signal(QtCore.QPoint) + close_requested = QtCore.Signal() + + def __init__(self, screen: QtCore.QObject, screen_id: str): + super().__init__() + self.setWindowFlags( + QtCore.Qt.Window + | QtCore.Qt.FramelessWindowHint + | QtCore.Qt.WindowStaysOnTopHint + | QtCore.Qt.CustomizeWindowHint + ) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.setCursor(QtCore.Qt.CrossCursor) + self.setMouseTracking(True) + + screen.geometryChanged.connect(self._fit_screen_geometry) + + self._screen = screen + self._opacity = 100 + self._click_pos = None + self._screen_id = screen_id + + def set_click_pos(self, pos): + self._click_pos = pos + self.repaint() + + def convert_end_pos(self, pos): + glob_pos = self.mapFromGlobal(pos) + new_pos = self._convert_pos(glob_pos) + return self.mapToGlobal(new_pos) + + def paintEvent(self, event): + """Paint event""" + # Convert click and current mouse positions to local space. + mouse_pos = self._convert_pos(self.mapFromGlobal(QtGui.QCursor.pos())) + + rect = event.rect() + fill_path = QtGui.QPainterPath() + fill_path.addRect(rect) + + capture_rect = None + if self._click_pos is not None: + click_pos = self.mapFromGlobal(self._click_pos) + capture_rect = QtCore.QRect(click_pos, mouse_pos) + + # Clear the capture area + sub_path = QtGui.QPainterPath() + sub_path.addRect(capture_rect) + fill_path = fill_path.subtracted(sub_path) + + painter = QtGui.QPainter(self) + painter.setRenderHints( + QtGui.QPainter.Antialiasing + | QtGui.QPainter.SmoothPixmapTransform + ) + + # Draw background. Aside from aesthetics, this makes the full + # tool region accept mouse events. + painter.setBrush(QtGui.QColor(0, 0, 0, self._opacity)) + painter.setPen(QtCore.Qt.NoPen) + painter.drawPath(fill_path) + + # Draw cropping markers at current mouse position + pen_color = QtGui.QColor(255, 255, 255, self._opacity) + pen = QtGui.QPen(pen_color, 1, QtCore.Qt.DotLine) + painter.setPen(pen) + painter.drawLine( + rect.left(), mouse_pos.y(), + rect.right(), mouse_pos.y() + ) + painter.drawLine( + mouse_pos.x(), rect.top(), + mouse_pos.x(), rect.bottom() + ) + + # Draw rectangle around selection area + if capture_rect is not None: + pen_color = QtGui.QColor(92, 173, 214) + pen = QtGui.QPen(pen_color, 2) + painter.setPen(pen) + painter.setBrush(QtCore.Qt.NoBrush) + l_x = capture_rect.left() + r_x = capture_rect.right() + if l_x > r_x: + l_x, r_x = r_x, l_x + t_y = capture_rect.top() + b_y = capture_rect.bottom() + if t_y > b_y: + t_y, b_y = b_y, t_y + + # -1 to draw 1px over the border + r_x -= 1 + b_y -= 1 + sel_rect = QtCore.QRect( + QtCore.QPoint(l_x, t_y), + QtCore.QPoint(r_x, b_y) + ) + painter.drawRect(sel_rect) + + painter.end() + + def mousePressEvent(self, event): + """Mouse click event""" + + if event.button() == QtCore.Qt.LeftButton: + # Begin click drag operation + self._click_pos = event.globalPos() + self.mouse_pressed.emit(self._click_pos, self._screen_id) + + def mouseReleaseEvent(self, event): + """Mouse release event""" + if event.button() == QtCore.Qt.LeftButton: + # End click drag operation and commit the current capture rect + self._click_pos = None + self.mouse_released.emit(event.globalPos()) + + def mouseMoveEvent(self, event): + """Mouse move event""" + self.mouse_moved.emit() + + def keyPressEvent(self, event): + """Mouse press event""" + if event.key() == QtCore.Qt.Key_Escape: + self._click_pos = None + event.accept() + self.close_requested.emit() + return + return super().keyPressEvent(event) + + def showEvent(self, event): + super().showEvent(event) + self._fit_screen_geometry() + + def closeEvent(self, event): + self._click_pos = None + super().closeEvent(event) + + def _convert_pos(self, pos): + geo = self.geometry() + if pos.x() > geo.width(): + pos.setX(geo.width() - 1) + elif pos.x() < 0: + pos.setX(0) + + if pos.y() > geo.height(): + pos.setY(geo.height() - 1) + elif pos.y() < 0: + pos.setY(0) + return pos + + def _fit_screen_geometry(self): + # Compute the union of all screen geometries, and resize to fit. + self.setGeometry(self._screen.geometry()) + + +class ScreenMarquee(QtCore.QObject): """Dialog to interactively define screen area. This allows to select a screen area through a marquee selection. @@ -17,187 +176,186 @@ class ScreenMarquee(QtWidgets.QDialog): def __init__(self, parent=None): super().__init__(parent=parent) - self.setWindowFlags( - QtCore.Qt.Window - | QtCore.Qt.FramelessWindowHint - | QtCore.Qt.WindowStaysOnTopHint - | QtCore.Qt.CustomizeWindowHint - ) - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - self.setCursor(QtCore.Qt.CrossCursor) - self.setMouseTracking(True) - - app = QtWidgets.QApplication.instance() - if hasattr(app, "screenAdded"): - app.screenAdded.connect(self._on_screen_added) - app.screenRemoved.connect(self._fit_screen_geometry) - elif hasattr(app, "desktop"): - desktop = app.desktop() - desktop.screenCountChanged.connect(self._fit_screen_geometry) - + screens_by_id = {} for screen in QtWidgets.QApplication.screens(): - screen.geometryChanged.connect(self._fit_screen_geometry) + screen_id = uuid.uuid4().hex + screen_dialog = ScreenMarqueeDialog(screen, screen_id) + screens_by_id[screen_id] = screen_dialog + screen_dialog.mouse_moved.connect(self._on_mouse_move) + screen_dialog.mouse_pressed.connect(self._on_mouse_press) + screen_dialog.mouse_released.connect(self._on_mouse_release) + screen_dialog.close_requested.connect(self._on_close_request) - self._opacity = 50 - self._click_pos = None - self._capture_rect = None + self._screens_by_id = screens_by_id + self._finished = False + self._captured = False + self._start_pos = None + self._end_pos = None + self._start_screen_id = None + self._pix = None def get_captured_pixmap(self): - if self._capture_rect is None: + if self._pix is None: return QtGui.QPixmap() + return self._pix - return self.get_desktop_pixmap(self._capture_rect) + def _close_dialogs(self): + for dialog in self._screens_by_id.values(): + dialog.close() - def paintEvent(self, event): - """Paint event""" + def _on_close_request(self): + self._close_dialogs() + self._finished = True - # Convert click and current mouse positions to local space. - mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos()) - click_pos = None - if self._click_pos is not None: - click_pos = self.mapFromGlobal(self._click_pos) - - painter = QtGui.QPainter(self) - painter.setRenderHints( - QtGui.QPainter.Antialiasing - | QtGui.QPainter.SmoothPixmapTransform - ) - - # Draw background. Aside from aesthetics, this makes the full - # tool region accept mouse events. - painter.setBrush(QtGui.QColor(0, 0, 0, self._opacity)) - painter.setPen(QtCore.Qt.NoPen) - rect = event.rect() - fill_path = QtGui.QPainterPath() - fill_path.addRect(rect) - - # Clear the capture area - if click_pos is not None: - sub_path = QtGui.QPainterPath() - capture_rect = QtCore.QRect(click_pos, mouse_pos) - sub_path.addRect(capture_rect) - fill_path = fill_path.subtracted(sub_path) - - painter.drawPath(fill_path) - - pen_color = QtGui.QColor(255, 255, 255, self._opacity) - pen = QtGui.QPen(pen_color, 1, QtCore.Qt.DotLine) - painter.setPen(pen) - - # Draw cropping markers at click position - if click_pos is not None: - painter.drawLine( - rect.left(), click_pos.y(), - rect.right(), click_pos.y() - ) - painter.drawLine( - click_pos.x(), rect.top(), - click_pos.x(), rect.bottom() - ) - - # Draw cropping markers at current mouse position - painter.drawLine( - rect.left(), mouse_pos.y(), - rect.right(), mouse_pos.y() - ) - painter.drawLine( - mouse_pos.x(), rect.top(), - mouse_pos.x(), rect.bottom() - ) - painter.end() - - def mousePressEvent(self, event): - """Mouse click event""" - - if event.button() == QtCore.Qt.LeftButton: - # Begin click drag operation - self._click_pos = event.globalPos() - - def mouseReleaseEvent(self, event): - """Mouse release event""" - if ( - self._click_pos is not None - and event.button() == QtCore.Qt.LeftButton - ): - # End click drag operation and commit the current capture rect - self._capture_rect = QtCore.QRect( - self._click_pos, event.globalPos() - ).normalized() - self._click_pos = None - self.close() - - def mouseMoveEvent(self, event): - """Mouse move event""" - self.repaint() - - def keyPressEvent(self, event): - """Mouse press event""" - if event.key() == QtCore.Qt.Key_Escape: - self._click_pos = None - self._capture_rect = None - event.accept() - self.close() + def _on_mouse_release(self, pos): + start_screen_dialog = self._screens_by_id.get(self._start_screen_id) + if start_screen_dialog is None: + self._finished = True + self._captured = False return - return super().keyPressEvent(event) - def showEvent(self, event): - self._fit_screen_geometry() + end_pos = start_screen_dialog.convert_end_pos(pos) - def _fit_screen_geometry(self): - # Compute the union of all screen geometries, and resize to fit. - workspace_rect = QtCore.QRect() - for screen in QtWidgets.QApplication.screens(): - workspace_rect = workspace_rect.united(screen.geometry()) - self.setGeometry(workspace_rect) + self._close_dialogs() + self._end_pos = end_pos + self._finished = True + self._captured = True - def _on_screen_added(self): - for screen in QtGui.QGuiApplication.screens(): - screen.geometryChanged.connect(self._fit_screen_geometry) + def _on_mouse_press(self, pos, screen_id): + self._start_pos = pos + self._start_screen_id = screen_id + + def _on_mouse_move(self): + for dialog in self._screens_by_id.values(): + dialog.repaint() + + def start_capture(self): + for dialog in self._screens_by_id.values(): + dialog.show() + # Activate so Escape event is not ignored. + dialog.setWindowState(QtCore.Qt.WindowActive) + + app = QtWidgets.QApplication.instance() + while not self._finished: + app.processEvents() + + # Give time to cloe dialogs + for _ in range(2): + app.processEvents() + + if self._captured: + self._pix = self.get_desktop_pixmap( + self._start_pos, self._end_pos + ) @classmethod - def get_desktop_pixmap(cls, rect): + def get_desktop_pixmap(cls, pos_start, pos_end): """Performs a screen capture on the specified rectangle. Args: - rect (QtCore.QRect): The rectangle to capture. + pos_start (QtCore.QPoint): Start of screen capture. + pos_end (QtCore.QPoint): End of screen capture. Returns: QtGui.QPixmap: Captured pixmap image - """ + """ + # Unify start and end points + # - start is top left + # - end is bottom right + if pos_start.y() > pos_end.y(): + pos_start, pos_end = pos_end, pos_start + + if pos_start.x() > pos_end.x(): + new_start = QtCore.QPoint(pos_end.x(), pos_start.y()) + new_end = QtCore.QPoint(pos_start.x(), pos_end.y()) + pos_start = new_start + pos_end = new_end + + # Validate if the rectangle is valid + rect = QtCore.QRect(pos_start, pos_end) if rect.width() < 1 or rect.height() < 1: return QtGui.QPixmap() - screen_pixes = [] - for screen in QtWidgets.QApplication.screens(): - screen_geo = screen.geometry() - if not screen_geo.intersects(rect): - continue + screen = QtWidgets.QApplication.screenAt(pos_start) + return screen.grabWindow( + 0, + pos_start.x() - screen.geometry().x(), + pos_start.y() - screen.geometry().y(), + pos_end.x() - pos_start.x(), + pos_end.y() - pos_start.y() + ) + # Multiscreen capture that does not work + # - does not handle pixel aspect ratio and positioning of screens - screen_pix_rect = screen_geo.intersected(rect) - screen_pix = screen.grabWindow( - 0, - screen_pix_rect.x() - screen_geo.x(), - screen_pix_rect.y() - screen_geo.y(), - screen_pix_rect.width(), screen_pix_rect.height() - ) - paste_point = QtCore.QPoint( - screen_pix_rect.x() - rect.x(), - screen_pix_rect.y() - rect.y() - ) - screen_pixes.append((screen_pix, paste_point)) - - output_pix = QtGui.QPixmap(rect.width(), rect.height()) - output_pix.fill(QtCore.Qt.transparent) - pix_painter = QtGui.QPainter() - pix_painter.begin(output_pix) - for item in screen_pixes: - (screen_pix, offset) = item - pix_painter.drawPixmap(offset, screen_pix) - - pix_painter.end() - - return output_pix + # most_left = None + # most_top = None + # for screen in QtWidgets.QApplication.screens(): + # screen_geo = screen.geometry() + # if most_left is None or most_left > screen_geo.x(): + # most_left = screen_geo.x() + # + # if most_top is None or most_top > screen_geo.y(): + # most_top = screen_geo.y() + # + # most_left = most_left or 0 + # most_top = most_top or 0 + # + # screen_pixes = [] + # for screen in QtWidgets.QApplication.screens(): + # screen_geo = screen.geometry() + # if not screen_geo.intersects(rect): + # continue + # + # pos_l_x = screen_geo.x() + # pos_l_y = screen_geo.y() + # pos_r_x = screen_geo.x() + screen_geo.width() + # pos_r_y = screen_geo.y() + screen_geo.height() + # if pos_start.x() > pos_l_x: + # pos_l_x = pos_start.x() + # + # if pos_start.y() > pos_l_y: + # pos_l_y = pos_start.y() + # + # if pos_end.x() < pos_r_x: + # pos_r_x = pos_end.x() + # + # if pos_end.y() < pos_r_y: + # pos_r_y = pos_end.y() + # + # capture_pos_x = pos_l_x - screen_geo.x() + # capture_pos_y = pos_l_y - screen_geo.y() + # capture_screen_width = pos_r_x - pos_l_x + # capture_screen_height = pos_r_y - pos_l_y + # screen_pix = screen.grabWindow( + # 0, + # capture_pos_x, capture_pos_y, + # capture_screen_width, capture_screen_height + # ) + # paste_point = QtCore.QPoint( + # (pos_l_x - screen_geo.x()) - rect.x(), + # (pos_l_y - screen_geo.y()) - rect.y() + # ) + # screen_pixes.append((screen_pix, paste_point)) + # + # output_pix = QtGui.QPixmap(rect.width(), rect.height()) + # output_pix.fill(QtCore.Qt.transparent) + # pix_painter = QtGui.QPainter() + # pix_painter.begin(output_pix) + # render_hints = ( + # QtGui.QPainter.Antialiasing + # | QtGui.QPainter.SmoothPixmapTransform + # ) + # if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): + # render_hints |= QtGui.QPainter.HighQualityAntialiasing + # pix_painter.setRenderHints(render_hints) + # for item in screen_pixes: + # (screen_pix, offset) = item + # pix_painter.drawPixmap(offset, screen_pix) + # + # pix_painter.end() + # + # return output_pix @classmethod def capture_to_pixmap(cls): @@ -209,12 +367,8 @@ class ScreenMarquee(QtWidgets.QDialog): Returns: QtGui.QPixmap: Captured pixmap image. """ - tool = cls() - # Activate so Escape event is not ignored. - tool.setWindowState(QtCore.Qt.WindowActive) - # Exec dialog and return captured pixmap. - tool.exec_() + tool.start_capture() return tool.get_captured_pixmap() @classmethod From 3245a74534a38bd795721810dbc2b20eda05134f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 28 Aug 2024 20:53:12 +0200 Subject: [PATCH 08/21] Add `max` and `substancepainter` + sort the host names --- client/ayon_core/plugins/publish/validate_file_saved.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/validate_file_saved.py b/client/ayon_core/plugins/publish/validate_file_saved.py index e4f009615a..8f956f586b 100644 --- a/client/ayon_core/plugins/publish/validate_file_saved.py +++ b/client/ayon_core/plugins/publish/validate_file_saved.py @@ -36,7 +36,7 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin): label = "Validate File Saved" order = pyblish.api.ValidatorOrder - 0.1 - hosts = ["maya", "houdini", "nuke", "fusion"] + hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter"] actions = [SaveByVersionUpAction, ShowWorkfilesAction] def process(self, context): From 41497e76e9c631af6a42026d8ac56914ed30ddc3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 2 Sep 2024 17:12:33 +0200 Subject: [PATCH 09/21] set screen of dialog before fitting it --- client/ayon_core/tools/publisher/widgets/screenshot_widget.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/screenshot_widget.py b/client/ayon_core/tools/publisher/widgets/screenshot_widget.py index 9037883c04..0706299f32 100644 --- a/client/ayon_core/tools/publisher/widgets/screenshot_widget.py +++ b/client/ayon_core/tools/publisher/widgets/screenshot_widget.py @@ -159,7 +159,9 @@ class ScreenMarqueeDialog(QtWidgets.QDialog): return pos def _fit_screen_geometry(self): - # Compute the union of all screen geometries, and resize to fit. + # On macOs it is required to set screen explicitly + if hasattr(self, "setScreen"): + self.setScreen(self._screen) self.setGeometry(self._screen.geometry()) From 8ea4dfd9f29fd2c477762226370fd866330cbd91 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:11:25 +0200 Subject: [PATCH 10/21] bump version to '0.4.4' --- client/ayon_core/version.py | 2 +- package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 55a14ba567..44517627b9 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON core addon version.""" -__version__ = "0.4.4-dev.1" +__version__ = "0.4.4" diff --git a/package.py b/package.py index ca4006425d..c704422aa1 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "0.4.4-dev.1" +version = "0.4.4" client_dir = "ayon_core" From f549a6e4377e046330781568b9223854590caade Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:11:51 +0200 Subject: [PATCH 11/21] bump version to '0.4.5-dev.1' --- client/ayon_core/version.py | 2 +- package.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 44517627b9..3ee3c976b9 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON core addon version.""" -__version__ = "0.4.4" +__version__ = "0.4.5-dev.1" diff --git a/package.py b/package.py index c704422aa1..26c004ae84 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "0.4.4" +version = "0.4.5-dev.1" client_dir = "ayon_core" From f6477b98f1b25907d399a0966f90a4a7ce28ab09 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:24:08 +0200 Subject: [PATCH 12/21] added pycodestyle warnings to ruff validation --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f8f840d2c9..cb2991fe44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,7 @@ target-version = "py39" [tool.ruff.lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. -select = ["E4", "E7", "E9", "F"] +select = ["E4", "E7", "E9", "F", "W"] ignore = [] # Allow fix for all enabled rules (when `--fix`) is provided. From a1b9d48c129321510ed29107a0dad4474879246b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:24:25 +0200 Subject: [PATCH 13/21] remove not existing path from ignore paths --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cb2991fe44..35d0df0964 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,6 @@ exclude = [ [tool.ruff.lint.per-file-ignores] "client/ayon_core/lib/__init__.py" = ["E402"] -"client/ayon_core/hosts/max/startup/startup.py" = ["E402"] [tool.ruff.format] # Like Black, use double quotes for strings. From 9fa4144feb1f7abfab068dd87619e2539406e04b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:28:51 +0200 Subject: [PATCH 14/21] remove trailing spaces --- client/ayon_core/lib/path_templates.py | 2 +- .../plugins/publish/extract_usd_layer_contributions.py | 2 +- client/ayon_core/tools/tray/lib.py | 2 +- server/settings/publish_plugins.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index ba0d26d5c1..dc88ec956b 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -503,7 +503,7 @@ class FormattingPart: # ensure key is properly formed [({})] properly closed. if not self.validate_key_is_matched(key): result.add_missing_key(key) - result.add_output(self.template) + result.add_output(self.template) return result # check if key expects subdictionary keys (e.g. project[name]) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index 68f2a8f00d..03ea66f418 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -83,7 +83,7 @@ def get_representation_path_in_publish_context( Allow resolving 'latest' paths from a publishing context's instances as if they will exist after publishing without them being integrated yet. - + Use first instance that has same folder path and product name, and contains representation with passed name. diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 5f92e8a04f..39fcc2cdd3 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -578,7 +578,7 @@ def make_sure_tray_is_running( args = get_ayon_launcher_args("tray", "--force") if env is None: env = os.environ.copy() - + # Make sure 'QT_API' is not set env.pop("QT_API", None) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 8e6b60f0d7..61972e64c4 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -57,7 +57,7 @@ class CollectFramesFixDefModel(BaseSettingsModel): True, title="Show 'Rewrite latest version' toggle" ) - + class ContributionLayersModel(BaseSettingsModel): _layout = "compact" From 809cb34171ff0e0c5c1f4c7c7d88706fa4be2366 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:29:03 +0200 Subject: [PATCH 15/21] add new line characters --- client/ayon_core/hooks/pre_global_host_data.py | 2 +- client/ayon_core/plugins/publish/collect_context_entities.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hooks/pre_global_host_data.py b/client/ayon_core/hooks/pre_global_host_data.py index e93b512742..12da6f12f8 100644 --- a/client/ayon_core/hooks/pre_global_host_data.py +++ b/client/ayon_core/hooks/pre_global_host_data.py @@ -94,4 +94,4 @@ class GlobalHostDataHook(PreLaunchHook): task_entity = get_task_by_name( project_name, folder_entity["id"], task_name ) - self.data["task_entity"] = task_entity \ No newline at end of file + self.data["task_entity"] = task_entity diff --git a/client/ayon_core/plugins/publish/collect_context_entities.py b/client/ayon_core/plugins/publish/collect_context_entities.py index f340178e4f..c8d25bc3e6 100644 --- a/client/ayon_core/plugins/publish/collect_context_entities.py +++ b/client/ayon_core/plugins/publish/collect_context_entities.py @@ -113,4 +113,4 @@ class CollectContextEntities(pyblish.api.ContextPlugin): "Task '{}' was not found in project '{}'.".format( task_path, project_name) ) - return task_entity \ No newline at end of file + return task_entity From 7a1950f98e01b28c0ae05360714c7101fff285e0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 3 Sep 2024 14:33:29 +0200 Subject: [PATCH 16/21] use correct escape sequence --- client/ayon_core/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index c2793f98a2..4390b00754 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -1900,7 +1900,7 @@ class OverscanCrop: string_value = re.sub(r"([ ]+)?px", " ", string_value) string_value = re.sub(r"([ ]+)%", "%", string_value) # Make sure +/- sign at the beginning of string is next to number - string_value = re.sub(r"^([\+\-])[ ]+", "\g<1>", string_value) + string_value = re.sub(r"^([\+\-])[ ]+", r"\g<1>", string_value) # Make sure +/- sign in the middle has zero spaces before number under # which belongs string_value = re.sub( From 614f2d4f6334186ee7d9673e61d9f4617534743f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 3 Sep 2024 22:18:16 +0200 Subject: [PATCH 17/21] Avoid double quotes around paths --- .../plugins/publish/collect_anatomy_instance_data.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py index 5b750a5232..925e14166e 100644 --- a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py +++ b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py @@ -217,9 +217,8 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): joined_paths = ", ".join( ["\"{}\"".format(path) for path in not_found_task_paths] ) - self.log.warning(( - "Not found task entities with paths \"{}\"." - ).format(joined_paths)) + self.log.warning( + f"Not found task entities with paths {joined_paths}.") def fill_latest_versions(self, context, project_name): """Try to find latest version for each instance's product name. From 73e125639bbeab03649022322d1165b54084937a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:29:56 +0200 Subject: [PATCH 18/21] remove trailing spaces --- client/ayon_core/plugins/publish/validate_file_saved.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/validate_file_saved.py b/client/ayon_core/plugins/publish/validate_file_saved.py index 8f956f586b..d132ba8d3a 100644 --- a/client/ayon_core/plugins/publish/validate_file_saved.py +++ b/client/ayon_core/plugins/publish/validate_file_saved.py @@ -51,9 +51,9 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin): def get_description(self): return inspect.cleandoc(""" ### File not saved - + Your workfile must be saved to continue publishing. - - The **Save Workfile** action will save it for you with the first + + The **Save Workfile** action will save it for you with the first available workfile version number in your current context. """) From 916c9a625783522366778ce170bf882ec832a137 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Sep 2024 12:48:32 +0200 Subject: [PATCH 19/21] Added todo note to get rid of instance variable --- .../plugins/publish/extract_otio_review.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 8c7719ceb6..c1cf50333c 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -61,7 +61,8 @@ class ExtractOTIOReview(publish.Extractor): make_sequence_collection ) - self.temp_file_head = self._get_unique_file_prefix(instance) + # TODO refactore from using instance variable + self.temp_file_head = self._get_folder_name_based_prefix(instance) # TODO: convert resulting image sequence to mp4 @@ -493,11 +494,13 @@ class ExtractOTIOReview(publish.Extractor): return output_path, out_frame_start - def _get_unique_file_prefix(self, instance): - """Creates unique human readable file prefix to differentiate. + def _get_folder_name_based_prefix(self, instance): + """Creates 'unique' human readable file prefix to differentiate. - Multiple instances might share same temp folder, this will provide - unique prefix for intermediate file for burnins. + Multiple instances might share same temp folder, but each instance + would be differentiated by asset, eg. folder name. + + It ix expected that there won't be multiple instances for same asset. """ folder_path = instance.data["folderPath"] folder_name = folder_path.split("/")[-1] From b636658f43d89cb093948363486f1ea5d70f75ea Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 5 Sep 2024 20:57:20 +0200 Subject: [PATCH 20/21] Allow placeholder representation name to be set as an empty value to load all representations --- .../pipeline/workfile/workfile_template_builder.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 7b15dff049..be5b7437de 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -1519,9 +1519,10 @@ class PlaceholderLoadMixin(object): if "asset" in placeholder.data: return [] - representation_name = placeholder.data["representation"] - if not representation_name: - return [] + representation_names = None + representation_name: str = placeholder.data["representation"] + if representation_name: + representation_names = [representation_name] project_name = self.builder.project_name current_folder_entity = self.builder.current_folder_entity @@ -1578,7 +1579,7 @@ class PlaceholderLoadMixin(object): ) return list(get_representations( project_name, - representation_names={representation_name}, + representation_names=representation_names, version_ids=version_ids )) From 2e039af0151c9ee8920c51a6d3dba1ff8b383122 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Sep 2024 10:30:51 +0200 Subject: [PATCH 21/21] Fix if statement --- client/ayon_core/vendor/python/scriptsmenu/launchformaya.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py b/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py index c8b0c777de..496278ac6f 100644 --- a/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py +++ b/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py @@ -130,7 +130,7 @@ def main(title="Scripts", parent=None, objectName=None): # Register control + shift callback to add to shelf (maya behavior) modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier - if int(cmds.about(version=True)) <= 2025: + if int(cmds.about(version=True)) < 2025: modifiers = int(modifiers) menu.register_callback(modifiers, to_shelf)