From b6a2be53d4f1e7d2caf827ad4d6b45e366e9b1b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Oct 2022 17:40:35 +0200 Subject: [PATCH 01/55] removed unused imports --- openpype/tools/publisher/widgets/create_widget.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index 910b2adfc72..c7d001e92ea 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -1,11 +1,8 @@ -import sys import re -import traceback from Qt import QtWidgets, QtCore, QtGui from openpype.pipeline.create import ( - CreatorError, SUBSET_NAME_ALLOWED_SYMBOLS, TaskNotSetError, ) From 2a91415b42400d24f77dc2bff008d5e29e387114 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Oct 2022 19:01:34 +0200 Subject: [PATCH 02/55] Create widget has thumbnail --- .../tools/publisher/widgets/create_widget.py | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index c7d001e92ea..a8ca9af17d1 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -10,6 +10,7 @@ from .widgets import ( IconValuePixmapLabel, CreateBtn, + ThumbnailWidget, ) from .assets_widget import CreateWidgetAssetsWidget from .tasks_widget import CreateWidgetTasksWidget @@ -23,11 +24,11 @@ SEPARATORS = ("---separator---", "---") -class VariantInputsWidget(QtWidgets.QWidget): +class ResizeControlWidget(QtWidgets.QWidget): resized = QtCore.Signal() def resizeEvent(self, event): - super(VariantInputsWidget, self).resizeEvent(event) + super(ResizeControlWidget, self).resizeEvent(event) self.resized.emit() @@ -150,13 +151,19 @@ def __init__(self, controller, parent=None): # --- Creator attr defs --- creators_attrs_widget = QtWidgets.QWidget(creators_splitter) + # Top part - variant / subset name + thumbnail + creators_attrs_top = QtWidgets.QWidget(creators_attrs_widget) + + # Basics - variant / subset name + creator_basics_widget = ResizeControlWidget(creators_attrs_top) + variant_subset_label = QtWidgets.QLabel( - "Create options", creators_attrs_widget + "Create options", creator_basics_widget ) - variant_subset_widget = QtWidgets.QWidget(creators_attrs_widget) + variant_subset_widget = QtWidgets.QWidget(creator_basics_widget) # Variant and subset input - variant_widget = VariantInputsWidget(creators_attrs_widget) + variant_widget = ResizeControlWidget(variant_subset_widget) variant_input = QtWidgets.QLineEdit(variant_widget) variant_input.setObjectName("VariantInput") @@ -183,6 +190,18 @@ def __init__(self, controller, parent=None): variant_subset_layout.addRow("Variant", variant_widget) variant_subset_layout.addRow("Subset", subset_name_input) + creator_basics_layout = QtWidgets.QVBoxLayout(creator_basics_widget) + creator_basics_layout.setContentsMargins(0, 0, 0, 0) + creator_basics_layout.addWidget(variant_subset_label, 0) + creator_basics_layout.addWidget(variant_subset_widget, 0) + + thumbnail_widget = ThumbnailWidget(creators_attrs_top) + + creators_attrs_top_layout = QtWidgets.QHBoxLayout(creators_attrs_top) + creators_attrs_top_layout.setContentsMargins(0, 0, 0, 0) + creators_attrs_top_layout.addWidget(creator_basics_widget, 1) + creators_attrs_top_layout.addWidget(thumbnail_widget, 0) + # Precreate attributes widget pre_create_widget = PreCreateWidget(creators_attrs_widget) @@ -198,8 +217,7 @@ def __init__(self, controller, parent=None): creators_attrs_layout = QtWidgets.QVBoxLayout(creators_attrs_widget) creators_attrs_layout.setContentsMargins(0, 0, 0, 0) - creators_attrs_layout.addWidget(variant_subset_label, 0) - creators_attrs_layout.addWidget(variant_subset_widget, 0) + creators_attrs_layout.addWidget(creators_attrs_top, 0) creators_attrs_layout.addWidget(pre_create_widget, 1) creators_attrs_layout.addWidget(create_btn_wrapper, 0) @@ -237,6 +255,7 @@ def __init__(self, controller, parent=None): create_btn.clicked.connect(self._on_create) variant_widget.resized.connect(self._on_variant_widget_resize) + creator_basics_widget.resized.connect(self._on_creator_basics_resize) variant_input.returnPressed.connect(self._on_create) variant_input.textChanged.connect(self._on_variant_change) creators_view.selectionModel().currentChanged.connect( @@ -275,6 +294,8 @@ def __init__(self, controller, parent=None): self._create_btn = create_btn self._creator_short_desc_widget = creator_short_desc_widget + self._creator_basics_widget = creator_basics_widget + self._thumbnail_widget = thumbnail_widget self._pre_create_widget = pre_create_widget self._attr_separator_widget = attr_separator_widget @@ -681,6 +702,11 @@ def showEvent(self, event): self._first_show = False self._on_first_show() + def _on_creator_basics_resize(self): + self._thumbnail_widget.set_height( + self._creator_basics_widget.sizeHint().height() + ) + def _on_create(self): indexes = self._creators_view.selectedIndexes() if not indexes or len(indexes) > 1: From 2afc5315778a300e118d8acbb9f63581c934147d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Oct 2022 19:03:42 +0200 Subject: [PATCH 03/55] modified thumbnail to paint the content on own --- openpype/tools/publisher/widgets/widgets.py | 171 +++++++++++++++++--- 1 file changed, 150 insertions(+), 21 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index d4c26237900..23ddeee2def 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -16,6 +16,7 @@ PixmapLabel, BaseClickableFrame, set_style_property, + paint_image_with_color, ) from openpype.style import get_objected_colors from openpype.pipeline.create import ( @@ -26,6 +27,7 @@ from .tasks_widget import TasksModel from .icons import ( get_pixmap, + get_image, get_icon_path ) @@ -1622,33 +1624,160 @@ def set_current_instances( class ThumbnailWidget(QtWidgets.QWidget): - """Instance thumbnail widget. + """Instance thumbnail widget.""" - Logic implementation of this widget is missing but widget is used - to offset `GlobalAttrsWidget` inputs visually. - """ - def __init__(self, parent): - super(ThumbnailWidget, self).__init__(parent) + width_ratio = 3.0 + height_ratio = 2.0 + border_width = 1 + offset_sep = 4 + def __init__(self, parent): # Missing implementation for thumbnail # - widget kept to make a visial offset of global attr widget offset - # default_pix = get_pixmap("thumbnail") - default_pix = QtGui.QPixmap(10, 10) - default_pix.fill(QtCore.Qt.transparent) - - thumbnail_label = QtWidgets.QLabel(self) - thumbnail_label.setPixmap( - default_pix.scaled( - 200, 100, + super(ThumbnailWidget, self).__init__(parent) + + # TODO remove hardcoded colors + border_color = QtGui.QColor(67, 74, 86) + thumbnail_bg_color = QtGui.QColor(54, 61, 72) + + default_image = get_image("thumbnail") + default_pix = paint_image_with_color(default_image, border_color) + + self.border_color = border_color + self.thumbnail_bg_color = thumbnail_bg_color + self._default_pix = default_pix + self._current_pixes = None + self._cached_pix = None + self._height = None + self._width = None + + def set_width(self, width): + if self._width == width: + return + + self._width = width + self._cached_pix = None + self.setMinimumHeight(int( + (width / self.width_ratio) * self.height_ratio + )) + if self._height is not None: + self.setMinimumWidth(0) + + def set_height(self, height): + if self._height == height: + return + + self._height = height + self._cached_pix = None + self.setMinimumWidth(int( + (height / self.height_ratio) * self.width_ratio + )) + if self._width is not None: + self.setMinimumHeight(0) + + def _get_current_pixes(self): + if self._current_pixes is None: + return [self._default_pix] + return self._current_pixes + + def _cache_pix(self): + rect = self.rect() + rect_width = rect.width() + rect_height = rect.height() + + pix_x_offset = 0 + pix_y_offset = 0 + expected_height = int( + (rect_width / self.width_ratio) * self.height_ratio + ) + if expected_height > rect_height: + expected_height = rect_height + expected_width = int( + (rect_height / self.height_ratio) * self.width_ratio + ) + pix_x_offset = (rect_width - expected_width) / 2 + else: + expected_width = rect_width + pix_y_offset = (rect_height - expected_height) / 2 + + pixes_to_draw = self._get_current_pixes() + max_pix = 3 + if len(pixes_to_draw) > max_pix: + pixes_to_draw = pixes_to_draw[:-max_pix] + pixes_len = len(pixes_to_draw) + + width_offset, height_offset = self._get_pix_offset_size( + expected_width, expected_height, pixes_len + ) + pix_width = expected_width - width_offset + pix_height = expected_height - height_offset + full_border_width = 2 * self.border_width + + pix_bg_brush = QtGui.QBrush(self.thumbnail_bg_color) + + pix_pen = QtGui.QPen() + pix_pen.setWidth(self.border_width) + pix_pen.setColor(self.border_color) + + backgrounded_images = [] + for src_pix in pixes_to_draw: + scaled_pix = src_pix.scaled( + pix_width - full_border_width, + pix_height - full_border_width, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation ) - ) + pos_x = int( + (pix_width - scaled_pix.width()) / 2 + ) + self.border_width + pos_y = int( + (pix_height - scaled_pix.height()) / 2 + ) + self.border_width + + new_pix = QtGui.QPixmap(pix_width, pix_height) + pix_painter = QtGui.QPainter() + pix_painter.begin(new_pix) + pix_painter.setBrush(pix_bg_brush) + pix_painter.setPen(pix_pen) + pix_painter.drawRect(0, 0, pix_width - 1, pix_height - 1) + pix_painter.drawPixmap(pos_x, pos_y, scaled_pix) + pix_painter.end() + backgrounded_images.append(new_pix) + + if pixes_len == 1: + width_offset_part = 0 + height_offset_part = 0 + else: + width_offset_part = int(float(width_offset) / (pixes_len - 1)) + height_offset_part = int(float(height_offset) / (pixes_len - 1)) + full_width_offset = width_offset + pix_x_offset - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(thumbnail_label, alignment=QtCore.Qt.AlignCenter) + final_pix = QtGui.QPixmap(rect_width, rect_height) + final_pix.fill(QtCore.Qt.transparent) + + final_painter = QtGui.QPainter() + final_painter.begin(final_pix) + for idx, pix in enumerate(backgrounded_images): + x_offset = full_width_offset - (width_offset_part * idx) + y_offset = (height_offset_part * idx) + pix_y_offset + final_painter.drawPixmap(x_offset, y_offset, pix) + final_painter.end() + + self._cached_pix = final_pix + + def _get_pix_offset_size(self, width, height, image_count): + if image_count == 1: + return 0, 0 - self.thumbnail_label = thumbnail_label - self.default_pix = default_pix - self.current_pix = None + part_width = width / self.offset_sep + part_height = height / self.offset_sep + return part_width, part_height + + def paintEvent(self, event): + if self._cached_pix is None: + self._cache_pix() + + painter = QtGui.QPainter() + painter.begin(self) + painter.drawPixmap(0, 0, self._cached_pix) + painter.end() From 89edb5cb9bfad4bc5fddfa04334e5e6444c0a1b1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Oct 2022 19:04:16 +0200 Subject: [PATCH 04/55] use private attribute --- openpype/tools/publisher/widgets/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 23ddeee2def..58a023d5f4b 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1570,7 +1570,7 @@ def __init__(self, controller, parent): self.creator_attrs_widget = creator_attrs_widget self.publish_attrs_widget = publish_attrs_widget - self.thumbnail_widget = thumbnail_widget + self._thumbnail_widget = thumbnail_widget self.top_bottom = top_bottom self.bottom_separator = bottom_separator From b6a2b51dad47efce305719f26caa112b3e42b7f3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Oct 2022 19:20:24 +0200 Subject: [PATCH 05/55] thumbnail widget can adapt to size changes --- openpype/tools/publisher/widgets/widgets.py | 38 +++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 58a023d5f4b..95ba321a631 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1650,11 +1650,25 @@ def __init__(self, parent): self._cached_pix = None self._height = None self._width = None + self._adapted_to_size = True + self._last_width = None + self._last_height = None + + def set_adapted_to_hint(self, enabled): + self._adapted_to_size = enabled + if self._width is not None: + self.setMinimumHeight(0) + self._width = None + + if self._height is not None: + self.setMinimumWidth(0) + self._height = None def set_width(self, width): if self._width == width: return + self._adapted_to_size = False self._width = width self._cached_pix = None self.setMinimumHeight(int( @@ -1662,18 +1676,21 @@ def set_width(self, width): )) if self._height is not None: self.setMinimumWidth(0) + self._height = None def set_height(self, height): if self._height == height: return self._height = height + self._adapted_to_size = False self._cached_pix = None self.setMinimumWidth(int( (height / self.height_ratio) * self.width_ratio )) if self._width is not None: self.setMinimumHeight(0) + self._width = None def _get_current_pixes(self): if self._current_pixes is None: @@ -1781,3 +1798,24 @@ def paintEvent(self, event): painter.begin(self) painter.drawPixmap(0, 0, self._cached_pix) painter.end() + + def _adapt_to_size(self): + if not self._adapted_to_size: + return + + width = self.width() + height = self.height() + if width == self._last_width and height == self._last_height: + return + + self._last_width = width + self._last_height = height + self._cached_pix = None + + def resizeEvent(self, event): + super(ThumbnailWidget, self).resizeEvent(event) + self._adapt_to_size() + + def showEvent(self, event): + super(ThumbnailWidget, self).showEvent(event) + self._adapt_to_size() From cc4d158c785d8a413076830aa556445d9292b234 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 15:12:51 +0200 Subject: [PATCH 06/55] moved thumbnail widget to separated file --- .../tools/publisher/widgets/create_widget.py | 2 +- .../publisher/widgets/thumbnail_widget.py | 312 ++++++++++++++++++ openpype/tools/publisher/widgets/widgets.py | 201 +---------- 3 files changed, 314 insertions(+), 201 deletions(-) create mode 100644 openpype/tools/publisher/widgets/thumbnail_widget.py diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index a8ca9af17d1..7695101ad17 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -7,10 +7,10 @@ TaskNotSetError, ) +from .thumbnail_widget import ThumbnailWidget from .widgets import ( IconValuePixmapLabel, CreateBtn, - ThumbnailWidget, ) from .assets_widget import CreateWidgetAssetsWidget from .tasks_widget import CreateWidgetTasksWidget diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py new file mode 100644 index 00000000000..29bb6fb62fc --- /dev/null +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -0,0 +1,312 @@ +import os +import tempfile +import uuid +from Qt import QtWidgets, QtCore, QtGui + +from openpype.lib import ( + run_subprocess, + is_oiio_supported, + get_oiio_tools_path, + get_ffmpeg_tool_path, +) +from openpype.lib.transcoding import ( + IMAGE_EXTENSIONS, + VIDEO_EXTENSIONS, +) + +from openpype.tools.utils import ( + paint_image_with_color, +) +from .icons import get_image + + +class ThumbnailWidget(QtWidgets.QWidget): + """Instance thumbnail widget.""" + + thumbnail_created = QtCore.Signal(str) + + width_ratio = 3.0 + height_ratio = 2.0 + border_width = 1 + offset_sep = 4 + + def __init__(self, parent): + # Missing implementation for thumbnail + # - widget kept to make a visial offset of global attr widget offset + super(ThumbnailWidget, self).__init__(parent) + self.setAcceptDrops(True) + + # TODO remove hardcoded colors + border_color = QtGui.QColor(67, 74, 86) + thumbnail_bg_color = QtGui.QColor(54, 61, 72) + + default_image = get_image("thumbnail") + default_pix = paint_image_with_color(default_image, border_color) + + self.border_color = border_color + self.thumbnail_bg_color = thumbnail_bg_color + self._default_pix = default_pix + self._current_pixes = None + self._cached_pix = None + self._height = None + self._width = None + self._adapted_to_size = True + self._last_width = None + self._last_height = None + self._review_extensions = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS) + + def _get_filepath_from_event(self, event): + mime_data = event.mimeData() + if not mime_data.hasUrls(): + return None + + filepaths = [] + for url in mime_data.urls(): + filepath = url.toLocalFile() + if os.path.exists(filepath): + filepaths.append(filepath) + + if len(filepaths) == 1: + filepath = filepaths[0] + ext = os.path.splitext(filepath)[-1] + if ext in self._review_extensions: + return filepath + return None + + def dragEnterEvent(self, event): + filepath = self._get_filepath_from_event(event) + if filepath: + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + + def dragLeaveEvent(self, event): + event.accept() + + def dropEvent(self, event): + filepath = self._get_filepath_from_event(event) + if filepath: + output = export_thumbnail(filepath) + if output: + self.thumbnail_created.emit(output) + + def set_adapted_to_hint(self, enabled): + self._adapted_to_size = enabled + if self._width is not None: + self.setMinimumHeight(0) + self._width = None + + if self._height is not None: + self.setMinimumWidth(0) + self._height = None + + def set_width(self, width): + if self._width == width: + return + + self._adapted_to_size = False + self._width = width + self._cached_pix = None + self.setMinimumHeight(int( + (width / self.width_ratio) * self.height_ratio + )) + if self._height is not None: + self.setMinimumWidth(0) + self._height = None + + def set_height(self, height): + if self._height == height: + return + + self._height = height + self._adapted_to_size = False + self._cached_pix = None + self.setMinimumWidth(int( + (height / self.height_ratio) * self.width_ratio + )) + if self._width is not None: + self.setMinimumHeight(0) + self._width = None + + def _get_current_pixes(self): + if self._current_pixes is None: + return [self._default_pix] + return self._current_pixes + + def _cache_pix(self): + rect = self.rect() + rect_width = rect.width() + rect_height = rect.height() + + pix_x_offset = 0 + pix_y_offset = 0 + expected_height = int( + (rect_width / self.width_ratio) * self.height_ratio + ) + if expected_height > rect_height: + expected_height = rect_height + expected_width = int( + (rect_height / self.height_ratio) * self.width_ratio + ) + pix_x_offset = (rect_width - expected_width) / 2 + else: + expected_width = rect_width + pix_y_offset = (rect_height - expected_height) / 2 + + pixes_to_draw = self._get_current_pixes() + max_pix = 3 + if len(pixes_to_draw) > max_pix: + pixes_to_draw = pixes_to_draw[:-max_pix] + pixes_len = len(pixes_to_draw) + + width_offset, height_offset = self._get_pix_offset_size( + expected_width, expected_height, pixes_len + ) + pix_width = expected_width - width_offset + pix_height = expected_height - height_offset + full_border_width = 2 * self.border_width + + pix_bg_brush = QtGui.QBrush(self.thumbnail_bg_color) + + pix_pen = QtGui.QPen() + pix_pen.setWidth(self.border_width) + pix_pen.setColor(self.border_color) + + backgrounded_images = [] + for src_pix in pixes_to_draw: + scaled_pix = src_pix.scaled( + pix_width - full_border_width, + pix_height - full_border_width, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + pos_x = int( + (pix_width - scaled_pix.width()) / 2 + ) + self.border_width + pos_y = int( + (pix_height - scaled_pix.height()) / 2 + ) + self.border_width + + new_pix = QtGui.QPixmap(pix_width, pix_height) + pix_painter = QtGui.QPainter() + pix_painter.begin(new_pix) + pix_painter.setBrush(pix_bg_brush) + pix_painter.setPen(pix_pen) + pix_painter.drawRect(0, 0, pix_width - 1, pix_height - 1) + pix_painter.drawPixmap(pos_x, pos_y, scaled_pix) + pix_painter.end() + backgrounded_images.append(new_pix) + + if pixes_len == 1: + width_offset_part = 0 + height_offset_part = 0 + else: + width_offset_part = int(float(width_offset) / (pixes_len - 1)) + height_offset_part = int(float(height_offset) / (pixes_len - 1)) + full_width_offset = width_offset + pix_x_offset + + final_pix = QtGui.QPixmap(rect_width, rect_height) + final_pix.fill(QtCore.Qt.transparent) + + final_painter = QtGui.QPainter() + final_painter.begin(final_pix) + for idx, pix in enumerate(backgrounded_images): + x_offset = full_width_offset - (width_offset_part * idx) + y_offset = (height_offset_part * idx) + pix_y_offset + final_painter.drawPixmap(x_offset, y_offset, pix) + final_painter.end() + + self._cached_pix = final_pix + + def _get_pix_offset_size(self, width, height, image_count): + if image_count == 1: + return 0, 0 + + part_width = width / self.offset_sep + part_height = height / self.offset_sep + return part_width, part_height + + def paintEvent(self, event): + if self._cached_pix is None: + self._cache_pix() + + painter = QtGui.QPainter() + painter.begin(self) + painter.drawPixmap(0, 0, self._cached_pix) + painter.end() + + def _adapt_to_size(self): + if not self._adapted_to_size: + return + + width = self.width() + height = self.height() + if width == self._last_width and height == self._last_height: + return + + self._last_width = width + self._last_height = height + self._cached_pix = None + + def resizeEvent(self, event): + super(ThumbnailWidget, self).resizeEvent(event) + self._adapt_to_size() + + def showEvent(self, event): + super(ThumbnailWidget, self).showEvent(event) + self._adapt_to_size() + + +def _run_silent_subprocess(args): + with open(os.devnull, "w") as devnull: + run_subprocess(args, stdout=devnull, stderr=devnull) + + +def _convert_thumbnail_oiio(src_path, dst_path): + if not is_oiio_supported(): + return None + + oiio_cmd = [ + get_oiio_tools_path(), + "-i", src_path, + "--subimage", "0", + "-o", dst_path + ] + try: + _run_silent_subprocess(oiio_cmd) + except Exception: + return None + return dst_path + + +def _convert_thumbnail_ffmpeg(src_path, dst_path): + ffmpeg_cmd = [ + get_ffmpeg_tool_path(), + "-y", + "-i", src_path, + dst_path + ] + try: + _run_silent_subprocess(ffmpeg_cmd) + except Exception: + return None + return dst_path + + +def export_thumbnail(src_path): + root_dir = os.path.join( + tempfile.gettempdir(), + "publisher_thumbnails" + ) + if not os.path.exists(root_dir): + os.makedirs(root_dir) + + ext = os.path.splitext(src_path)[-1] + if ext not in (".jpeg", ".jpg", ".png"): + ext = ".jpeg" + filename = str(uuid.uuid4()) + ext + dst_path = os.path.join(root_dir, filename) + + output_path = _convert_thumbnail_oiio(src_path, dst_path) + if not output_path: + output_path = _convert_thumbnail_ffmpeg(src_path, dst_path) + return output_path diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 95ba321a631..290f69f2806 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -16,18 +16,17 @@ PixmapLabel, BaseClickableFrame, set_style_property, - paint_image_with_color, ) from openpype.style import get_objected_colors from openpype.pipeline.create import ( SUBSET_NAME_ALLOWED_SYMBOLS, TaskNotSetError, ) +from .thumbnail_widget import ThumbnailWidget from .assets_widget import AssetsDialog from .tasks_widget import TasksModel from .icons import ( get_pixmap, - get_image, get_icon_path ) @@ -1621,201 +1620,3 @@ def set_current_instances( ) self.creator_attrs_widget.set_instances_valid(all_valid) self.publish_attrs_widget.set_instances_valid(all_valid) - - -class ThumbnailWidget(QtWidgets.QWidget): - """Instance thumbnail widget.""" - - width_ratio = 3.0 - height_ratio = 2.0 - border_width = 1 - offset_sep = 4 - - def __init__(self, parent): - # Missing implementation for thumbnail - # - widget kept to make a visial offset of global attr widget offset - super(ThumbnailWidget, self).__init__(parent) - - # TODO remove hardcoded colors - border_color = QtGui.QColor(67, 74, 86) - thumbnail_bg_color = QtGui.QColor(54, 61, 72) - - default_image = get_image("thumbnail") - default_pix = paint_image_with_color(default_image, border_color) - - self.border_color = border_color - self.thumbnail_bg_color = thumbnail_bg_color - self._default_pix = default_pix - self._current_pixes = None - self._cached_pix = None - self._height = None - self._width = None - self._adapted_to_size = True - self._last_width = None - self._last_height = None - - def set_adapted_to_hint(self, enabled): - self._adapted_to_size = enabled - if self._width is not None: - self.setMinimumHeight(0) - self._width = None - - if self._height is not None: - self.setMinimumWidth(0) - self._height = None - - def set_width(self, width): - if self._width == width: - return - - self._adapted_to_size = False - self._width = width - self._cached_pix = None - self.setMinimumHeight(int( - (width / self.width_ratio) * self.height_ratio - )) - if self._height is not None: - self.setMinimumWidth(0) - self._height = None - - def set_height(self, height): - if self._height == height: - return - - self._height = height - self._adapted_to_size = False - self._cached_pix = None - self.setMinimumWidth(int( - (height / self.height_ratio) * self.width_ratio - )) - if self._width is not None: - self.setMinimumHeight(0) - self._width = None - - def _get_current_pixes(self): - if self._current_pixes is None: - return [self._default_pix] - return self._current_pixes - - def _cache_pix(self): - rect = self.rect() - rect_width = rect.width() - rect_height = rect.height() - - pix_x_offset = 0 - pix_y_offset = 0 - expected_height = int( - (rect_width / self.width_ratio) * self.height_ratio - ) - if expected_height > rect_height: - expected_height = rect_height - expected_width = int( - (rect_height / self.height_ratio) * self.width_ratio - ) - pix_x_offset = (rect_width - expected_width) / 2 - else: - expected_width = rect_width - pix_y_offset = (rect_height - expected_height) / 2 - - pixes_to_draw = self._get_current_pixes() - max_pix = 3 - if len(pixes_to_draw) > max_pix: - pixes_to_draw = pixes_to_draw[:-max_pix] - pixes_len = len(pixes_to_draw) - - width_offset, height_offset = self._get_pix_offset_size( - expected_width, expected_height, pixes_len - ) - pix_width = expected_width - width_offset - pix_height = expected_height - height_offset - full_border_width = 2 * self.border_width - - pix_bg_brush = QtGui.QBrush(self.thumbnail_bg_color) - - pix_pen = QtGui.QPen() - pix_pen.setWidth(self.border_width) - pix_pen.setColor(self.border_color) - - backgrounded_images = [] - for src_pix in pixes_to_draw: - scaled_pix = src_pix.scaled( - pix_width - full_border_width, - pix_height - full_border_width, - QtCore.Qt.KeepAspectRatio, - QtCore.Qt.SmoothTransformation - ) - pos_x = int( - (pix_width - scaled_pix.width()) / 2 - ) + self.border_width - pos_y = int( - (pix_height - scaled_pix.height()) / 2 - ) + self.border_width - - new_pix = QtGui.QPixmap(pix_width, pix_height) - pix_painter = QtGui.QPainter() - pix_painter.begin(new_pix) - pix_painter.setBrush(pix_bg_brush) - pix_painter.setPen(pix_pen) - pix_painter.drawRect(0, 0, pix_width - 1, pix_height - 1) - pix_painter.drawPixmap(pos_x, pos_y, scaled_pix) - pix_painter.end() - backgrounded_images.append(new_pix) - - if pixes_len == 1: - width_offset_part = 0 - height_offset_part = 0 - else: - width_offset_part = int(float(width_offset) / (pixes_len - 1)) - height_offset_part = int(float(height_offset) / (pixes_len - 1)) - full_width_offset = width_offset + pix_x_offset - - final_pix = QtGui.QPixmap(rect_width, rect_height) - final_pix.fill(QtCore.Qt.transparent) - - final_painter = QtGui.QPainter() - final_painter.begin(final_pix) - for idx, pix in enumerate(backgrounded_images): - x_offset = full_width_offset - (width_offset_part * idx) - y_offset = (height_offset_part * idx) + pix_y_offset - final_painter.drawPixmap(x_offset, y_offset, pix) - final_painter.end() - - self._cached_pix = final_pix - - def _get_pix_offset_size(self, width, height, image_count): - if image_count == 1: - return 0, 0 - - part_width = width / self.offset_sep - part_height = height / self.offset_sep - return part_width, part_height - - def paintEvent(self, event): - if self._cached_pix is None: - self._cache_pix() - - painter = QtGui.QPainter() - painter.begin(self) - painter.drawPixmap(0, 0, self._cached_pix) - painter.end() - - def _adapt_to_size(self): - if not self._adapted_to_size: - return - - width = self.width() - height = self.height() - if width == self._last_width and height == self._last_height: - return - - self._last_width = width - self._last_height = height - self._cached_pix = None - - def resizeEvent(self, event): - super(ThumbnailWidget, self).resizeEvent(event) - self._adapt_to_size() - - def showEvent(self, event): - super(ThumbnailWidget, self).showEvent(event) - self._adapt_to_size() From c4432bf6ea127cf6bf113b6ae67d98c16cdc7b24 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 15:13:02 +0200 Subject: [PATCH 07/55] fix variant input style --- openpype/style/style.css | 4 ++-- openpype/tools/publisher/widgets/create_widget.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 9919973b064..585adceb268 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -911,11 +911,11 @@ PublisherTabBtn[active="1"]:hover { #PublishLogConsole { font-family: "Noto Sans Mono"; } -VariantInputsWidget QLineEdit { +#VariantInputsWidget QLineEdit { border-bottom-right-radius: 0px; border-top-right-radius: 0px; } -VariantInputsWidget QToolButton { +#VariantInputsWidget QToolButton { border-bottom-left-radius: 0px; border-top-left-radius: 0px; padding-top: 0.5em; diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index 7695101ad17..d47c2a07e05 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -164,6 +164,7 @@ def __init__(self, controller, parent=None): variant_subset_widget = QtWidgets.QWidget(creator_basics_widget) # Variant and subset input variant_widget = ResizeControlWidget(variant_subset_widget) + variant_widget.setObjectName("VariantInputsWidget") variant_input = QtWidgets.QLineEdit(variant_widget) variant_input.setObjectName("VariantInput") From bd5121b17ba468c35c466fae2908b65aca94ba4a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 15:13:32 +0200 Subject: [PATCH 08/55] traypublisher has REVIEW_EXTENSIONS as set --- openpype/hosts/traypublisher/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 555041d3897..f6dcce800d0 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -14,7 +14,7 @@ from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS -REVIEW_EXTENSIONS = IMAGE_EXTENSIONS + VIDEO_EXTENSIONS +REVIEW_EXTENSIONS = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS) def _cache_and_get_instances(creator): From 48c4c238f55bad7ae773abcf0c13d818180f0512 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:38:28 +0200 Subject: [PATCH 09/55] added helper function to get fake process id --- openpype/pipeline/__init__.py | 1 + openpype/pipeline/context_tools.py | 17 +++++++++++++++++ openpype/pipeline/workfile/lock_workfile.py | 14 +++----------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index 2cf785d981d..f5319c5a48c 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -85,6 +85,7 @@ register_host, registered_host, deregister_host, + get_process_id, ) install = install_host uninstall = uninstall_host diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index af0ee79f47d..0ec19d50fe2 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -5,6 +5,7 @@ import types import logging import platform +import uuid import pyblish.api from pyblish.lib import MessageHandler @@ -37,6 +38,7 @@ _is_installed = False +_process_id = None _registered_root = {"_": ""} _registered_host = {"_": None} # Keep modules manager (and it's modules) in memory @@ -546,3 +548,18 @@ def change_current_context(asset_doc, task_name, template_key=None): emit_event("taskChanged", data) return changes + + +def get_process_id(): + """Fake process id created on demand using uuid. + + Can be used to create process specific folders in temp directory. + + Returns: + str: Process id. + """ + + global _process_id + if _process_id is None: + _process_id = str(uuid.uuid4()) + return _process_id diff --git a/openpype/pipeline/workfile/lock_workfile.py b/openpype/pipeline/workfile/lock_workfile.py index fbec44247af..579840c07d8 100644 --- a/openpype/pipeline/workfile/lock_workfile.py +++ b/openpype/pipeline/workfile/lock_workfile.py @@ -1,9 +1,9 @@ import os import json -from uuid import uuid4 from openpype.lib import Logger, filter_profiles from openpype.lib.pype_info import get_workstation_info from openpype.settings import get_project_settings +from openpype.pipeline import get_process_id def _read_lock_file(lock_filepath): @@ -37,7 +37,7 @@ def is_workfile_locked_for_current_process(filepath): lock_filepath = _get_lock_file(filepath) data = _read_lock_file(lock_filepath) - return data["process_id"] == _get_process_id() + return data["process_id"] == get_process_id() def delete_workfile_lock(filepath): @@ -49,7 +49,7 @@ def delete_workfile_lock(filepath): def create_workfile_lock(filepath): lock_filepath = _get_lock_file(filepath) info = get_workstation_info() - info["process_id"] = _get_process_id() + info["process_id"] = get_process_id() with open(lock_filepath, "w") as stream: json.dump(info, stream) @@ -59,14 +59,6 @@ def remove_workfile_lock(filepath): delete_workfile_lock(filepath) -def _get_process_id(): - process_id = os.environ.get("OPENPYPE_PROCESS_ID") - if not process_id: - process_id = str(uuid4()) - os.environ["OPENPYPE_PROCESS_ID"] = process_id - return process_id - - def is_workfile_lock_enabled(host_name, project_name, project_setting=None): if project_setting is None: project_setting = get_project_settings(project_name) From 8cf23ec864a3bffca1f264b2bf0709862edc2807 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:40:18 +0200 Subject: [PATCH 10/55] create context can store thumbnails --- openpype/pipeline/create/context.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 52a17292339..71338f96e0e 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1077,6 +1077,8 @@ def __init__( # Shared data across creators during collection phase self._collection_shared_data = None + self.thumbnail_paths_by_instance_id = {} + # Trigger reset if was enabled if reset: self.reset(discover_publish_plugins) @@ -1146,11 +1148,37 @@ def reset(self, discover_publish_plugins=True): self.reset_finalization() + def refresh_thumbnails(self): + """Cleanup thumbnail paths. + + Remove all thumbnail filepaths that are empty or lead to files which + does not exists or of instances that are not available anymore. + """ + + invalid = set() + for instance_id, path in self.thumbnail_paths_by_instance_id.items(): + instance_available = True + if instance_id is not None: + instance_available = ( + instance_id not in self._instances_by_id + ) + + if ( + not instance_available + or not path + or not os.path.exists(path) + ): + invalid.add(instance_id) + + for instance_id in invalid: + self.thumbnail_paths_by_instance_id.pop(instance_id) + def reset_preparation(self): """Prepare attributes that must be prepared/cleaned before reset.""" # Give ability to store shared data for collection phase self._collection_shared_data = {} + self.refresh_thumbnails() def reset_finalization(self): """Cleanup of attributes after reset.""" From d71a1f8d524e34a47058110590cea38c7a4659a9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:41:11 +0200 Subject: [PATCH 11/55] creators can set thumbnail path and allow to pass thumbnail in precreation part --- openpype/pipeline/create/__init__.py | 2 ++ openpype/pipeline/create/constants.py | 2 ++ openpype/pipeline/create/creator_plugins.py | 11 +++++++++++ 3 files changed, 15 insertions(+) diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index 4b91951a088..89b876e6def 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -1,6 +1,7 @@ from .constants import ( SUBSET_NAME_ALLOWED_SYMBOLS, DEFAULT_SUBSET_TEMPLATE, + PRE_CREATE_THUMBNAIL_KEY, ) from .subset_name import ( @@ -40,6 +41,7 @@ __all__ = ( "SUBSET_NAME_ALLOWED_SYMBOLS", "DEFAULT_SUBSET_TEMPLATE", + "PRE_CREATE_THUMBNAIL_KEY", "TaskNotSetError", "get_subset_name", diff --git a/openpype/pipeline/create/constants.py b/openpype/pipeline/create/constants.py index 3af96519479..375cfc4a12f 100644 --- a/openpype/pipeline/create/constants.py +++ b/openpype/pipeline/create/constants.py @@ -1,8 +1,10 @@ SUBSET_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_." DEFAULT_SUBSET_TEMPLATE = "{family}{Variant}" +PRE_CREATE_THUMBNAIL_KEY = "thumbnail_source" __all__ = ( "SUBSET_NAME_ALLOWED_SYMBOLS", "DEFAULT_SUBSET_TEMPLATE", + "PRE_CREATE_THUMBNAIL_KEY", ) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index c69abb88612..1e8423e48b7 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -442,6 +442,13 @@ def collection_shared_data(self): return self.create_context.collection_shared_data + def set_instance_thumbnail_path(self, instance_id, thumbnail_path=None): + """Set path to thumbnail for instance.""" + + self.create_context.thumbnail_paths_by_instance_id[instance_id] = ( + thumbnail_path + ) + class Creator(BaseCreator): """Creator that has more information for artist to show in UI. @@ -468,6 +475,10 @@ class Creator(BaseCreator): # - in some cases it may confuse artists because it would not be used # e.g. for buld creators create_allow_context_change = True + # A thumbnail can be passed in precreate attributes + # - if is set to True is should expect that a thumbnail path under key + # PRE_CREATE_THUMBNAIL_KEY can be sent in data with precreate data + create_allow_thumbnail = False # Precreate attribute definitions showed before creation # - similar to instance attribute definitions From f5c73f5a948ea52fbfe18d0a38f27d5506f7ad4e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:41:53 +0200 Subject: [PATCH 12/55] create context collector also adds thumbnail source to instances --- .../plugins/publish/collect_movie_batch.py | 3 ++- .../publish/collect_simple_instances.py | 3 ++- .../plugins/publish/extract_thumbnail.py | 13 +++++---- .../publish/collect_from_create_context.py | 27 ++++++++++++++++--- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_movie_batch.py b/openpype/hosts/traypublisher/plugins/publish/collect_movie_batch.py index 3d93e2c9273..5f8b2878b78 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_movie_batch.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_movie_batch.py @@ -40,7 +40,8 @@ def process(self, instance): if creator_attributes["add_review_family"]: repre["tags"].append("review") instance.data["families"].append("review") - instance.data["thumbnailSource"] = file_url + if not instance.data.get("thumbnailSource"): + instance.data["thumbnailSource"] = file_url instance.data["source"] = file_url diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py index 7035a61d7bd..183195a5155 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py @@ -188,7 +188,8 @@ def _create_review_representation( if "review" not in instance.data["families"]: instance.data["families"].append("review") - instance.data["thumbnailSource"] = first_filepath + if not instance.data.get("thumbnailSource"): + instance.data["thumbnailSource"] = first_filepath review_representation["tags"].append("review") self.log.debug("Representation {} was marked for review. {}".format( diff --git a/openpype/hosts/traypublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/traypublisher/plugins/publish/extract_thumbnail.py index 7781bb7b3ee..96aefe00439 100644 --- a/openpype/hosts/traypublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/traypublisher/plugins/publish/extract_thumbnail.py @@ -42,7 +42,15 @@ def process(self, instance): "Processing instance with subset name {}".format(subset_name) ) + # Check if already has thumbnail created + if self._already_has_thumbnail(instance): + self.log.info("Thumbnail representation already present.") + return + thumbnail_source = instance.data.get("thumbnailSource") + if not thumbnail_source: + thumbnail_source = instance.context.data.get("thumbnailSource") + if not thumbnail_source: self.log.debug("Thumbnail source not filled. Skipping.") return @@ -53,11 +61,6 @@ def process(self, instance): thumbnail_source)) return - # Check if already has thumbnail created - if self._already_has_thumbnail(instance): - self.log.info("Thumbnail representation already present.") - return - # Create temp directory for thumbnail # - this is to avoid "override" of source file dst_staging = tempfile.mkdtemp(prefix="pyblish_tmp_") diff --git a/openpype/plugins/publish/collect_from_create_context.py b/openpype/plugins/publish/collect_from_create_context.py index fc0f97b187a..ddb6908a4cb 100644 --- a/openpype/plugins/publish/collect_from_create_context.py +++ b/openpype/plugins/publish/collect_from_create_context.py @@ -19,14 +19,28 @@ def process(self, context): if not create_context: return + thumbnail_paths_by_instance_id = ( + create_context.thumbnail_paths_by_instance_id + ) + context.data["thumbnailSource"] = ( + thumbnail_paths_by_instance_id.get(None) + ) + project_name = create_context.project_name if project_name: context.data["projectName"] = project_name + for created_instance in create_context.instances: instance_data = created_instance.data_to_store() if instance_data["active"]: + thumbnail_path = thumbnail_paths_by_instance_id.get( + created_instance.id + ) self.create_instance( - context, instance_data, created_instance.transient_data + context, + instance_data, + created_instance.transient_data, + thumbnail_path ) # Update global data to context @@ -39,7 +53,13 @@ def process(self, context): legacy_io.Session[key] = value os.environ[key] = value - def create_instance(self, context, in_data, transient_data): + def create_instance( + self, + context, + in_data, + transient_data, + thumbnail_path + ): subset = in_data["subset"] # If instance data already contain families then use it instance_families = in_data.get("families") or [] @@ -53,7 +73,8 @@ def create_instance(self, context, in_data, transient_data): "name": subset, "family": in_data["family"], "families": instance_families, - "representations": [] + "representations": [], + "thumbnailSource": thumbnail_path }) for key, value in in_data.items(): if key not in instance.data: From 52c83227a42e6e8a37fb831e494933a4cb0e6ee1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:42:26 +0200 Subject: [PATCH 13/55] enhanced caching of instance data in tray publisher --- openpype/hosts/traypublisher/api/plugin.py | 34 ++++++++++++---------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index f6dcce800d0..0b62492477a 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -1,3 +1,5 @@ +import collections + from openpype.lib.attribute_definitions import FileDef from openpype.pipeline.create import ( Creator, @@ -29,7 +31,11 @@ def _cache_and_get_instances(creator): shared_key = "openpype.traypublisher.instances" if shared_key not in creator.collection_shared_data: - creator.collection_shared_data[shared_key] = list_instances() + instances_by_creator_id = collections.defaultdict(list) + for instance_data in list_instances(): + creator_id = instance_data.get("creator_identifier") + instances_by_creator_id[creator_id].append(instance_data) + creator.collection_shared_data[shared_key] = instances_by_creator_id return creator.collection_shared_data[shared_key] @@ -37,13 +43,12 @@ class HiddenTrayPublishCreator(HiddenCreator): host_name = "traypublisher" def collect_instances(self): - for instance_data in _cache_and_get_instances(self): - creator_id = instance_data.get("creator_identifier") - if creator_id == self.identifier: - instance = CreatedInstance.from_existing( - instance_data, self - ) - self._add_instance_to_context(instance) + instance_data_by_identifier = _cache_and_get_instances(self) + for instance_data in instance_data_by_identifier[self.identifier]: + instance = CreatedInstance.from_existing( + instance_data, self + ) + self._add_instance_to_context(instance) def update_instances(self, update_list): update_instances(update_list) @@ -74,13 +79,12 @@ class TrayPublishCreator(Creator): host_name = "traypublisher" def collect_instances(self): - for instance_data in _cache_and_get_instances(self): - creator_id = instance_data.get("creator_identifier") - if creator_id == self.identifier: - instance = CreatedInstance.from_existing( - instance_data, self - ) - self._add_instance_to_context(instance) + instance_data_by_identifier = _cache_and_get_instances(self) + for instance_data in instance_data_by_identifier[self.identifier]: + instance = CreatedInstance.from_existing( + instance_data, self + ) + self._add_instance_to_context(instance) def update_instances(self, update_list): update_instances(update_list) From 3489a71b08d10478541b2e831f622eae81846620 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:42:43 +0200 Subject: [PATCH 14/55] settings creator allows thumbnail in precreation --- openpype/hosts/traypublisher/api/plugin.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 0b62492477a..40877968e97 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -4,7 +4,8 @@ from openpype.pipeline.create import ( Creator, HiddenCreator, - CreatedInstance + CreatedInstance, + PRE_CREATE_THUMBNAIL_KEY, ) from .pipeline import ( @@ -114,11 +115,14 @@ def _store_new_instance(self, new_instance): class SettingsCreator(TrayPublishCreator): create_allow_context_change = True + create_allow_thumbnail = True extensions = [] def create(self, subset_name, data, pre_create_data): # Pass precreate data to creator attributes + thumbnail_path = pre_create_data.pop(PRE_CREATE_THUMBNAIL_KEY, None) + data["creator_attributes"] = pre_create_data data["settings_creator"] = True # Create new instance @@ -126,6 +130,9 @@ def create(self, subset_name, data, pre_create_data): self._store_new_instance(new_instance) + if thumbnail_path: + self.set_instance_thumbnail_path(new_instance.id, thumbnail_path) + def get_instance_attr_defs(self): return [ FileDef( From 9c048478bb3870a1d50eb4e669fd6436218925a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:44:34 +0200 Subject: [PATCH 15/55] added small comment --- openpype/pipeline/create/creator_plugins.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 1e8423e48b7..ef92b7ccc48 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -478,6 +478,9 @@ class Creator(BaseCreator): # A thumbnail can be passed in precreate attributes # - if is set to True is should expect that a thumbnail path under key # PRE_CREATE_THUMBNAIL_KEY can be sent in data with precreate data + # - is disabled by default because the feature was added in later stages + # and creators who would not expect PRE_CREATE_THUMBNAIL_KEY could + # cause issues with instance data create_allow_thumbnail = False # Precreate attribute definitions showed before creation From 90222b1b3fa6beddaf67f0989aa86a20074b6300 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:46:09 +0200 Subject: [PATCH 16/55] publisher has temp dir for thumbnails which is cleared up on publisher close --- openpype/tools/publisher/control.py | 39 +++++++++++++++++++++++++++++ openpype/tools/publisher/window.py | 1 + 2 files changed, 40 insertions(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index e05cffe20ed..7a2f2bbb82b 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -4,6 +4,8 @@ import traceback import collections import uuid +import tempfile +import shutil from abc import ABCMeta, abstractmethod, abstractproperty import six @@ -24,6 +26,7 @@ KnownPublishError, registered_host, legacy_io, + get_process_id, ) from openpype.pipeline.create import ( CreateContext, @@ -1283,6 +1286,22 @@ def emit_card_message( pass + @abstractmethod + def get_thumbnail_temp_dir_path(self): + """Return path to directory where thumbnails can be temporary stored. + + Returns: + str: Path to a directory. + """ + + pass + + @abstractmethod + def clear_thumbnail_temp_dir_path(self): + """Remove content of thumbnail temp directory.""" + + pass + class BasePublisherController(AbstractPublisherController): """Implement common logic for controllers. @@ -1523,6 +1542,26 @@ def get_creator_icon(self, identifier): return creator_item.icon return None + def get_thumbnail_temp_dir_path(self): + """Return path to directory where thumbnails can be temporary stored. + + Returns: + str: Path to a directory. + """ + + return os.path.join( + tempfile.gettempdir(), + "publisher_thumbnails", + get_process_id() + ) + + def clear_thumbnail_temp_dir_path(self): + """Remove content of thumbnail temp directory.""" + + dirpath = self.get_thumbnail_temp_dir_path() + if os.path.exists(dirpath): + shutil.rmtree(dirpath) + class PublisherController(BasePublisherController): """Middleware between UI, CreateContext and publish Context. diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index a3387043b80..77d4339052c 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -383,6 +383,7 @@ def _on_show_restart_timer(self): def closeEvent(self, event): self.save_changes() self._reset_on_show = True + self._controller.clear_thumbnail_temp_dir_path() super(PublisherWindow, self).closeEvent(event) def save_changes(self): From 7c09494ad0d04a377d93e35ada9399370ba0a45a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:46:22 +0200 Subject: [PATCH 17/55] implemented getter and setters for thumbnails --- openpype/tools/publisher/control.py | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 7a2f2bbb82b..eb6425b8208 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1259,6 +1259,14 @@ def convertor_items(self): def trigger_convertor_items(self, convertor_identifiers): pass + @abstractmethod + def get_thumbnail_paths_for_instances(self, instance_ids): + pass + + @abstractmethod + def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping): + pass + @abstractmethod def set_comment(self, comment): """Set comment on pyblish context. @@ -1817,6 +1825,29 @@ def _reset_instances(self): self._on_create_instance_change() + def get_thumbnail_paths_for_instances(self, instance_ids): + thumbnail_paths_by_instance_id = ( + self._create_context.thumbnail_paths_by_instance_id + ) + return { + instance_id: thumbnail_paths_by_instance_id.get(instance_id) + for instance_id in instance_ids + } + + def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping): + thumbnail_paths_by_instance_id = ( + self._create_context.thumbnail_paths_by_instance_id + ) + for instance_id, thumbnail_path in thumbnail_path_mapping.items(): + thumbnail_paths_by_instance_id[instance_id] = thumbnail_path + + self._emit_event( + "instance.thumbnail.changed", + { + "mapping": thumbnail_path_mapping + } + ) + def emit_card_message( self, message, message_type=CardMessageTypes.standard ): From 82aea56768145fc5978a45cf65f3c2f2a26e1684 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:46:35 +0200 Subject: [PATCH 18/55] added forgotter abstract methods --- openpype/tools/publisher/control.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index eb6425b8208..b113c9316a9 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1118,11 +1118,13 @@ def create( pass + @abstractmethod def save_changes(self): """Save changes in create context.""" pass + @abstractmethod def remove_instances(self, instance_ids): """Remove list of instances from create context.""" # TODO expect instance ids From 334ec3310fd586b0361b4237f58c509f833b4af9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:46:58 +0200 Subject: [PATCH 19/55] added potential implementation of remote qt publisher controller --- openpype/tools/publisher/control_qt.py | 46 ++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/openpype/tools/publisher/control_qt.py b/openpype/tools/publisher/control_qt.py index 56132a40464..8b5856f2343 100644 --- a/openpype/tools/publisher/control_qt.py +++ b/openpype/tools/publisher/control_qt.py @@ -115,6 +115,11 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._created_instances = {} + self._thumbnail_paths_by_instance_id = None + + def _reset_attributes(self): + super()._reset_attributes() + self._thumbnail_paths_by_instance_id = None @abstractmethod def _get_serialized_instances(self): @@ -180,6 +185,11 @@ def remote_events_handler(self, event_data): self.host_is_valid = event["value"] return + # Don't skip because UI want know about it too + if event.topic == "instance.thumbnail.changed": + for instance_id, path in event["mapping"].items(): + self.thumbnail_paths_by_instance_id[instance_id] = path + # Topics that can be just passed by because are not affecting # controller itself # - "show.card.message" @@ -256,6 +266,42 @@ def get_task_names_by_asset_names(self, asset_names): def get_existing_subset_names(self, asset_name): pass + @property + def thumbnail_paths_by_instance_id(self): + if self._thumbnail_paths_by_instance_id is None: + self._thumbnail_paths_by_instance_id = ( + self._collect_thumbnail_paths_by_instance_id() + ) + return self._thumbnail_paths_by_instance_id + + def get_thumbnail_path_for_instance(self, instance_id): + return self.thumbnail_paths_by_instance_id.get(instance_id) + + def set_thumbnail_path_for_instance(self, instance_id, thumbnail_path): + self._set_thumbnail_path_on_context(self, instance_id, thumbnail_path) + + @abstractmethod + def _collect_thumbnail_paths_by_instance_id(self): + """Collect thumbnail paths by instance id in remote controller. + + These should be collected from 'CreatedContext' there. + + Returns: + Dict[str, str]: Mapping of thumbnail path by instance id. + """ + + pass + + @abstractmethod + def _set_thumbnail_path_on_context(self, instance_id, thumbnail_path): + """Send change of thumbnail path in remote controller. + + That should trigger event 'instance.thumbnail.changed' which is + captured and handled in default implementation in this class. + """ + + pass + @abstractmethod def get_subset_name( self, From 7a21dc8812c4c7720d2e12d67a9b27761c0f39bb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:47:30 +0200 Subject: [PATCH 20/55] thumbnail widget is using potential of controller --- .../tools/publisher/widgets/create_widget.py | 2 +- .../publisher/widgets/thumbnail_widget.py | 43 +++++++++++++------ openpype/tools/publisher/widgets/widgets.py | 2 +- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index d47c2a07e05..fc35cd31cdb 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -196,7 +196,7 @@ def __init__(self, controller, parent=None): creator_basics_layout.addWidget(variant_subset_label, 0) creator_basics_layout.addWidget(variant_subset_widget, 0) - thumbnail_widget = ThumbnailWidget(creators_attrs_top) + thumbnail_widget = ThumbnailWidget(controller, creators_attrs_top) creators_attrs_top_layout = QtWidgets.QHBoxLayout(creators_attrs_top) creators_attrs_top_layout.setContentsMargins(0, 0, 0, 0) diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index 29bb6fb62fc..c93b555d5b9 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -1,5 +1,4 @@ import os -import tempfile import uuid from Qt import QtWidgets, QtCore, QtGui @@ -17,6 +16,8 @@ from openpype.tools.utils import ( paint_image_with_color, ) +from openpype.tools.publisher.control import CardMessageTypes + from .icons import get_image @@ -30,7 +31,7 @@ class ThumbnailWidget(QtWidgets.QWidget): border_width = 1 offset_sep = 4 - def __init__(self, parent): + def __init__(self, controller, parent): # Missing implementation for thumbnail # - widget kept to make a visial offset of global attr widget offset super(ThumbnailWidget, self).__init__(parent) @@ -55,6 +56,9 @@ def __init__(self, parent): self._last_height = None self._review_extensions = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS) + self._controller = controller + self._output_dir = controller.get_thumbnail_temp_dir_path() + def _get_filepath_from_event(self, event): mime_data = event.mimeData() if not mime_data.hasUrls(): @@ -84,10 +88,17 @@ def dragLeaveEvent(self, event): def dropEvent(self, event): filepath = self._get_filepath_from_event(event) - if filepath: - output = export_thumbnail(filepath) - if output: - self.thumbnail_created.emit(output) + if not filepath: + return + + output = export_thumbnail(filepath, self._output_dir) + if output: + self.thumbnail_created.emit(output) + else: + self._controller.emit_card_message( + "Couldn't convert the source for thumbnail", + CardMessageTypes.error + ) def set_adapted_to_hint(self, enabled): self._adapted_to_size = enabled @@ -127,6 +138,16 @@ def set_height(self, height): self.setMinimumHeight(0) self._width = None + def set_current_thumbnails(self, thumbnail_paths=None): + pixes = [] + if thumbnail_paths: + for thumbnail_path in thumbnail_paths: + pixes.append(QtGui.QPixmap(thumbnail_path)) + + self._current_pixes = pixes or None + self._cached_pix = None + self.repaint() + def _get_current_pixes(self): if self._current_pixes is None: return [self._default_pix] @@ -181,10 +202,10 @@ def _cache_pix(self): ) pos_x = int( (pix_width - scaled_pix.width()) / 2 - ) + self.border_width + ) pos_y = int( (pix_height - scaled_pix.height()) / 2 - ) + self.border_width + ) new_pix = QtGui.QPixmap(pix_width, pix_height) pix_painter = QtGui.QPainter() @@ -292,11 +313,7 @@ def _convert_thumbnail_ffmpeg(src_path, dst_path): return dst_path -def export_thumbnail(src_path): - root_dir = os.path.join( - tempfile.gettempdir(), - "publisher_thumbnails" - ) +def export_thumbnail(src_path, root_dir): if not os.path.exists(root_dir): os.makedirs(root_dir) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 290f69f2806..ae32e5f42d5 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1472,7 +1472,7 @@ def __init__(self, controller, parent): # Global attributes global_attrs_widget = GlobalAttrsWidget(controller, top_widget) - thumbnail_widget = ThumbnailWidget(top_widget) + thumbnail_widget = ThumbnailWidget(controller, top_widget) top_layout = QtWidgets.QHBoxLayout(top_widget) top_layout.setContentsMargins(0, 0, 0, 0) From 25d8139df229eb2c1f30d1219764cd2ff1cb9643 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:48:02 +0200 Subject: [PATCH 21/55] creator adds thumbnail to creators create --- .../tools/publisher/widgets/create_widget.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index fc35cd31cdb..a610c405a47 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -1,9 +1,11 @@ +import os import re from Qt import QtWidgets, QtCore, QtGui from openpype.pipeline.create import ( SUBSET_NAME_ALLOWED_SYMBOLS, + PRE_CREATE_THUMBNAIL_KEY, TaskNotSetError, ) @@ -269,6 +271,7 @@ def __init__(self, controller, parent=None): self._on_current_session_context_request ) tasks_widget.task_changed.connect(self._on_task_change) + thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create) controller.event_system.add_callback( "plugins.refresh.finished", self._on_plugins_refresh @@ -302,6 +305,7 @@ def __init__(self, controller, parent=None): self._prereq_timer = prereq_timer self._first_show = True + self._last_thumbnail_path = None @property def current_asset_name(self): @@ -492,6 +496,14 @@ def _on_task_change(self): if self._context_change_is_enabled(): self._invalidate_prereq_deffered() + def _on_thumbnail_create(self, thumbnail_path): + last_path = self._last_thumbnail_path + if last_path and os.path.exists(last_path): + os.remove(last_path) + + self._last_thumbnail_path = thumbnail_path + self._thumbnail_widget.set_current_thumbnails([thumbnail_path]) + def _on_current_session_context_request(self): self._assets_widget.set_current_session_asset() task_name = self.current_task_name @@ -730,6 +742,8 @@ def _on_create(self): task_name = self._get_task_name() pre_create_data = self._pre_create_widget.current_value() + pre_create_data[PRE_CREATE_THUMBNAIL_KEY] = self._last_thumbnail_path + # Where to define these data? # - what data show be stored? instance_data = { @@ -749,3 +763,5 @@ def _on_create(self): if success: self._set_creator(self._selected_creator) self._controller.emit_card_message("Creation finished...") + self._last_thumbnail_path = None + self._thumbnail_widget.set_current_thumbnails() From e537d2d83c3249178bdd217776245ce73546210f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:48:23 +0200 Subject: [PATCH 22/55] handle thumbnail changes in subset widget --- openpype/tools/publisher/widgets/widgets.py | 65 ++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index ae32e5f42d5..1682e3e047f 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -3,6 +3,8 @@ import re import copy import functools +import uuid +import shutil import collections from Qt import QtWidgets, QtCore, QtGui import qtawesome @@ -1064,6 +1066,7 @@ def __init__(self, controller, parent): def _on_submit(self): """Commit changes for selected instances.""" + variant_value = None asset_name = None task_name = None @@ -1132,6 +1135,7 @@ def _on_submit(self): def _on_cancel(self): """Cancel changes and set back to their irigin value.""" + self.variant_input.reset_to_origin() self.asset_value_widget.reset_to_origin() self.task_value_widget.reset_to_origin() @@ -1257,6 +1261,7 @@ def __init__(self, controller, parent): def set_instances_valid(self, valid): """Change valid state of current instances.""" + if ( self._content_widget is not None and self._content_widget.isEnabled() != valid @@ -1265,6 +1270,7 @@ def set_instances_valid(self, valid): def set_current_instances(self, instances): """Set current instances for which are attribute definitions shown.""" + prev_content_widget = self._scroll_area.widget() if prev_content_widget: self._scroll_area.takeWidget() @@ -1354,6 +1360,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): families. Similar definitions are merged into one (different label does not count). """ + def __init__(self, controller, parent): super(PublishPluginAttrsWidget, self).__init__(parent) @@ -1387,6 +1394,7 @@ def set_instances_valid(self, valid): def set_current_instances(self, instances, context_selected): """Set current instances for which are attribute definitions shown.""" + prev_content_widget = self._scroll_area.widget() if prev_content_widget: self._scroll_area.takeWidget() @@ -1560,6 +1568,11 @@ def __init__(self, controller, parent): self._on_instance_context_changed ) convert_btn.clicked.connect(self._on_convert_click) + thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create) + + controller.event_system.add_callback( + "instance.thumbnail.changed", self._on_thumbnail_changed + ) self._controller = controller @@ -1596,10 +1609,11 @@ def set_current_instances( """Change currently selected items. Args: - instances(list): List of currently selected + instances(List[CreatedInstance]): List of currently selected instances. context_selected(bool): Is context selected. """ + all_valid = True for instance in instances: if not instance.has_valid_context: @@ -1620,3 +1634,52 @@ def set_current_instances( ) self.creator_attrs_widget.set_instances_valid(all_valid) self.publish_attrs_widget.set_instances_valid(all_valid) + + self._update_thumbnails() + + def _on_thumbnail_create(self, path): + instance_ids = [ + instance.id + for instance in self._current_instances + ] + if self._context_selected: + instance_ids.append(None) + + if not instance_ids: + return + + mapping = {} + if len(instance_ids) == 1: + mapping[instance_ids[0]] = path + + else: + for instance_id in range(len(instance_ids)): + root = os.path.dirname(path) + ext = os.path.splitext(path)[-1] + dst_path = os.path.join(root, str(uuid.uuid4()) + ext) + shutil.copy(path, dst_path) + mapping[instance_id] = dst_path + + self._controller.set_thumbnail_paths_for_instances(mapping) + + def _on_thumbnail_changed(self, event): + self._update_thumbnails() + + def _update_thumbnails(self): + instance_ids = [ + instance.id + for instance in self._current_instances + ] + if self._context_selected: + instance_ids.append(None) + + mapping = self._controller.get_thumbnail_paths_for_instances( + instance_ids + ) + thumbnail_paths = [] + for instance_id in instance_ids: + path = mapping[instance_id] + if path: + thumbnail_paths.append(path) + + self._thumbnail_widget.set_current_thumbnails(thumbnail_paths) From eff9b5710e78b3c38948aab48d3d63cbec41964a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:56:31 +0200 Subject: [PATCH 23/55] CreateItem knows if support drop of thumbnails in create page --- openpype/tools/publisher/control.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index b113c9316a9..10734a69f4c 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -828,6 +828,7 @@ def __init__( default_variant, default_variants, create_allow_context_change, + create_allow_thumbnail, pre_create_attributes_defs ): self.identifier = identifier @@ -841,6 +842,7 @@ def __init__( self.default_variant = default_variant self.default_variants = default_variants self.create_allow_context_change = create_allow_context_change + self.create_allow_thumbnail = create_allow_thumbnail self.instance_attributes_defs = instance_attributes_defs self.pre_create_attributes_defs = pre_create_attributes_defs @@ -867,6 +869,7 @@ def from_creator(cls, creator): default_variants = None pre_create_attr_defs = None create_allow_context_change = None + create_allow_thumbnail = None if creator_type is CreatorTypes.artist: description = creator.get_description() detail_description = creator.get_detail_description() @@ -874,6 +877,7 @@ def from_creator(cls, creator): default_variants = creator.get_default_variants() pre_create_attr_defs = creator.get_pre_create_attr_defs() create_allow_context_change = creator.create_allow_context_change + create_allow_thumbnail = creator.create_allow_thumbnail identifier = creator.identifier return cls( @@ -889,6 +893,7 @@ def from_creator(cls, creator): default_variant, default_variants, create_allow_context_change, + create_allow_thumbnail, pre_create_attr_defs ) @@ -917,6 +922,7 @@ def to_data(self): "default_variant": self.default_variant, "default_variants": self.default_variants, "create_allow_context_change": self.create_allow_context_change, + "create_allow_thumbnail": self.create_allow_thumbnail, "instance_attributes_defs": instance_attributes_defs, "pre_create_attributes_defs": pre_create_attributes_defs, } From ba849905c63fcef2aa630c5ab3a00be9b7f0ef17 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:56:51 +0200 Subject: [PATCH 24/55] thumbnail widdget can disable dropping --- .../publisher/widgets/thumbnail_widget.py | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index c93b555d5b9..8c436021479 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -44,20 +44,25 @@ def __init__(self, controller, parent): default_image = get_image("thumbnail") default_pix = paint_image_with_color(default_image, border_color) + self._controller = controller + self._output_dir = controller.get_thumbnail_temp_dir_path() + self.border_color = border_color self.thumbnail_bg_color = thumbnail_bg_color self._default_pix = default_pix + + self._drop_enabled = True + self._current_pixes = None self._cached_pix = None + + self._review_extensions = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS) + self._height = None self._width = None self._adapted_to_size = True self._last_width = None self._last_height = None - self._review_extensions = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS) - - self._controller = controller - self._output_dir = controller.get_thumbnail_temp_dir_path() def _get_filepath_from_event(self, event): mime_data = event.mimeData() @@ -78,6 +83,10 @@ def _get_filepath_from_event(self, event): return None def dragEnterEvent(self, event): + if not self._drop_enabled: + event.ignore() + return + filepath = self._get_filepath_from_event(event) if filepath: event.setDropAction(QtCore.Qt.CopyAction) @@ -87,6 +96,9 @@ def dragLeaveEvent(self, event): event.accept() def dropEvent(self, event): + if not self._drop_enabled: + return + filepath = self._get_filepath_from_event(event) if not filepath: return @@ -100,6 +112,13 @@ def dropEvent(self, event): CardMessageTypes.error ) + def set_drop_enabled(self, enabled): + if self._drop_enabled is enabled: + return + self._drop_enabled = enabled + self._cached_pix = None + self.repaint() + def set_adapted_to_hint(self, enabled): self._adapted_to_size = enabled if self._width is not None: @@ -149,6 +168,10 @@ def set_current_thumbnails(self, thumbnail_paths=None): self.repaint() def _get_current_pixes(self): + if not self._drop_enabled: + # TODO different image for disabled drop + return [self._default_pix] + if self._current_pixes is None: return [self._default_pix] return self._current_pixes From 40fbc3a7e21fbc5f34bdcf275dfd3c189bc70392 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:57:07 +0200 Subject: [PATCH 25/55] create widget is handling enabled dropping of thumbnails --- openpype/tools/publisher/constants.py | 7 ++++--- .../tools/publisher/widgets/create_widget.py | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index 8bea69c8123..74337ea1abb 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -20,9 +20,10 @@ SORT_VALUE_ROLE = QtCore.Qt.UserRole + 2 IS_GROUP_ROLE = QtCore.Qt.UserRole + 3 CREATOR_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 4 -FAMILY_ROLE = QtCore.Qt.UserRole + 5 -GROUP_ROLE = QtCore.Qt.UserRole + 6 -CONVERTER_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 7 +CREATOR_THUMBNAIL_ENABLED_ROLE = QtCore.Qt.UserRole + 5 +FAMILY_ROLE = QtCore.Qt.UserRole + 6 +GROUP_ROLE = QtCore.Qt.UserRole + 7 +CONVERTER_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 8 __all__ = ( diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index a610c405a47..f0db132d98d 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -20,7 +20,8 @@ from ..constants import ( VARIANT_TOOLTIP, CREATOR_IDENTIFIER_ROLE, - FAMILY_ROLE + FAMILY_ROLE, + CREATOR_THUMBNAIL_ENABLED_ROLE, ) SEPARATORS = ("---separator---", "---") @@ -457,6 +458,10 @@ def _refresh_creators(self): item.setData(creator_item.label, QtCore.Qt.DisplayRole) item.setData(identifier, CREATOR_IDENTIFIER_ROLE) + item.setData( + creator_item.create_allow_thumbnail, + CREATOR_THUMBNAIL_ENABLED_ROLE + ) item.setData(creator_item.family, FAMILY_ROLE) # Remove families that are no more available @@ -558,6 +563,10 @@ def _set_creator(self, creator_item): self._set_context_enabled(creator_item.create_allow_context_change) self._refresh_asset() + self._thumbnail_widget.set_drop_enabled( + creator_item.create_allow_thumbnail + ) + default_variants = creator_item.default_variants if not default_variants: default_variants = ["Main"] @@ -742,7 +751,10 @@ def _on_create(self): task_name = self._get_task_name() pre_create_data = self._pre_create_widget.current_value() - pre_create_data[PRE_CREATE_THUMBNAIL_KEY] = self._last_thumbnail_path + if index.data(CREATOR_THUMBNAIL_ENABLED_ROLE): + pre_create_data[PRE_CREATE_THUMBNAIL_KEY] = ( + self._last_thumbnail_path + ) # Where to define these data? # - what data show be stored? From f18e5c5896b8dc1fdcc9c1b08044d1a31a240d89 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 28 Oct 2022 10:31:30 +0200 Subject: [PATCH 26/55] moved extract thumbnail from tray publisher to global plugins --- .../publish/extract_thumbnail_from_source.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/{hosts/traypublisher/plugins/publish/extract_thumbnail.py => plugins/publish/extract_thumbnail_from_source.py} (100%) diff --git a/openpype/hosts/traypublisher/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail_from_source.py similarity index 100% rename from openpype/hosts/traypublisher/plugins/publish/extract_thumbnail.py rename to openpype/plugins/publish/extract_thumbnail_from_source.py From b42346e187cb5b18b6903074c0a31245c5d416fc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 28 Oct 2022 10:31:49 +0200 Subject: [PATCH 27/55] use faster checks first --- .../publish/extract_thumbnail_from_source.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/openpype/plugins/publish/extract_thumbnail_from_source.py b/openpype/plugins/publish/extract_thumbnail_from_source.py index 96aefe00439..eaf48df5cb6 100644 --- a/openpype/plugins/publish/extract_thumbnail_from_source.py +++ b/openpype/plugins/publish/extract_thumbnail_from_source.py @@ -41,12 +41,6 @@ def process(self, instance): self.log.info( "Processing instance with subset name {}".format(subset_name) ) - - # Check if already has thumbnail created - if self._already_has_thumbnail(instance): - self.log.info("Thumbnail representation already present.") - return - thumbnail_source = instance.data.get("thumbnailSource") if not thumbnail_source: thumbnail_source = instance.context.data.get("thumbnailSource") @@ -55,10 +49,15 @@ def process(self, instance): self.log.debug("Thumbnail source not filled. Skipping.") return - elif not os.path.exists(thumbnail_source): - self.log.debug( - "Thumbnail source file was not found {}. Skipping.".format( - thumbnail_source)) + # Check if already has thumbnail created + if self._already_has_thumbnail(instance): + self.log.info("Thumbnail representation already present.") + return + + if not os.path.exists(thumbnail_source): + self.log.debug(( + "Thumbnail source is set but file was not found {}. Skipping." + ).format(thumbnail_source)) return # Create temp directory for thumbnail From 7a18d3d85efe1c55d0413768ade1a230fb00cb4f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 28 Oct 2022 10:31:58 +0200 Subject: [PATCH 28/55] removed hosts filter --- openpype/plugins/publish/extract_thumbnail_from_source.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/plugins/publish/extract_thumbnail_from_source.py b/openpype/plugins/publish/extract_thumbnail_from_source.py index eaf48df5cb6..1d75b6c3814 100644 --- a/openpype/plugins/publish/extract_thumbnail_from_source.py +++ b/openpype/plugins/publish/extract_thumbnail_from_source.py @@ -34,7 +34,6 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): label = "Extract Thumbnail (from source)" # Before 'ExtractThumbnail' in global plugins order = pyblish.api.ExtractorOrder - 0.00001 - hosts = ["traypublisher"] def process(self, instance): subset_name = instance.data["subset"] From fec7df18f12334a11ff0cb61177993d5dc89f626 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 28 Oct 2022 12:07:20 +0200 Subject: [PATCH 29/55] use colors from style data --- openpype/tools/publisher/widgets/thumbnail_widget.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index 8c436021479..dcb18d9cb7d 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -2,6 +2,7 @@ import uuid from Qt import QtWidgets, QtCore, QtGui +from openpype.style import get_objected_colors from openpype.lib import ( run_subprocess, is_oiio_supported, @@ -37,9 +38,8 @@ def __init__(self, controller, parent): super(ThumbnailWidget, self).__init__(parent) self.setAcceptDrops(True) - # TODO remove hardcoded colors - border_color = QtGui.QColor(67, 74, 86) - thumbnail_bg_color = QtGui.QColor(54, 61, 72) + border_color = get_objected_colors("bg-buttons").get_qcolor() + thumbnail_bg_color = get_objected_colors("border").get_qcolor() default_image = get_image("thumbnail") default_pix = paint_image_with_color(default_image, border_color) From f7dec32e5c50836273d1c294b1d1733ff1671e1b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 28 Oct 2022 12:08:49 +0200 Subject: [PATCH 30/55] draw disabled drop with slashed circle --- .../publisher/widgets/thumbnail_widget.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index dcb18d9cb7d..48f40f7b5b4 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -1,5 +1,7 @@ import os import uuid +import math + from Qt import QtWidgets, QtCore, QtGui from openpype.style import get_objected_colors @@ -40,6 +42,7 @@ def __init__(self, controller, parent): border_color = get_objected_colors("bg-buttons").get_qcolor() thumbnail_bg_color = get_objected_colors("border").get_qcolor() + overlay_color = get_objected_colors("font").get_qcolor() default_image = get_image("thumbnail") default_pix = paint_image_with_color(default_image, border_color) @@ -49,6 +52,7 @@ def __init__(self, controller, parent): self.border_color = border_color self.thumbnail_bg_color = thumbnail_bg_color + self.overlay_color = overlay_color self._default_pix = default_pix self._drop_enabled = True @@ -231,8 +235,14 @@ def _cache_pix(self): ) new_pix = QtGui.QPixmap(pix_width, pix_height) + new_pix.fill(QtCore.Qt.transparent) pix_painter = QtGui.QPainter() pix_painter.begin(new_pix) + pix_painter.setRenderHints( + pix_painter.Antialiasing + | pix_painter.SmoothPixmapTransform + | pix_painter.HighQualityAntialiasing + ) pix_painter.setBrush(pix_bg_brush) pix_painter.setPen(pix_pen) pix_painter.drawRect(0, 0, pix_width - 1, pix_height - 1) @@ -253,14 +263,63 @@ def _cache_pix(self): final_painter = QtGui.QPainter() final_painter.begin(final_pix) + final_painter.setRenderHints( + final_painter.Antialiasing + | final_painter.SmoothPixmapTransform + | final_painter.HighQualityAntialiasing + ) for idx, pix in enumerate(backgrounded_images): x_offset = full_width_offset - (width_offset_part * idx) y_offset = (height_offset_part * idx) + pix_y_offset final_painter.drawPixmap(x_offset, y_offset, pix) + + if not self._drop_enabled: + overlay = self._get_drop_disabled_overlay(rect_width, rect_height) + final_painter.drawPixmap(0, 0, overlay) + final_painter.end() self._cached_pix = final_pix + def _get_drop_disabled_overlay(self, width, height): + min_size = min(width, height) + circle_size = int(min_size * 0.8) + pen_width = int(circle_size * 0.1) + if pen_width < 1: + pen_width = 1 + + x_offset = int((width - circle_size) / 2) + y_offset = int((height - circle_size) / 2) + half_size = int(circle_size / 2) + angle = math.radians(45) + line_offset_p = QtCore.QPoint( + half_size * math.cos(angle), + half_size * math.sin(angle) + ) + overlay_pix = QtGui.QPixmap(width, height) + overlay_pix.fill(QtCore.Qt.transparent) + + painter = QtGui.QPainter() + painter.begin(overlay_pix) + painter.setRenderHints( + painter.Antialiasing + | painter.SmoothPixmapTransform + | painter.HighQualityAntialiasing + ) + painter.setBrush(QtCore.Qt.transparent) + pen = QtGui.QPen(self.overlay_color) + pen.setWidth(pen_width) + painter.setPen(pen) + rect = QtCore.QRect(x_offset, y_offset, circle_size, circle_size) + painter.drawEllipse(rect) + painter.drawLine( + rect.center() - line_offset_p, + rect.center() + line_offset_p + ) + painter.end() + + return overlay_pix + def _get_pix_offset_size(self, width, height, image_count): if image_count == 1: return 0, 0 From 4cf0fe9145f67d17e19232ba4b0ae1f769a1cb3d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 28 Oct 2022 12:14:36 +0200 Subject: [PATCH 31/55] disable drop when no instance is selected --- openpype/tools/publisher/widgets/widgets.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 1682e3e047f..96addb70a32 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1673,6 +1673,11 @@ def _update_thumbnails(self): if self._context_selected: instance_ids.append(None) + if not instance_ids: + self._thumbnail_widget.set_drop_enabled(False) + self._thumbnail_widget.set_current_thumbnails(None) + return + mapping = self._controller.get_thumbnail_paths_for_instances( instance_ids ) From b3ca4abf6d0b191e57f001cbfc89a69baa7741e9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 28 Oct 2022 12:45:07 +0200 Subject: [PATCH 32/55] allow the drop if instances are selected --- openpype/tools/publisher/widgets/widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 96addb70a32..fb9bd761f44 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1687,4 +1687,5 @@ def _update_thumbnails(self): if path: thumbnail_paths.append(path) + self._thumbnail_widget.set_drop_enabled(True) self._thumbnail_widget.set_current_thumbnails(thumbnail_paths) From 6c80a7f81f1e4ea5df27480ed0a2d7bb60fe165b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 28 Oct 2022 13:07:44 +0200 Subject: [PATCH 33/55] context thumbnail is not used directly in extract thumbnail from source but creates thumbnail elsewhere and store it to "thumbnailPath" key on context --- .../publish/extract_thumbnail_from_source.py | 66 ++++++++++++------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/openpype/plugins/publish/extract_thumbnail_from_source.py b/openpype/plugins/publish/extract_thumbnail_from_source.py index 1d75b6c3814..df877cec29f 100644 --- a/openpype/plugins/publish/extract_thumbnail_from_source.py +++ b/openpype/plugins/publish/extract_thumbnail_from_source.py @@ -36,23 +36,49 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): order = pyblish.api.ExtractorOrder - 0.00001 def process(self, instance): + self._create_context_thumbnail(instance.context) + subset_name = instance.data["subset"] self.log.info( "Processing instance with subset name {}".format(subset_name) ) thumbnail_source = instance.data.get("thumbnailSource") - if not thumbnail_source: - thumbnail_source = instance.context.data.get("thumbnailSource") - if not thumbnail_source: self.log.debug("Thumbnail source not filled. Skipping.") return # Check if already has thumbnail created - if self._already_has_thumbnail(instance): + if self._instance_has_thumbnail(instance): self.log.info("Thumbnail representation already present.") return + dst_filepath = self._create_thumbnail( + instance.context, thumbnail_source + ) + if not dst_filepath: + return + + dst_staging, dst_filename = os.path.split(dst_filepath) + new_repre = { + "name": "thumbnail", + "ext": "jpg", + "files": dst_filename, + "stagingDir": dst_staging, + "thumbnail": True, + "tags": ["thumbnail"] + } + + # adding representation + self.log.debug( + "Adding thumbnail representation: {}".format(new_repre) + ) + instance.data["representations"].append(new_repre) + + def _create_thumbnail(self, context, thumbnail_source): + if not thumbnail_source: + self.log.debug("Thumbnail source not filled. Skipping.") + return + if not os.path.exists(thumbnail_source): self.log.debug(( "Thumbnail source is set but file was not found {}. Skipping." @@ -66,7 +92,7 @@ def process(self, instance): "Create temp directory {} for thumbnail".format(dst_staging) ) # Store new staging to cleanup paths - instance.context.data["cleanupFullPaths"].append(dst_staging) + context.data["cleanupFullPaths"].append(dst_staging) thumbnail_created = False oiio_supported = is_oiio_supported() @@ -98,26 +124,12 @@ def process(self, instance): ) # Skip representation and try next one if wasn't created - if not thumbnail_created: - self.log.warning("Thumbanil has not been created.") - return - - new_repre = { - "name": "thumbnail", - "ext": "jpg", - "files": dst_filename, - "stagingDir": dst_staging, - "thumbnail": True, - "tags": ["thumbnail"] - } + if thumbnail_created: + return full_output_path - # adding representation - self.log.debug( - "Adding thumbnail representation: {}".format(new_repre) - ) - instance.data["representations"].append(new_repre) + self.log.warning("Thumbanil has not been created.") - def _already_has_thumbnail(self, instance): + def _instance_has_thumbnail(self, instance): if "representations" not in instance.data: self.log.warning( "Instance does not have 'representations' key filled" @@ -172,3 +184,11 @@ def create_thumbnail_ffmpeg(self, src_path, dst_path): exc_info=True ) return False + + def _create_context_thumbnail(self, context): + if "thumbnailPath" in context.data: + return + + thumbnail_source = context.data.get("thumbnailSource") + thumbnail_path = self._create_thumbnail(thumbnail_source) + context.data["thumbnailPath"] = thumbnail_path From 831023b3029a554ef9b390c2a5659186bd10ee57 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 28 Oct 2022 13:08:01 +0200 Subject: [PATCH 34/55] integrate thumbnail can use context thumbnail (if is available) --- .../plugins/publish/integrate_thumbnail.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index e7046ba2eac..d8a3a000416 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -33,6 +33,8 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): ] def process(self, instance): + context_thumbnail_path = instance.context.get("thumbnailPath") + env_key = "AVALON_THUMBNAIL_ROOT" thumbnail_root_format_key = "{thumbnail_root}" thumbnail_root = os.environ.get(env_key) or "" @@ -66,37 +68,43 @@ def process(self, instance): ).format(env_key)) return + version_id = None thumb_repre = None thumb_repre_anatomy_data = None for repre_info in published_repres.values(): repre = repre_info["representation"] + if version_id is None: + version_id = repre["parent"] + if repre["name"].lower() == "thumbnail": thumb_repre = repre thumb_repre_anatomy_data = repre_info["anatomy_data"] break + # Use context thumbnail (if is available) if not thumb_repre: self.log.debug( "There is not representation with name \"thumbnail\"" ) + src_full_path = context_thumbnail_path + else: + # Get full path to thumbnail file from representation + src_full_path = os.path.normpath(thumb_repre["data"]["path"]) + + if not os.path.exists(src_full_path): + self.log.warning("Thumbnail file was not found. Path: {}".format( + src_full_path + )) return - version = get_version_by_id(project_name, thumb_repre["parent"]) + version = get_version_by_id(project_name, version_id) if not version: raise AssertionError( "There does not exist version with id {}".format( - str(thumb_repre["parent"]) + str(version_id) ) ) - # Get full path to thumbnail file from representation - src_full_path = os.path.normpath(thumb_repre["data"]["path"]) - if not os.path.exists(src_full_path): - self.log.warning("Thumbnail file was not found. Path: {}".format( - src_full_path - )) - return - filename, file_extension = os.path.splitext(src_full_path) # Create id for mongo entity now to fill anatomy template thumbnail_doc = new_thumbnail_doc() From b8719c6cd2906021aadb359572a178238b24cd58 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 28 Oct 2022 13:45:58 +0200 Subject: [PATCH 35/55] don't remove last path --- openpype/tools/publisher/widgets/create_widget.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index f0db132d98d..e3c171912f6 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -502,10 +502,6 @@ def _on_task_change(self): self._invalidate_prereq_deffered() def _on_thumbnail_create(self, thumbnail_path): - last_path = self._last_thumbnail_path - if last_path and os.path.exists(last_path): - os.remove(last_path) - self._last_thumbnail_path = thumbnail_path self._thumbnail_widget.set_current_thumbnails([thumbnail_path]) From 5861480c77725d11f87d312e964321b9d4b889fb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 28 Oct 2022 13:46:38 +0200 Subject: [PATCH 36/55] add missing argument --- openpype/plugins/publish/extract_thumbnail_from_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_thumbnail_from_source.py b/openpype/plugins/publish/extract_thumbnail_from_source.py index df877cec29f..8da12138071 100644 --- a/openpype/plugins/publish/extract_thumbnail_from_source.py +++ b/openpype/plugins/publish/extract_thumbnail_from_source.py @@ -190,5 +190,5 @@ def _create_context_thumbnail(self, context): return thumbnail_source = context.data.get("thumbnailSource") - thumbnail_path = self._create_thumbnail(thumbnail_source) + thumbnail_path = self._create_thumbnail(context, thumbnail_source) context.data["thumbnailPath"] = thumbnail_path From 1b040af666233794f78751f0061b8b6bbbe3c81f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 28 Oct 2022 13:50:00 +0200 Subject: [PATCH 37/55] removed unused import --- openpype/tools/publisher/widgets/create_widget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index e3c171912f6..a57a8791a89 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -1,4 +1,3 @@ -import os import re from Qt import QtWidgets, QtCore, QtGui From 7647176173d3e4d28290650761370efeede53505 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 13:37:07 +0100 Subject: [PATCH 38/55] hide thumbnail widget if drop is disabled --- .../tools/publisher/widgets/create_widget.py | 2 +- .../publisher/widgets/thumbnail_widget.py | 62 ------------------- openpype/tools/publisher/widgets/widgets.py | 4 +- 3 files changed, 3 insertions(+), 65 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index a57a8791a89..4540e70eb8d 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -558,7 +558,7 @@ def _set_creator(self, creator_item): self._set_context_enabled(creator_item.create_allow_context_change) self._refresh_asset() - self._thumbnail_widget.set_drop_enabled( + self._thumbnail_widget.setVisible( creator_item.create_allow_thumbnail ) diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index 48f40f7b5b4..53152f488f1 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -55,8 +55,6 @@ def __init__(self, controller, parent): self.overlay_color = overlay_color self._default_pix = default_pix - self._drop_enabled = True - self._current_pixes = None self._cached_pix = None @@ -87,10 +85,6 @@ def _get_filepath_from_event(self, event): return None def dragEnterEvent(self, event): - if not self._drop_enabled: - event.ignore() - return - filepath = self._get_filepath_from_event(event) if filepath: event.setDropAction(QtCore.Qt.CopyAction) @@ -100,9 +94,6 @@ def dragLeaveEvent(self, event): event.accept() def dropEvent(self, event): - if not self._drop_enabled: - return - filepath = self._get_filepath_from_event(event) if not filepath: return @@ -116,13 +107,6 @@ def dropEvent(self, event): CardMessageTypes.error ) - def set_drop_enabled(self, enabled): - if self._drop_enabled is enabled: - return - self._drop_enabled = enabled - self._cached_pix = None - self.repaint() - def set_adapted_to_hint(self, enabled): self._adapted_to_size = enabled if self._width is not None: @@ -172,10 +156,6 @@ def set_current_thumbnails(self, thumbnail_paths=None): self.repaint() def _get_current_pixes(self): - if not self._drop_enabled: - # TODO different image for disabled drop - return [self._default_pix] - if self._current_pixes is None: return [self._default_pix] return self._current_pixes @@ -273,53 +253,11 @@ def _cache_pix(self): y_offset = (height_offset_part * idx) + pix_y_offset final_painter.drawPixmap(x_offset, y_offset, pix) - if not self._drop_enabled: - overlay = self._get_drop_disabled_overlay(rect_width, rect_height) - final_painter.drawPixmap(0, 0, overlay) final_painter.end() self._cached_pix = final_pix - def _get_drop_disabled_overlay(self, width, height): - min_size = min(width, height) - circle_size = int(min_size * 0.8) - pen_width = int(circle_size * 0.1) - if pen_width < 1: - pen_width = 1 - - x_offset = int((width - circle_size) / 2) - y_offset = int((height - circle_size) / 2) - half_size = int(circle_size / 2) - angle = math.radians(45) - line_offset_p = QtCore.QPoint( - half_size * math.cos(angle), - half_size * math.sin(angle) - ) - overlay_pix = QtGui.QPixmap(width, height) - overlay_pix.fill(QtCore.Qt.transparent) - - painter = QtGui.QPainter() - painter.begin(overlay_pix) - painter.setRenderHints( - painter.Antialiasing - | painter.SmoothPixmapTransform - | painter.HighQualityAntialiasing - ) - painter.setBrush(QtCore.Qt.transparent) - pen = QtGui.QPen(self.overlay_color) - pen.setWidth(pen_width) - painter.setPen(pen) - rect = QtCore.QRect(x_offset, y_offset, circle_size, circle_size) - painter.drawEllipse(rect) - painter.drawLine( - rect.center() - line_offset_p, - rect.center() + line_offset_p - ) - painter.end() - - return overlay_pix - def _get_pix_offset_size(self, width, height, image_count): if image_count == 1: return 0, 0 diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index fb9bd761f44..9af9595a97a 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1674,7 +1674,7 @@ def _update_thumbnails(self): instance_ids.append(None) if not instance_ids: - self._thumbnail_widget.set_drop_enabled(False) + self._thumbnail_widget.setVisible(False) self._thumbnail_widget.set_current_thumbnails(None) return @@ -1687,5 +1687,5 @@ def _update_thumbnails(self): if path: thumbnail_paths.append(path) - self._thumbnail_widget.set_drop_enabled(True) + self._thumbnail_widget.setVisible(True) self._thumbnail_widget.set_current_thumbnails(thumbnail_paths) From 0a7c20398cc985f050a8399fb6cced87c98f3b78 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 13:41:13 +0100 Subject: [PATCH 39/55] draw dashes if user can drop thumbnails --- .../publisher/widgets/thumbnail_widget.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index 53152f488f1..808210a6730 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -155,11 +155,6 @@ def set_current_thumbnails(self, thumbnail_paths=None): self._cached_pix = None self.repaint() - def _get_current_pixes(self): - if self._current_pixes is None: - return [self._default_pix] - return self._current_pixes - def _cache_pix(self): rect = self.rect() rect_width = rect.width() @@ -180,7 +175,13 @@ def _cache_pix(self): expected_width = rect_width pix_y_offset = (rect_height - expected_height) / 2 - pixes_to_draw = self._get_current_pixes() + if self._current_pixes is None: + draw_dashes = True + pixes_to_draw = [self._default_pix] + else: + draw_dashes = False + pixes_to_draw = self._current_pixes + max_pix = 3 if len(pixes_to_draw) > max_pix: pixes_to_draw = pixes_to_draw[:-max_pix] @@ -253,6 +254,15 @@ def _cache_pix(self): y_offset = (height_offset_part * idx) + pix_y_offset final_painter.drawPixmap(x_offset, y_offset, pix) + # Draw drop enabled dashes + if draw_dashes: + pen = QtGui.QPen() + pen.setWidth(1) + pen.setBrush(QtCore.Qt.darkGray) + pen.setStyle(QtCore.Qt.DashLine) + final_painter.setPen(pen) + final_painter.setBrush(QtCore.Qt.transparent) + final_painter.drawRect(rect) final_painter.end() From 1c604ee1be458d4b65bbc41999396902572d9afc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 13:41:39 +0100 Subject: [PATCH 40/55] define max thumbnails in class variable --- openpype/tools/publisher/widgets/thumbnail_widget.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index 808210a6730..e119d640b4b 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -33,6 +33,7 @@ class ThumbnailWidget(QtWidgets.QWidget): height_ratio = 2.0 border_width = 1 offset_sep = 4 + max_thumbnails = 3 def __init__(self, controller, parent): # Missing implementation for thumbnail @@ -182,9 +183,8 @@ def _cache_pix(self): draw_dashes = False pixes_to_draw = self._current_pixes - max_pix = 3 - if len(pixes_to_draw) > max_pix: - pixes_to_draw = pixes_to_draw[:-max_pix] + if len(pixes_to_draw) > self.max_thumbnails: + pixes_to_draw = pixes_to_draw[:-self.max_thumbnails] pixes_len = len(pixes_to_draw) width_offset, height_offset = self._get_pix_offset_size( From 72e729f59f5276ecb752fc59d01921b7815656de Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 15:07:57 +0100 Subject: [PATCH 41/55] fix mapping on multiselection --- openpype/tools/publisher/widgets/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 9af9595a97a..c7b69659916 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1653,7 +1653,7 @@ def _on_thumbnail_create(self, path): mapping[instance_ids[0]] = path else: - for instance_id in range(len(instance_ids)): + for instance_id in instance_ids: root = os.path.dirname(path) ext = os.path.splitext(path)[-1] dst_path = os.path.join(root, str(uuid.uuid4()) + ext) From c9d255ce59719ed2282f6528c1bd32c8bbbc5df2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 15:18:07 +0100 Subject: [PATCH 42/55] separated thumbnail painter widget and thumbnail widget to be able handle buttons overlay --- .../tools/publisher/widgets/create_widget.py | 4 + .../publisher/widgets/thumbnail_widget.py | 284 +++++++++++------- openpype/tools/publisher/widgets/widgets.py | 18 ++ 3 files changed, 196 insertions(+), 110 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index 4540e70eb8d..7bdac46273e 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -272,6 +272,7 @@ def __init__(self, controller, parent=None): ) tasks_widget.task_changed.connect(self._on_task_change) thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create) + thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear) controller.event_system.add_callback( "plugins.refresh.finished", self._on_plugins_refresh @@ -504,6 +505,9 @@ def _on_thumbnail_create(self, thumbnail_path): self._last_thumbnail_path = thumbnail_path self._thumbnail_widget.set_current_thumbnails([thumbnail_path]) + def _on_thumbnail_clear(self): + self._last_thumbnail_path = None + def _on_current_session_context_request(self): self._assets_widget.set_current_session_asset() task_name = self.current_task_name diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index e119d640b4b..b45d61623e2 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -1,6 +1,5 @@ import os import uuid -import math from Qt import QtWidgets, QtCore, QtGui @@ -24,22 +23,15 @@ from .icons import get_image -class ThumbnailWidget(QtWidgets.QWidget): - """Instance thumbnail widget.""" - - thumbnail_created = QtCore.Signal(str) - +class ThumbnailPainterWidget(QtWidgets.QWidget): width_ratio = 3.0 height_ratio = 2.0 border_width = 1 - offset_sep = 4 max_thumbnails = 3 + offset_sep = 4 - def __init__(self, controller, parent): - # Missing implementation for thumbnail - # - widget kept to make a visial offset of global attr widget offset - super(ThumbnailWidget, self).__init__(parent) - self.setAcceptDrops(True) + def __init__(self, parent): + super(ThumbnailPainterWidget, self).__init__(parent) border_color = get_objected_colors("bg-buttons").get_qcolor() thumbnail_bg_color = get_objected_colors("border").get_qcolor() @@ -48,103 +40,22 @@ def __init__(self, controller, parent): default_image = get_image("thumbnail") default_pix = paint_image_with_color(default_image, border_color) - self._controller = controller - self._output_dir = controller.get_thumbnail_temp_dir_path() - self.border_color = border_color self.thumbnail_bg_color = thumbnail_bg_color self.overlay_color = overlay_color self._default_pix = default_pix - self._current_pixes = None self._cached_pix = None + self._current_pixes = None + self._has_pixes = False - self._review_extensions = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS) - - self._height = None - self._width = None - self._adapted_to_size = True - self._last_width = None - self._last_height = None - - def _get_filepath_from_event(self, event): - mime_data = event.mimeData() - if not mime_data.hasUrls(): - return None - - filepaths = [] - for url in mime_data.urls(): - filepath = url.toLocalFile() - if os.path.exists(filepath): - filepaths.append(filepath) - - if len(filepaths) == 1: - filepath = filepaths[0] - ext = os.path.splitext(filepath)[-1] - if ext in self._review_extensions: - return filepath - return None - - def dragEnterEvent(self, event): - filepath = self._get_filepath_from_event(event) - if filepath: - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - - def dragLeaveEvent(self, event): - event.accept() - - def dropEvent(self, event): - filepath = self._get_filepath_from_event(event) - if not filepath: - return - - output = export_thumbnail(filepath, self._output_dir) - if output: - self.thumbnail_created.emit(output) - else: - self._controller.emit_card_message( - "Couldn't convert the source for thumbnail", - CardMessageTypes.error - ) - - def set_adapted_to_hint(self, enabled): - self._adapted_to_size = enabled - if self._width is not None: - self.setMinimumHeight(0) - self._width = None - - if self._height is not None: - self.setMinimumWidth(0) - self._height = None - - def set_width(self, width): - if self._width == width: - return - - self._adapted_to_size = False - self._width = width - self._cached_pix = None - self.setMinimumHeight(int( - (width / self.width_ratio) * self.height_ratio - )) - if self._height is not None: - self.setMinimumWidth(0) - self._height = None - - def set_height(self, height): - if self._height == height: - return + @property + def has_pixes(self): + return self._has_pixes - self._height = height - self._adapted_to_size = False + def clear_cache(self): self._cached_pix = None - self.setMinimumWidth(int( - (height / self.height_ratio) * self.width_ratio - )) - if self._width is not None: - self.setMinimumHeight(0) - self._width = None + self.repaint() def set_current_thumbnails(self, thumbnail_paths=None): pixes = [] @@ -153,8 +64,17 @@ def set_current_thumbnails(self, thumbnail_paths=None): pixes.append(QtGui.QPixmap(thumbnail_path)) self._current_pixes = pixes or None - self._cached_pix = None - self.repaint() + self._has_pixes = self._current_pixes is not None + self.clear_cache() + + def paintEvent(self, event): + if self._cached_pix is None: + self._cache_pix() + + painter = QtGui.QPainter() + painter.begin(self) + painter.drawPixmap(0, 0, self._cached_pix) + painter.end() def _cache_pix(self): rect = self.rect() @@ -276,14 +196,146 @@ def _get_pix_offset_size(self, width, height, image_count): part_height = height / self.offset_sep return part_width, part_height - def paintEvent(self, event): - if self._cached_pix is None: - self._cache_pix() - painter = QtGui.QPainter() - painter.begin(self) - painter.drawPixmap(0, 0, self._cached_pix) - painter.end() +class ThumbnailWidget(QtWidgets.QWidget): + """Instance thumbnail widget.""" + + thumbnail_created = QtCore.Signal(str) + thumbnail_cleared = QtCore.Signal() + + def __init__(self, controller, parent): + # Missing implementation for thumbnail + # - widget kept to make a visial offset of global attr widget offset + super(ThumbnailWidget, self).__init__(parent) + self.setAcceptDrops(True) + + thumbnail_painter = ThumbnailPainterWidget(self) + + buttons_widget = QtWidgets.QWidget(self) + buttons_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + clear_button = QtWidgets.QPushButton("x", buttons_widget) + + buttons_layout = QtWidgets.QHBoxLayout(buttons_widget) + buttons_layout.setContentsMargins(3, 3, 3, 3) + buttons_layout.addStretch(1) + buttons_layout.addWidget(clear_button, 0) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(thumbnail_painter) + + clear_button.clicked.connect(self._on_clear_clicked) + + self._controller = controller + self._output_dir = controller.get_thumbnail_temp_dir_path() + + self._review_extensions = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS) + + self._height = None + self._width = None + self._adapted_to_size = True + self._last_width = None + self._last_height = None + + self._buttons_widget = buttons_widget + self._thumbnail_painter = thumbnail_painter + + @property + def width_ratio(self): + return self._thumbnail_painter.width_ratio + + @property + def height_ratio(self): + return self._thumbnail_painter.height_ratio + + def _get_filepath_from_event(self, event): + mime_data = event.mimeData() + if not mime_data.hasUrls(): + return None + + filepaths = [] + for url in mime_data.urls(): + filepath = url.toLocalFile() + if os.path.exists(filepath): + filepaths.append(filepath) + + if len(filepaths) == 1: + filepath = filepaths[0] + ext = os.path.splitext(filepath)[-1] + if ext in self._review_extensions: + return filepath + return None + + def dragEnterEvent(self, event): + filepath = self._get_filepath_from_event(event) + if filepath: + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + + def dragLeaveEvent(self, event): + event.accept() + + def dropEvent(self, event): + filepath = self._get_filepath_from_event(event) + if not filepath: + return + + output = export_thumbnail(filepath, self._output_dir) + if output: + self.thumbnail_created.emit(output) + else: + self._controller.emit_card_message( + "Couldn't convert the source for thumbnail", + CardMessageTypes.error + ) + + def set_adapted_to_hint(self, enabled): + self._adapted_to_size = enabled + if self._width is not None: + self.setMinimumHeight(0) + self._width = None + + if self._height is not None: + self.setMinimumWidth(0) + self._height = None + + def set_width(self, width): + if self._width == width: + return + + self._adapted_to_size = False + self._width = width + self.setMinimumHeight(int( + (width / self.width_ratio) * self.height_ratio + )) + if self._height is not None: + self.setMinimumWidth(0) + self._height = None + self._thumbnail_painter.clear_cache() + + def set_height(self, height): + if self._height == height: + return + + self._height = height + self._adapted_to_size = False + self.setMinimumWidth(int( + (height / self.height_ratio) * self.width_ratio + )) + if self._width is not None: + self.setMinimumHeight(0) + self._width = None + + self._thumbnail_painter.clear_cache() + + def set_current_thumbnails(self, thumbnail_paths=None): + self._thumbnail_painter.set_current_thumbnails(thumbnail_paths) + self._update_buttons_position() + + def _on_clear_clicked(self): + self.set_current_thumbnails() + self.thumbnail_cleared.emit() def _adapt_to_size(self): if not self._adapted_to_size: @@ -296,15 +348,27 @@ def _adapt_to_size(self): self._last_width = width self._last_height = height - self._cached_pix = None + self._thumbnail_painter.clear_cache() + + def _update_buttons_position(self): + self._buttons_widget.setVisible(self._thumbnail_painter.has_pixes) + size = self.size() + my_height = size.height() + height = self._buttons_widget.sizeHint().height() + self._buttons_widget.setGeometry( + 0, my_height - height, + size.width(), height + ) def resizeEvent(self, event): super(ThumbnailWidget, self).resizeEvent(event) self._adapt_to_size() + self._update_buttons_position() def showEvent(self, event): super(ThumbnailWidget, self).showEvent(event) self._adapt_to_size() + self._update_buttons_position() def _run_silent_subprocess(args): diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index c7b69659916..744c51ce077 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1569,6 +1569,7 @@ def __init__(self, controller, parent): ) convert_btn.clicked.connect(self._on_convert_click) thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create) + thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear) controller.event_system.add_callback( "instance.thumbnail.changed", self._on_thumbnail_changed @@ -1662,6 +1663,23 @@ def _on_thumbnail_create(self, path): self._controller.set_thumbnail_paths_for_instances(mapping) + def _on_thumbnail_clear(self): + instance_ids = [ + instance.id + for instance in self._current_instances + ] + if self._context_selected: + instance_ids.append(None) + + if not instance_ids: + return + + mapping = { + instance_id: None + for instance_id in instance_ids + } + self._controller.set_thumbnail_paths_for_instances(mapping) + def _on_thumbnail_changed(self, event): self._update_thumbnails() From 1f2ad7c304f3d8e623bfac657d01d2ebe22c790f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 16:45:54 +0100 Subject: [PATCH 43/55] don't use alpha on button hover color --- openpype/style/data.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index 146af846632..404ca6944c0 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -27,7 +27,7 @@ "bg": "#2C313A", "bg-inputs": "#21252B", "bg-buttons": "#434a56", - "bg-button-hover": "rgba(168, 175, 189, 0.3)", + "bg-button-hover": "rgb(81, 86, 97)", "bg-inputs-disabled": "#2C313A", "bg-buttons-disabled": "#434a56", From 44f6d1c724ce5312dc42b044088f99870bf19494 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 18:10:44 +0100 Subject: [PATCH 44/55] set render hint for paint image with color --- openpype/tools/utils/lib.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index d8dd80046ad..5302946c28b 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -79,6 +79,11 @@ def paint_image_with_color(image, color): pixmap.fill(QtCore.Qt.transparent) painter = QtGui.QPainter(pixmap) + painter.setRenderHints( + painter.Antialiasing + | painter.SmoothPixmapTransform + | painter.HighQualityAntialiasing + ) painter.setClipRegion(alpha_region) painter.setPen(QtCore.Qt.NoPen) painter.setBrush(color) From 1d827f997fa36127c3627c0199cd9aa16e8f2e5d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 18:12:29 +0100 Subject: [PATCH 45/55] draw backgroup only final image --- .../tools/publisher/widgets/thumbnail_widget.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index b45d61623e2..69161a7bd76 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -114,11 +114,6 @@ def _cache_pix(self): pix_height = expected_height - height_offset full_border_width = 2 * self.border_width - pix_bg_brush = QtGui.QBrush(self.thumbnail_bg_color) - - pix_pen = QtGui.QPen() - pix_pen.setWidth(self.border_width) - pix_pen.setColor(self.border_color) backgrounded_images = [] for src_pix in pixes_to_draw: @@ -144,9 +139,6 @@ def _cache_pix(self): | pix_painter.SmoothPixmapTransform | pix_painter.HighQualityAntialiasing ) - pix_painter.setBrush(pix_bg_brush) - pix_painter.setPen(pix_pen) - pix_painter.drawRect(0, 0, pix_width - 1, pix_height - 1) pix_painter.drawPixmap(pos_x, pos_y, scaled_pix) pix_painter.end() backgrounded_images.append(new_pix) @@ -162,6 +154,10 @@ def _cache_pix(self): final_pix = QtGui.QPixmap(rect_width, rect_height) final_pix.fill(QtCore.Qt.transparent) + bg_pen = QtGui.QPen() + bg_pen.setWidth(self.border_width) + bg_pen.setColor(self.border_color) + final_painter = QtGui.QPainter() final_painter.begin(final_pix) final_painter.setRenderHints( @@ -169,6 +165,10 @@ def _cache_pix(self): | final_painter.SmoothPixmapTransform | final_painter.HighQualityAntialiasing ) + final_painter.setBrush(QtGui.QBrush(self.thumbnail_bg_color)) + final_painter.setPen(bg_pen) + final_painter.drawRect(rect) + for idx, pix in enumerate(backgrounded_images): x_offset = full_width_offset - (width_offset_part * idx) y_offset = (height_offset_part * idx) + pix_y_offset From 53c3ae8e5614c0b939ba1875499f58ffdf1bb811 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 18:13:03 +0100 Subject: [PATCH 46/55] added helper function to draw checker --- .../publisher/widgets/thumbnail_widget.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index 69161a7bd76..3f159d58121 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -29,6 +29,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): border_width = 1 max_thumbnails = 3 offset_sep = 4 + checker_boxes_count = 20 def __init__(self, parent): super(ThumbnailPainterWidget, self).__init__(parent) @@ -76,6 +77,43 @@ def paintEvent(self, event): painter.drawPixmap(0, 0, self._cached_pix) painter.end() + def _draw_empty_checker(self, width, height): + checker_size = int(float(width) / self.checker_boxes_count) + if checker_size < 1: + checker_size = 1 + + single_checker_pix = QtGui.QPixmap(checker_size * 2, checker_size * 2) + single_checker_pix.fill(QtCore.Qt.transparent) + single_checker_painter = QtGui.QPainter() + single_checker_painter.begin(single_checker_pix) + single_checker_painter.setPen(QtCore.Qt.NoPen) + single_checker_painter.setBrush(QtGui.QColor(89, 89, 89)) + single_checker_painter.drawRect( + 0, 0, single_checker_pix.width(), single_checker_pix.height() + ) + single_checker_painter.setBrush(QtGui.QColor(188, 187, 187)) + single_checker_painter.drawRect( + 0, 0, checker_size, checker_size + ) + single_checker_painter.drawRect( + checker_size, checker_size, checker_size, checker_size + ) + single_checker_painter.end() + x_offset = (width % checker_size) * -0.5 + y_offset = (height % checker_size) * -0.5 + + empty_pix = QtGui.QPixmap(width, height) + empty_pix.fill(QtCore.Qt.transparent) + empty_painter = QtGui.QPainter() + empty_painter.begin(empty_pix) + empty_painter.drawTiledPixmap( + QtCore.QRectF(0, 0, width, height), + single_checker_pix, + QtCore.QPointF(x_offset, y_offset) + ) + empty_painter.end() + return empty_pix + def _cache_pix(self): rect = self.rect() rect_width = rect.width() From 2d4e13ae5655c6ef2bc8f7c69b6c465c8916037d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 18:44:32 +0100 Subject: [PATCH 47/55] implemented new pixmap button which is not pushbutton based --- openpype/style/style.css | 12 ++++ openpype/tools/utils/__init__.py | 2 + openpype/tools/utils/widgets.py | 94 ++++++++++++++++++++++++++++++-- 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 585adceb268..15abb6130bb 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -884,6 +884,18 @@ PublisherTabBtn[active="1"]:hover { background: {color:bg}; } +PixmapButton{ + border: 0px solid transparent; + border-radius: 0.2em; + background: {color:bg-buttons}; +} +PixmapButton:hover { + background: {color:bg-button-hover}; +} +PixmapButton:disabled { + background: {color:bg-buttons-disabled}; +} + #CreatorDetailedDescription { padding-left: 5px; padding-right: 5px; diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 019ea163913..31c8232f47c 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -7,6 +7,7 @@ ExpandBtn, PixmapLabel, IconButton, + PixmapButton, SeparatorWidget, ) from .views import DeselectableTreeView @@ -38,6 +39,7 @@ "ExpandBtn", "PixmapLabel", "IconButton", + "PixmapButton", "SeparatorWidget", "DeselectableTreeView", diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index ca651821246..13225081edb 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -252,6 +252,90 @@ def resizeEvent(self, event): super(PixmapLabel, self).resizeEvent(event) +class PixmapButtonPainter(QtWidgets.QWidget): + def __init__(self, pixmap, parent): + super(PixmapButtonPainter, self).__init__(parent) + + self._pixmap = pixmap + self._cached_pixmap = None + + def set_pixmap(self, pixmap): + self._pixmap = pixmap + self._cached_pixmap = None + + self.repaint() + + def _cache_pixmap(self): + size = self.size() + self._cached_pixmap = self._pixmap.scaled( + size.width(), + size.height(), + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + + def paintEvent(self, event): + painter = QtGui.QPainter() + painter.begin(self) + if self._pixmap is None: + painter.end() + return + + painter.setRenderHints( + painter.Antialiasing + | painter.SmoothPixmapTransform + | painter.HighQualityAntialiasing + ) + if self._cached_pixmap is None: + self._cache_pixmap() + + painter.drawPixmap(0, 0, self._cached_pixmap) + + painter.end() + + +class PixmapButton(ClickableFrame): + def __init__(self, pixmap=None, parent=None): + super(PixmapButton, self).__init__(parent) + + button_painter = PixmapButtonPainter(pixmap, self) + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(2, 2, 2, 2) + + self._button_painter = button_painter + + def setContentsMargins(self, *args): + layout = self.layout() + layout.setContentsMargins(*args) + self._update_painter_geo() + + def set_pixmap(self, pixmap): + self._button_painter.set_pixmap(pixmap) + + def sizeHint(self): + font_height = self.fontMetrics().height() + return QtCore.QSize(font_height, font_height) + + def resizeEvent(self, event): + super(PixmapButton, self).resizeEvent(event) + self._update_painter_geo() + + def showEvent(self, event): + super(PixmapButton, self).showEvent(event) + self._update_painter_geo() + + def _update_painter_geo(self): + size = self.size() + layout = self.layout() + left, top, right, bottom = layout.getContentsMargins() + self._button_painter.setGeometry( + left, + top, + size.width() - (left + right), + size.height() - (top + bottom) + ) + + class OptionalMenu(QtWidgets.QMenu): """A subclass of `QtWidgets.QMenu` to work with `OptionalAction` @@ -474,8 +558,10 @@ def __init__(self, size=2, orientation=QtCore.Qt.Horizontal, parent=None): self.set_size(size) def set_size(self, size): - if size == self._size: - return + if size != self._size: + self._set_size(size) + + def _set_size(self, size): if self._orientation == QtCore.Qt.Vertical: self.setMinimumWidth(size) self.setMaximumWidth(size) @@ -499,6 +585,4 @@ def set_orientation(self, orientation): self._orientation = orientation - size = self._size - self._size = None - self.set_size(size) + self._set_size(self._size) From 2ed10e4f0fb9baf5006f87ee056619bf7a9f997b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 18:45:56 +0100 Subject: [PATCH 48/55] separated painting into smaller methods --- .../publisher/widgets/thumbnail_widget.py | 166 +++++++++++------- 1 file changed, 103 insertions(+), 63 deletions(-) diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index 3f159d58121..d0ac83d6eb0 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -77,42 +77,105 @@ def paintEvent(self, event): painter.drawPixmap(0, 0, self._cached_pix) painter.end() - def _draw_empty_checker(self, width, height): + def _paint_checker(self, width, height): checker_size = int(float(width) / self.checker_boxes_count) if checker_size < 1: checker_size = 1 - single_checker_pix = QtGui.QPixmap(checker_size * 2, checker_size * 2) - single_checker_pix.fill(QtCore.Qt.transparent) - single_checker_painter = QtGui.QPainter() - single_checker_painter.begin(single_checker_pix) - single_checker_painter.setPen(QtCore.Qt.NoPen) - single_checker_painter.setBrush(QtGui.QColor(89, 89, 89)) - single_checker_painter.drawRect( - 0, 0, single_checker_pix.width(), single_checker_pix.height() + checker_pix = QtGui.QPixmap(checker_size * 2, checker_size * 2) + checker_pix.fill(QtCore.Qt.transparent) + checker_painter = QtGui.QPainter() + checker_painter.begin(checker_pix) + checker_painter.setPen(QtCore.Qt.NoPen) + checker_painter.setBrush(QtGui.QColor(89, 89, 89)) + checker_painter.drawRect( + 0, 0, checker_pix.width(), checker_pix.height() ) - single_checker_painter.setBrush(QtGui.QColor(188, 187, 187)) - single_checker_painter.drawRect( + checker_painter.setBrush(QtGui.QColor(188, 187, 187)) + checker_painter.drawRect( 0, 0, checker_size, checker_size ) - single_checker_painter.drawRect( + checker_painter.drawRect( checker_size, checker_size, checker_size, checker_size ) - single_checker_painter.end() - x_offset = (width % checker_size) * -0.5 - y_offset = (height % checker_size) * -0.5 - - empty_pix = QtGui.QPixmap(width, height) - empty_pix.fill(QtCore.Qt.transparent) - empty_painter = QtGui.QPainter() - empty_painter.begin(empty_pix) - empty_painter.drawTiledPixmap( - QtCore.QRectF(0, 0, width, height), - single_checker_pix, - QtCore.QPointF(x_offset, y_offset) + checker_painter.end() + return checker_pix + + def _paint_default_pix(self, pix_width, pix_height): + full_border_width = 2 * self.border_width + width = pix_width - full_border_width + height = pix_height - full_border_width + if width > 100: + width = int(width * 0.6) + height = int(height * 0.6) + + scaled_pix = self._default_pix.scaled( + width, + height, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + pos_x = int( + (pix_width - scaled_pix.width()) / 2 + ) + pos_y = int( + (pix_height - scaled_pix.height()) / 2 + ) + new_pix = QtGui.QPixmap(pix_width, pix_height) + new_pix.fill(QtCore.Qt.transparent) + pix_painter = QtGui.QPainter() + pix_painter.begin(new_pix) + pix_painter.setRenderHints( + pix_painter.Antialiasing + | pix_painter.SmoothPixmapTransform + | pix_painter.HighQualityAntialiasing ) - empty_painter.end() - return empty_pix + pix_painter.drawPixmap(pos_x, pos_y, scaled_pix) + pix_painter.end() + return new_pix + + def _draw_thumbnails(self, thumbnails, pix_width, pix_height): + full_border_width = 2 * self.border_width + + checker_pix = self._paint_checker(pix_width, pix_height) + + backgrounded_images = [] + for src_pix in thumbnails: + scaled_pix = src_pix.scaled( + pix_width - full_border_width, + pix_height - full_border_width, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + pos_x = int( + (pix_width - scaled_pix.width()) / 2 + ) + pos_y = int( + (pix_height - scaled_pix.height()) / 2 + ) + + new_pix = QtGui.QPixmap(pix_width, pix_height) + new_pix.fill(QtCore.Qt.transparent) + pix_painter = QtGui.QPainter() + pix_painter.begin(new_pix) + pix_painter.setRenderHints( + pix_painter.Antialiasing + | pix_painter.SmoothPixmapTransform + | pix_painter.HighQualityAntialiasing + ) + + tiled_rect = QtCore.QRectF( + pos_x, pos_y, scaled_pix.width(), scaled_pix.height() + ) + pix_painter.drawTiledPixmap( + tiled_rect, + checker_pix, + QtCore.QPointF(0.0, 0.0) + ) + pix_painter.drawPixmap(pos_x, pos_y, scaled_pix) + pix_painter.end() + backgrounded_images.append(new_pix) + return backgrounded_images def _cache_pix(self): rect = self.rect() @@ -135,51 +198,28 @@ def _cache_pix(self): pix_y_offset = (rect_height - expected_height) / 2 if self._current_pixes is None: - draw_dashes = True - pixes_to_draw = [self._default_pix] + used_default_pix = True + pixes_to_draw = None + pixes_len = 1 else: - draw_dashes = False + used_default_pix = False pixes_to_draw = self._current_pixes - - if len(pixes_to_draw) > self.max_thumbnails: - pixes_to_draw = pixes_to_draw[:-self.max_thumbnails] - pixes_len = len(pixes_to_draw) + if len(pixes_to_draw) > self.max_thumbnails: + pixes_to_draw = pixes_to_draw[:-self.max_thumbnails] + pixes_len = len(pixes_to_draw) width_offset, height_offset = self._get_pix_offset_size( expected_width, expected_height, pixes_len ) pix_width = expected_width - width_offset pix_height = expected_height - height_offset - full_border_width = 2 * self.border_width - - backgrounded_images = [] - for src_pix in pixes_to_draw: - scaled_pix = src_pix.scaled( - pix_width - full_border_width, - pix_height - full_border_width, - QtCore.Qt.KeepAspectRatio, - QtCore.Qt.SmoothTransformation - ) - pos_x = int( - (pix_width - scaled_pix.width()) / 2 - ) - pos_y = int( - (pix_height - scaled_pix.height()) / 2 - ) - - new_pix = QtGui.QPixmap(pix_width, pix_height) - new_pix.fill(QtCore.Qt.transparent) - pix_painter = QtGui.QPainter() - pix_painter.begin(new_pix) - pix_painter.setRenderHints( - pix_painter.Antialiasing - | pix_painter.SmoothPixmapTransform - | pix_painter.HighQualityAntialiasing + if used_default_pix: + thumbnail_images = [self._paint_default_pix(pix_width, pix_height)] + else: + thumbnail_images = self._draw_thumbnails( + pixes_to_draw, pix_width, pix_height ) - pix_painter.drawPixmap(pos_x, pos_y, scaled_pix) - pix_painter.end() - backgrounded_images.append(new_pix) if pixes_len == 1: width_offset_part = 0 @@ -207,13 +247,13 @@ def _cache_pix(self): final_painter.setPen(bg_pen) final_painter.drawRect(rect) - for idx, pix in enumerate(backgrounded_images): + for idx, pix in enumerate(thumbnail_images): x_offset = full_width_offset - (width_offset_part * idx) y_offset = (height_offset_part * idx) + pix_y_offset final_painter.drawPixmap(x_offset, y_offset, pix) # Draw drop enabled dashes - if draw_dashes: + if used_default_pix: pen = QtGui.QPen() pen.setWidth(1) pen.setBrush(QtCore.Qt.darkGray) From 01279cc6fd97017abb8b8adf3fa3aeadc23147e4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 18:46:08 +0100 Subject: [PATCH 49/55] change thumbnail bg color --- openpype/tools/publisher/widgets/thumbnail_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index d0ac83d6eb0..53e0891623c 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -35,7 +35,7 @@ def __init__(self, parent): super(ThumbnailPainterWidget, self).__init__(parent) border_color = get_objected_colors("bg-buttons").get_qcolor() - thumbnail_bg_color = get_objected_colors("border").get_qcolor() + thumbnail_bg_color = get_objected_colors("bg-view").get_qcolor() overlay_color = get_objected_colors("font").get_qcolor() default_image = get_image("thumbnail") From bbaf811df913818b062c64bae7a75a68d6944edd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 18:46:36 +0100 Subject: [PATCH 50/55] added image for clear thumbnail button and use pixmap button --- .../widgets/images/clear_thumbnail.png | Bin 0 -> 15872 bytes .../tools/publisher/widgets/thumbnail_widget.py | 9 ++++++++- openpype/tools/publisher/widgets/widgets.py | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 openpype/tools/publisher/widgets/images/clear_thumbnail.png diff --git a/openpype/tools/publisher/widgets/images/clear_thumbnail.png b/openpype/tools/publisher/widgets/images/clear_thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..406328cb51319160db0e722e673b7cac0449eee3 GIT binary patch literal 15872 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 literal 0 HcmV?d00001 diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index 53e0891623c..aef50356036 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -17,6 +17,7 @@ from openpype.tools.utils import ( paint_image_with_color, + PixmapButton, ) from openpype.tools.publisher.control import CardMessageTypes @@ -292,7 +293,13 @@ def __init__(self, controller, parent): buttons_widget = QtWidgets.QWidget(self) buttons_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - clear_button = QtWidgets.QPushButton("x", buttons_widget) + icon_color = get_objected_colors("bg-view-selection").get_qcolor() + icon_color.setAlpha(255) + clear_image = get_image("clear_thumbnail") + clear_pix = paint_image_with_color(clear_image, icon_color) + + clear_button = PixmapButton(clear_pix, buttons_widget) + clear_button.setObjectName("PixmapHoverButton") buttons_layout = QtWidgets.QHBoxLayout(buttons_widget) buttons_layout.setContentsMargins(3, 3, 3, 3) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 744c51ce077..f0c1a6df808 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -127,6 +127,7 @@ class PublishIconBtn(IconButton): - error : other error happened - success : publishing finished """ + def __init__(self, pixmap_path, *args, **kwargs): super(PublishIconBtn, self).__init__(*args, **kwargs) From 33178d15702500715c8400039742d6e5da3e4774 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 18:46:47 +0100 Subject: [PATCH 51/55] add different styles for button --- openpype/style/style.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index 15abb6130bb..0a703d11703 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -896,6 +896,14 @@ PixmapButton:disabled { background: {color:bg-buttons-disabled}; } +#PixmapHoverButton { + font-size: 11pt; + background: {color:bg-view}; +} +#PixmapHoverButton:hover { + background: {color:bg-button-hover}; +} + #CreatorDetailedDescription { padding-left: 5px; padding-right: 5px; From 78a725ca26a02e88e2361ccaff1287e7ce6bb8ea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 18:47:39 +0100 Subject: [PATCH 52/55] chnage the object name --- openpype/style/style.css | 4 ++-- openpype/tools/publisher/widgets/thumbnail_widget.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 0a703d11703..887c044daef 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -896,11 +896,11 @@ PixmapButton:disabled { background: {color:bg-buttons-disabled}; } -#PixmapHoverButton { +#ThumbnailPixmapHoverButton { font-size: 11pt; background: {color:bg-view}; } -#PixmapHoverButton:hover { +#ThumbnailPixmapHoverButton:hover { background: {color:bg-button-hover}; } diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index aef50356036..035ec4b04b8 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -299,7 +299,7 @@ def __init__(self, controller, parent): clear_pix = paint_image_with_color(clear_image, icon_color) clear_button = PixmapButton(clear_pix, buttons_widget) - clear_button.setObjectName("PixmapHoverButton") + clear_button.setObjectName("ThumbnailPixmapHoverButton") buttons_layout = QtWidgets.QHBoxLayout(buttons_widget) buttons_layout.setContentsMargins(3, 3, 3, 3) From bebb9031c18534d06a3e7236f950c16c6cc88c02 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 10:12:26 +0100 Subject: [PATCH 53/55] change type of 'IMAGE_EXTENSIONS' and 'VIDEO_EXTENSIONS' to set --- openpype/lib/transcoding.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index e736ba8ef01..0bfccd34437 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -42,7 +42,7 @@ # Regex to parse array attributes ARRAY_TYPE_REGEX = re.compile(r"^(int|float|string)\[\d+\]$") -IMAGE_EXTENSIONS = [ +IMAGE_EXTENSIONS = { ".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", ".cal", ".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", ".fits", ".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", ".icer", @@ -54,15 +54,15 @@ ".rgbe", ".logluv", ".tiff", ".sgi", ".tga", ".tiff", ".tiff/ep", ".tiff/it", ".ufo", ".ufp", ".wbmp", ".webp", ".xbm", ".xcf", ".xpm", ".xwd" -] +} -VIDEO_EXTENSIONS = [ +VIDEO_EXTENSIONS = { ".3g2", ".3gp", ".amv", ".asf", ".avi", ".drc", ".f4a", ".f4b", ".f4p", ".f4v", ".flv", ".gif", ".gifv", ".m2v", ".m4p", ".m4v", ".mkv", ".mng", ".mov", ".mp2", ".mp4", ".mpe", ".mpeg", ".mpg", ".mpv", ".mxf", ".nsv", ".ogg", ".ogv", ".qt", ".rm", ".rmvb", ".roq", ".svi", ".vob", ".webm", ".wmv", ".yuv" -] +} def get_transcode_temp_directory(): From 32b91ef39feeb4e68eb05b2b4fe060fcf8902884 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 11:32:12 +0100 Subject: [PATCH 54/55] Integrate thumbnails plugin is context plugin without family filters --- .../plugins/publish/integrate_thumbnail.py | 345 ++++++++++++------ 1 file changed, 225 insertions(+), 120 deletions(-) diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index d8a3a000416..f74c3d96096 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -13,174 +13,279 @@ import errno import shutil import copy +import collections import six import pyblish.api -from openpype.client import get_version_by_id +from openpype.client import get_versions from openpype.client.operations import OperationsSession, new_thumbnail_doc +InstanceFilterResult = collections.namedtuple( + "InstanceFilterResult", + ["instance", "thumbnail_path", "version_id"] +) -class IntegrateThumbnails(pyblish.api.InstancePlugin): + +class IntegrateThumbnails(pyblish.api.ContextPlugin): """Integrate Thumbnails for Openpype use in Loaders.""" label = "Integrate Thumbnails" order = pyblish.api.IntegratorOrder + 0.01 - families = ["review"] required_context_keys = [ "project", "asset", "task", "subset", "version" ] - def process(self, instance): - context_thumbnail_path = instance.context.get("thumbnailPath") + def process(self, context): + # Filter instances which can be used for integration + filtered_instance_items = self._prepare_instances(context) + if not filtered_instance_items: + self.log.info( + "All instances were filtered. Thumbnail integration skipped." + ) + return + # Initial validation of available templated and required keys env_key = "AVALON_THUMBNAIL_ROOT" thumbnail_root_format_key = "{thumbnail_root}" thumbnail_root = os.environ.get(env_key) or "" - published_repres = instance.data.get("published_representations") - if not published_repres: - self.log.debug( - "There are no published representations on the instance." - ) - return - - anatomy = instance.context.data["anatomy"] + anatomy = context.data["anatomy"] project_name = anatomy.project_name if "publish" not in anatomy.templates: - self.log.warning("Anatomy is missing the \"publish\" key!") + self.log.warning( + "Anatomy is missing the \"publish\" key. Skipping." + ) return if "thumbnail" not in anatomy.templates["publish"]: self.log.warning(( - "There is no \"thumbnail\" template set for the project \"{}\"" + "There is no \"thumbnail\" template set for the project" + " \"{}\". Skipping." ).format(project_name)) return thumbnail_template = anatomy.templates["publish"]["thumbnail"] + if not thumbnail_template: + self.log.info("Thumbnail template is not filled. Skipping.") + return + if ( not thumbnail_root and thumbnail_root_format_key in thumbnail_template ): - self.log.warning(( - "{} is not set. Skipping thumbnail integration." - ).format(env_key)) + self.log.warning(("{} is not set. Skipping.").format(env_key)) return - version_id = None - thumb_repre = None - thumb_repre_anatomy_data = None - for repre_info in published_repres.values(): - repre = repre_info["representation"] - if version_id is None: - version_id = repre["parent"] - - if repre["name"].lower() == "thumbnail": - thumb_repre = repre - thumb_repre_anatomy_data = repre_info["anatomy_data"] + # Collect verion ids from all filtered instance + version_ids = { + instance_items.version_id + for instance_items in filtered_instance_items + } + # Query versions + version_docs = get_versions( + project_name, + version_ids=version_ids, + hero=True, + fields=["_id", "type", "name"] + ) + # Store version by their id (converted to string) + version_docs_by_str_id = { + str(version_doc["_id"]): version_doc + for version_doc in version_docs + } + self._integrate_thumbnails( + filtered_instance_items, + version_docs_by_str_id, + anatomy, + thumbnail_root + ) + + def _prepare_instances(self, context): + context_thumbnail_path = context.get("thumbnailPath") + valid_context_thumbnail = False + if context_thumbnail_path and os.path.exists(context_thumbnail_path): + valid_context_thumbnail = True + + filtered_instances = [] + for instance in context: + instance_label = self._get_instance_label(instance) + # Skip instances without published representations + # - there is no place where to put the thumbnail + published_repres = instance.data.get("published_representations") + if not published_repres: + self.log.debug(( + "There are no published representations" + " on the instance {}." + ).format(instance_label)) + continue + + # Find thumbnail path on instance + thumbnail_path = self._get_instance_thumbnail_path( + published_repres) + if thumbnail_path: + self.log.debug(( + "Found thumbnail path for instance \"{}\"." + " Thumbnail path: {}" + ).format(instance_label, thumbnail_path)) + + elif valid_context_thumbnail: + # Use context thumbnail path if is available + thumbnail_path = context_thumbnail_path + self.log.debug(( + "Using context thumbnail path for instance \"{}\"." + " Thumbnail path: {}" + ).format(instance_label, thumbnail_path)) + + # Skip instance if thumbnail path is not available for it + if not thumbnail_path: + self.log.info(( + "Skipping thumbnail integration for instance \"{}\"." + " Instance and context" + " thumbnail paths are not available." + ).format(instance_label)) + continue + + version_id = str(self._get_version_id(published_repres)) + filtered_instances.append( + InstanceFilterResult(instance, thumbnail_path, version_id) + ) + return filtered_instances + + def _get_version_id(self, published_representations): + for repre_info in published_representations.values(): + return repre_info["representation"]["parent"] + + def _get_instance_thumbnail_path(self, published_representations): + thumb_repre_doc = None + for repre_info in published_representations.values(): + repre_doc = repre_info["representation"] + if repre_doc["name"].lower() == "thumbnail": + thumb_repre_doc = repre_doc break - # Use context thumbnail (if is available) - if not thumb_repre: + if thumb_repre_doc is None: self.log.debug( "There is not representation with name \"thumbnail\"" ) - src_full_path = context_thumbnail_path - else: - # Get full path to thumbnail file from representation - src_full_path = os.path.normpath(thumb_repre["data"]["path"]) - - if not os.path.exists(src_full_path): - self.log.warning("Thumbnail file was not found. Path: {}".format( - src_full_path - )) - return + return None - version = get_version_by_id(project_name, version_id) - if not version: - raise AssertionError( - "There does not exist version with id {}".format( - str(version_id) - ) + path = thumb_repre_doc["data"]["path"] + if not os.path.exists(path): + self.log.warning( + "Thumbnail file cannot be found. Path: {}".format(path) ) + return None + return os.path.normpath(path) + + def _integrate_thumbnails( + self, + filtered_instance_items, + version_docs_by_str_id, + anatomy, + thumbnail_root + ): + op_session = OperationsSession() + project_name = anatomy.project_name - filename, file_extension = os.path.splitext(src_full_path) - # Create id for mongo entity now to fill anatomy template - thumbnail_doc = new_thumbnail_doc() - thumbnail_id = thumbnail_doc["_id"] - - # Prepare anatomy template fill data - template_data = copy.deepcopy(thumb_repre_anatomy_data) - template_data.update({ - "_id": str(thumbnail_id), - "ext": file_extension[1:], - "thumbnail_root": thumbnail_root, - "thumbnail_type": "thumbnail" - }) - - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled["publish"]["thumbnail"] - - dst_full_path = os.path.normpath(str(template_filled)) - self.log.debug( - "Copying file .. {} -> {}".format(src_full_path, dst_full_path) - ) - dirname = os.path.dirname(dst_full_path) - try: - os.makedirs(dirname) - except OSError as e: - if e.errno != errno.EEXIST: - tp, value, tb = sys.exc_info() - six.reraise(tp, value, tb) - - shutil.copy(src_full_path, dst_full_path) - - # Clean template data from keys that are dynamic - for key in ("_id", "thumbnail_root"): - template_data.pop(key, None) - - repre_context = template_filled.used_values - for key in self.required_context_keys: - value = template_data.get(key) - if not value: + for instance_item in filtered_instance_items: + instance, thumbnail_path, version_id = instance_item + instance_label = self._get_instance_label(instance) + version_doc = version_docs_by_str_id.get(version_id) + if not version_doc: + self.log.warning(( + "Version entity for instance \"{}\" was not found." + ).format(instance_label)) continue - repre_context[key] = template_data[key] - - op_session = OperationsSession() - thumbnail_doc["data"] = { - "template": thumbnail_template, - "template_data": repre_context - } - op_session.create_entity( - project_name, thumbnail_doc["type"], thumbnail_doc - ) - # Create thumbnail entity - self.log.debug( - "Creating entity in database {}".format(str(thumbnail_doc)) - ) + filename, file_extension = os.path.splitext(thumbnail_path) + # Create id for mongo entity now to fill anatomy template + thumbnail_doc = new_thumbnail_doc() + thumbnail_id = thumbnail_doc["_id"] + + # Prepare anatomy template fill data + template_data = copy.deepcopy(instance.data["anatomyData"]) + template_data.update({ + "_id": str(thumbnail_id), + "ext": file_extension[1:], + "name": "thumbnail", + "thumbnail_root": thumbnail_root, + "thumbnail_type": "thumbnail" + }) + + anatomy_filled = anatomy.format(template_data) + thumbnail_template = anatomy.templates["publish"]["thumbnail"] + template_filled = anatomy_filled["publish"]["thumbnail"] + + dst_full_path = os.path.normpath(str(template_filled)) + self.log.debug("Copying file .. {} -> {}".format( + thumbnail_path, dst_full_path + )) + dirname = os.path.dirname(dst_full_path) + try: + os.makedirs(dirname) + except OSError as e: + if e.errno != errno.EEXIST: + tp, value, tb = sys.exc_info() + six.reraise(tp, value, tb) + + shutil.copy(thumbnail_path, dst_full_path) + + # Clean template data from keys that are dynamic + for key in ("_id", "thumbnail_root"): + template_data.pop(key, None) + + repre_context = template_filled.used_values + for key in self.required_context_keys: + value = template_data.get(key) + if not value: + continue + repre_context[key] = template_data[key] + + thumbnail_doc["data"] = { + "template": thumbnail_template, + "template_data": repre_context + } + op_session.create_entity( + project_name, thumbnail_doc["type"], thumbnail_doc + ) + # Create thumbnail entity + self.log.debug( + "Creating entity in database {}".format(str(thumbnail_doc)) + ) - # Set thumbnail id for version - op_session.update_entity( - project_name, - version["type"], - version["_id"], - {"data.thumbnail_id": thumbnail_id} - ) - self.log.debug("Setting thumbnail for version \"{}\" <{}>".format( - version["name"], str(version["_id"]) - )) + # Set thumbnail id for version + op_session.update_entity( + project_name, + version_doc["type"], + version_doc["_id"], + {"data.thumbnail_id": thumbnail_id} + ) + if version_doc["type"] == "hero_version": + version_name = "Hero" + else: + version_name = version_doc["name"] + self.log.debug("Setting thumbnail for version \"{}\" <{}>".format( + version_name, version_id + )) - asset_entity = instance.data["assetEntity"] - op_session.update_entity( - project_name, - asset_entity["type"], - asset_entity["_id"], - {"data.thumbnail_id": thumbnail_id} - ) - self.log.debug("Setting thumbnail for asset \"{}\" <{}>".format( - asset_entity["name"], str(version["_id"]) - )) + asset_entity = instance.data["assetEntity"] + op_session.update_entity( + project_name, + asset_entity["type"], + asset_entity["_id"], + {"data.thumbnail_id": thumbnail_id} + ) + self.log.debug("Setting thumbnail for asset \"{}\" <{}>".format( + asset_entity["name"], version_id + )) op_session.commit() + + def _get_instance_label(self, instance): + return ( + instance.data.get("label") + or instance.data.get("name") + or "N/A" + ) From 7df622df0cfd2af6092faf5918e75e97e1658473 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 18:29:21 +0100 Subject: [PATCH 55/55] fix thumbnail refreshing --- openpype/pipeline/create/context.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 71338f96e0e..4fd460ffea1 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1159,9 +1159,7 @@ def refresh_thumbnails(self): for instance_id, path in self.thumbnail_paths_by_instance_id.items(): instance_available = True if instance_id is not None: - instance_available = ( - instance_id not in self._instances_by_id - ) + instance_available = instance_id in self._instances_by_id if ( not instance_available @@ -1178,13 +1176,13 @@ def reset_preparation(self): # Give ability to store shared data for collection phase self._collection_shared_data = {} - self.refresh_thumbnails() def reset_finalization(self): """Cleanup of attributes after reset.""" # Stop access to collection shared data self._collection_shared_data = None + self.refresh_thumbnails() def reset_avalon_context(self): """Give ability to reset avalon context.