Merge branch 'develop' into enhancement/exception-for-artist-error

This commit is contained in:
Jakub Trllo 2024-09-10 14:04:05 +02:00 committed by GitHub
commit 7a6484ea83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 406 additions and 188 deletions

View file

@ -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
self.data["task_entity"] = task_entity

View file

@ -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])

View file

@ -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
))

View file

@ -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.
@ -321,7 +320,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'

View file

@ -113,4 +113,4 @@ class CollectContextEntities(pyblish.api.ContextPlugin):
"Task '{}' was not found in project '{}'.".format(
task_path, project_name)
)
return task_entity
return task_entity

View file

@ -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'))

View file

@ -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,9 @@ class ExtractOTIOReview(publish.Extractor):
make_sequence_collection
)
# TODO refactore from using instance variable
self.temp_file_head = self._get_folder_name_based_prefix(instance)
# TODO: convert resulting image sequence to mp4
# get otio clip and other time info from instance clip
@ -491,3 +493,21 @@ class ExtractOTIOReview(publish.Extractor):
out_frame_start = self.used_frames[-1]
return output_path, out_frame_start
def _get_folder_name_based_prefix(self, instance):
"""Creates 'unique' human readable file prefix to differentiate.
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]
folder_path = folder_path.replace("/", "_").lstrip("_")
file_prefix = f"{folder_path}_{folder_name}."
self.log.debug(f"file_prefix::{file_prefix}")
return file_prefix

View file

@ -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(

View file

@ -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.

View file

@ -1,17 +1,59 @@
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):
"""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
hosts = ["maya", "houdini", "nuke"]
hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter"]
actions = [SaveByVersionUpAction, ShowWorkfilesAction]
def process(self, context):
current_file = context.data["currentFile"]
if not current_file:
raise PublishValidationError("File not saved")
raise PublishValidationError(
"Workfile is not saved. Please save your scene to continue.",
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.
The **Save Workfile** action will save it for you with the first
available workfile version number in your current context.
""")

View file

@ -1,10 +1,171 @@
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):
# On macOs it is required to set screen explicitly
if hasattr(self, "setScreen"):
self.setScreen(self._screen)
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 +178,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 +369,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

View file

@ -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)

View file

@ -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)

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON core addon version."""
__version__ = "0.4.4-dev.1"
__version__ = "0.4.5-dev.1"

View file

@ -1,6 +1,6 @@
name = "core"
title = "Core"
version = "0.4.4-dev.1"
version = "0.4.5-dev.1"
client_dir = "ayon_core"

View file

@ -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.
@ -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.

View file

@ -57,7 +57,7 @@ class CollectFramesFixDefModel(BaseSettingsModel):
True,
title="Show 'Rewrite latest version' toggle"
)
class ContributionLayersModel(BaseSettingsModel):
_layout = "compact"