From 797d1ea59da616f84083e21b9b790c056f8f8c29 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 7 Sep 2022 13:47:02 +0200 Subject: [PATCH 001/160] copied attribute defs widgets into 'openpype.tools' --- openpype/tools/attribute_defs/__init__.py | 10 + openpype/tools/attribute_defs/files_widget.py | 968 ++++++++++++++++++ openpype/tools/attribute_defs/widgets.py | 490 +++++++++ 3 files changed, 1468 insertions(+) create mode 100644 openpype/tools/attribute_defs/__init__.py create mode 100644 openpype/tools/attribute_defs/files_widget.py create mode 100644 openpype/tools/attribute_defs/widgets.py diff --git a/openpype/tools/attribute_defs/__init__.py b/openpype/tools/attribute_defs/__init__.py new file mode 100644 index 0000000000..ce6b80109e --- /dev/null +++ b/openpype/tools/attribute_defs/__init__.py @@ -0,0 +1,10 @@ +from .widgets import ( + create_widget_for_attr_def, + AttributeDefinitionsWidget, +) + + +__all__ = ( + "create_widget_for_attr_def", + "AttributeDefinitionsWidget", +) diff --git a/openpype/tools/attribute_defs/files_widget.py b/openpype/tools/attribute_defs/files_widget.py new file mode 100644 index 0000000000..d29aa1b607 --- /dev/null +++ b/openpype/tools/attribute_defs/files_widget.py @@ -0,0 +1,968 @@ +import os +import collections +import uuid +import json + +from Qt import QtWidgets, QtCore, QtGui + +from openpype.lib import FileDefItem +from openpype.tools.utils import ( + paint_image_with_color, + ClickableLabel, +) +# TODO change imports +from openpype.tools.resources import get_image +from openpype.tools.utils import ( + IconButton, + PixmapLabel +) + +ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 +ITEM_LABEL_ROLE = QtCore.Qt.UserRole + 2 +ITEM_ICON_ROLE = QtCore.Qt.UserRole + 3 +FILENAMES_ROLE = QtCore.Qt.UserRole + 4 +DIRPATH_ROLE = QtCore.Qt.UserRole + 5 +IS_DIR_ROLE = QtCore.Qt.UserRole + 6 +IS_SEQUENCE_ROLE = QtCore.Qt.UserRole + 7 +EXT_ROLE = QtCore.Qt.UserRole + 8 + + +def convert_bytes_to_json(bytes_value): + if isinstance(bytes_value, QtCore.QByteArray): + # Raw data are already QByteArray and we don't have to load them + encoded_data = bytes_value + else: + encoded_data = QtCore.QByteArray.fromRawData(bytes_value) + stream = QtCore.QDataStream(encoded_data, QtCore.QIODevice.ReadOnly) + text = stream.readQString() + try: + return json.loads(text) + except Exception: + return None + + +def convert_data_to_bytes(data): + bytes_value = QtCore.QByteArray() + stream = QtCore.QDataStream(bytes_value, QtCore.QIODevice.WriteOnly) + stream.writeQString(json.dumps(data)) + return bytes_value + + +class SupportLabel(QtWidgets.QLabel): + pass + + +class DropEmpty(QtWidgets.QWidget): + _empty_extensions = "Any file" + + def __init__(self, single_item, allow_sequences, extensions_label, parent): + super(DropEmpty, self).__init__(parent) + + drop_label_widget = QtWidgets.QLabel("Drag & Drop files here", self) + + items_label_widget = SupportLabel(self) + items_label_widget.setWordWrap(True) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addSpacing(20) + layout.addWidget( + drop_label_widget, 0, alignment=QtCore.Qt.AlignCenter + ) + layout.addSpacing(30) + layout.addStretch(1) + layout.addWidget( + items_label_widget, 0, alignment=QtCore.Qt.AlignCenter + ) + layout.addSpacing(10) + + for widget in ( + drop_label_widget, + items_label_widget, + ): + widget.setAlignment(QtCore.Qt.AlignCenter) + widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + update_size_timer = QtCore.QTimer() + update_size_timer.setInterval(10) + update_size_timer.setSingleShot(True) + + update_size_timer.timeout.connect(self._on_update_size_timer) + + self._update_size_timer = update_size_timer + + if extensions_label and not extensions_label.startswith(" "): + extensions_label = " " + extensions_label + + self._single_item = single_item + self._extensions_label = extensions_label + self._allow_sequences = allow_sequences + self._allowed_extensions = set() + self._allow_folders = None + + self._drop_label_widget = drop_label_widget + self._items_label_widget = items_label_widget + + self.set_allow_folders(False) + + def set_extensions(self, extensions): + if extensions: + extensions = { + ext.replace(".", "") + for ext in extensions + } + if extensions == self._allowed_extensions: + return + self._allowed_extensions = extensions + + self._update_items_label() + + def set_allow_folders(self, allowed): + if self._allow_folders == allowed: + return + + self._allow_folders = allowed + self._update_items_label() + + def _update_items_label(self): + allowed_items = [] + if self._allow_folders: + allowed_items.append("folder") + + if self._allowed_extensions: + allowed_items.append("file") + if self._allow_sequences: + allowed_items.append("sequence") + + if not self._single_item: + allowed_items = [item + "s" for item in allowed_items] + + if not allowed_items: + self._items_label_widget.setText( + "It is not allowed to add anything here!" + ) + return + + items_label = "Multiple " + if self._single_item: + items_label = "Single " + + if len(allowed_items) == 1: + extensions_label = allowed_items[0] + elif len(allowed_items) == 2: + extensions_label = " or ".join(allowed_items) + else: + last_item = allowed_items.pop(-1) + new_last_item = " or ".join(last_item, allowed_items.pop(-1)) + allowed_items.append(new_last_item) + extensions_label = ", ".join(allowed_items) + + allowed_items_label = extensions_label + + items_label += allowed_items_label + label_tooltip = None + if self._allowed_extensions: + items_label += " of\n{}".format( + ", ".join(sorted(self._allowed_extensions)) + ) + + if self._extensions_label: + label_tooltip = items_label + items_label = self._extensions_label + + if self._items_label_widget.text() == items_label: + return + + self._items_label_widget.setToolTip(label_tooltip) + self._items_label_widget.setText(items_label) + self._update_size_timer.start() + + def resizeEvent(self, event): + super(DropEmpty, self).resizeEvent(event) + self._update_size_timer.start() + + def _on_update_size_timer(self): + """Recalculate height of label with extensions. + + Dynamic QLabel with word wrap does not handle properly it's sizeHint + calculations on show. This way it is recalculated. It is good practice + to trigger this method with small offset using '_update_size_timer'. + """ + + width = self._items_label_widget.width() + height = self._items_label_widget.heightForWidth(width) + self._items_label_widget.setMinimumHeight(height) + self._items_label_widget.updateGeometry() + + def paintEvent(self, event): + super(DropEmpty, self).paintEvent(event) + painter = QtGui.QPainter(self) + pen = QtGui.QPen() + pen.setWidth(1) + pen.setBrush(QtCore.Qt.darkGray) + pen.setStyle(QtCore.Qt.DashLine) + painter.setPen(pen) + content_margins = self.layout().contentsMargins() + + left_m = content_margins.left() + top_m = content_margins.top() + rect = QtCore.QRect( + left_m, + top_m, + ( + self.rect().width() + - (left_m + content_margins.right() + pen.width()) + ), + ( + self.rect().height() + - (top_m + content_margins.bottom() + pen.width()) + ) + ) + painter.drawRect(rect) + + +class FilesModel(QtGui.QStandardItemModel): + def __init__(self, single_item, allow_sequences): + super(FilesModel, self).__init__() + + self._id = str(uuid.uuid4()) + self._single_item = single_item + self._multivalue = False + self._allow_sequences = allow_sequences + + self._items_by_id = {} + self._file_items_by_id = {} + self._filenames_by_dirpath = collections.defaultdict(set) + self._items_by_dirpath = collections.defaultdict(list) + + @property + def id(self): + return self._id + + def set_multivalue(self, multivalue): + """Disable filtering.""" + + if self._multivalue == multivalue: + return + self._multivalue = multivalue + + def add_filepaths(self, items): + if not items: + return + + file_items = FileDefItem.from_value(items, self._allow_sequences) + if not file_items: + return + + if not self._multivalue and self._single_item: + file_items = [file_items[0]] + current_ids = list(self._file_items_by_id.keys()) + if current_ids: + self.remove_item_by_ids(current_ids) + + new_model_items = [] + for file_item in file_items: + item_id, model_item = self._create_item(file_item) + new_model_items.append(model_item) + self._file_items_by_id[item_id] = file_item + self._items_by_id[item_id] = model_item + + if new_model_items: + roow_item = self.invisibleRootItem() + roow_item.appendRows(new_model_items) + + def remove_item_by_ids(self, item_ids): + if not item_ids: + return + + items = [] + for item_id in set(item_ids): + if item_id not in self._items_by_id: + continue + item = self._items_by_id.pop(item_id) + self._file_items_by_id.pop(item_id) + items.append(item) + + if items: + for item in items: + self.removeRows(item.row(), 1) + + def get_file_item_by_id(self, item_id): + return self._file_items_by_id.get(item_id) + + def _create_item(self, file_item): + if file_item.is_dir: + icon_pixmap = paint_image_with_color( + get_image(filename="folder.png"), QtCore.Qt.white + ) + else: + icon_pixmap = paint_image_with_color( + get_image(filename="file.png"), QtCore.Qt.white + ) + + item = QtGui.QStandardItem() + item_id = str(uuid.uuid4()) + item.setData(item_id, ITEM_ID_ROLE) + item.setData(file_item.label or "< empty >", ITEM_LABEL_ROLE) + item.setData(file_item.filenames, FILENAMES_ROLE) + item.setData(file_item.directory, DIRPATH_ROLE) + item.setData(icon_pixmap, ITEM_ICON_ROLE) + item.setData(file_item.ext, EXT_ROLE) + item.setData(file_item.is_dir, IS_DIR_ROLE) + item.setData(file_item.is_sequence, IS_SEQUENCE_ROLE) + + return item_id, item + + def mimeData(self, indexes): + item_ids = [ + index.data(ITEM_ID_ROLE) + for index in indexes + ] + + item_ids_data = convert_data_to_bytes(item_ids) + mime_data = super(FilesModel, self).mimeData(indexes) + mime_data.setData("files_widget/internal_move", item_ids_data) + + file_items = [] + for item_id in item_ids: + file_item = self.get_file_item_by_id(item_id) + if file_item: + file_items.append(file_item.to_dict()) + + full_item_data = convert_data_to_bytes({ + "items": file_items, + "id": self._id + }) + mime_data.setData("files_widget/full_data", full_item_data) + return mime_data + + def dropMimeData(self, mime_data, action, row, col, index): + item_ids = convert_bytes_to_json( + mime_data.data("files_widget/internal_move") + ) + if item_ids is None: + return False + + # Find matching item after which will be items moved + # - store item before moved items are removed + root = self.invisibleRootItem() + if row >= 0: + src_item = self.item(row) + else: + src_item_id = index.data(ITEM_ID_ROLE) + src_item = self._items_by_id.get(src_item_id) + + # Take out items that should be moved + items = [] + for item_id in item_ids: + item = self._items_by_id.get(item_id) + if item: + self.takeRow(item.row()) + items.append(item) + + # Skip if there are not items that can be moved + if not items: + return False + + # Calculate row where items should be inserted + if src_item: + src_row = src_item.row() + else: + src_row = root.rowCount() + + root.insertRow(src_row, items) + return True + + +class FilesProxyModel(QtCore.QSortFilterProxyModel): + def __init__(self, *args, **kwargs): + super(FilesProxyModel, self).__init__(*args, **kwargs) + self._allow_folders = False + self._allowed_extensions = None + self._multivalue = False + + def set_multivalue(self, multivalue): + """Disable filtering.""" + + if self._multivalue == multivalue: + return + self._multivalue = multivalue + self.invalidateFilter() + + def set_allow_folders(self, allow=None): + if allow is None: + allow = not self._allow_folders + + if allow == self._allow_folders: + return + self._allow_folders = allow + self.invalidateFilter() + + def set_allowed_extensions(self, extensions=None): + if extensions is not None: + _extensions = set() + for ext in set(extensions): + if not ext.startswith("."): + ext = ".{}".format(ext) + _extensions.add(ext.lower()) + extensions = _extensions + + if self._allowed_extensions != extensions: + self._allowed_extensions = extensions + self.invalidateFilter() + + def are_valid_files(self, filepaths): + for filepath in filepaths: + if os.path.isfile(filepath): + _, ext = os.path.splitext(filepath) + if ext in self._allowed_extensions: + return True + + elif self._allow_folders: + return True + return False + + def filter_valid_files(self, filepaths): + filtered_paths = [] + for filepath in filepaths: + if os.path.isfile(filepath): + _, ext = os.path.splitext(filepath) + if ext in self._allowed_extensions: + filtered_paths.append(filepath) + + elif self._allow_folders: + filtered_paths.append(filepath) + return filtered_paths + + def filterAcceptsRow(self, row, parent_index): + # Skip filtering if multivalue is set + if self._multivalue: + return True + + model = self.sourceModel() + index = model.index(row, self.filterKeyColumn(), parent_index) + # First check if item is folder and if folders are enabled + if index.data(IS_DIR_ROLE): + if not self._allow_folders: + return False + return True + + # Check if there are any allowed extensions + if self._allowed_extensions is None: + return False + + if index.data(EXT_ROLE) not in self._allowed_extensions: + return False + return True + + def lessThan(self, left, right): + left_comparison = left.data(DIRPATH_ROLE) + right_comparison = right.data(DIRPATH_ROLE) + if left_comparison == right_comparison: + left_comparison = left.data(ITEM_LABEL_ROLE) + right_comparison = right.data(ITEM_LABEL_ROLE) + + if sorted((left_comparison, right_comparison))[0] == left_comparison: + return True + return False + + +class ItemWidget(QtWidgets.QWidget): + context_menu_requested = QtCore.Signal(QtCore.QPoint) + + def __init__( + self, item_id, label, pixmap_icon, is_sequence, multivalue, parent=None + ): + self._item_id = item_id + + super(ItemWidget, self).__init__(parent) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + icon_widget = PixmapLabel(pixmap_icon, self) + label_widget = QtWidgets.QLabel(label, self) + + label_size_hint = label_widget.sizeHint() + height = label_size_hint.height() + actions_menu_pix = paint_image_with_color( + get_image(filename="menu.png"), QtCore.Qt.white + ) + + split_btn = ClickableLabel(self) + split_btn.setFixedSize(height, height) + split_btn.setPixmap(actions_menu_pix) + if multivalue: + split_btn.setVisible(False) + else: + split_btn.setVisible(is_sequence) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(5, 5, 5, 5) + layout.addWidget(icon_widget, 0) + layout.addWidget(label_widget, 1) + layout.addWidget(split_btn, 0) + + split_btn.clicked.connect(self._on_actions_clicked) + + self._icon_widget = icon_widget + self._label_widget = label_widget + self._split_btn = split_btn + self._actions_menu_pix = actions_menu_pix + self._last_scaled_pix_height = None + + def _update_btn_size(self): + label_size_hint = self._label_widget.sizeHint() + height = label_size_hint.height() + if height == self._last_scaled_pix_height: + return + self._last_scaled_pix_height = height + self._split_btn.setFixedSize(height, height) + pix = self._actions_menu_pix.scaled( + height, height, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + self._split_btn.setPixmap(pix) + + def showEvent(self, event): + super(ItemWidget, self).showEvent(event) + self._update_btn_size() + + def resizeEvent(self, event): + super(ItemWidget, self).resizeEvent(event) + self._update_btn_size() + + def _on_actions_clicked(self): + pos = self._split_btn.rect().bottomLeft() + point = self._split_btn.mapToGlobal(pos) + self.context_menu_requested.emit(point) + + +class InViewButton(IconButton): + pass + + +class FilesView(QtWidgets.QListView): + """View showing instances and their groups.""" + + remove_requested = QtCore.Signal() + context_menu_requested = QtCore.Signal(QtCore.QPoint) + + def __init__(self, *args, **kwargs): + super(FilesView, self).__init__(*args, **kwargs) + + self.setEditTriggers(QtWidgets.QListView.NoEditTriggers) + self.setSelectionMode( + QtWidgets.QAbstractItemView.ExtendedSelection + ) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.setAcceptDrops(True) + self.setDragEnabled(True) + self.setDragDropMode(self.InternalMove) + + remove_btn = InViewButton(self) + pix_enabled = paint_image_with_color( + get_image(filename="delete.png"), QtCore.Qt.white + ) + pix_disabled = paint_image_with_color( + get_image(filename="delete.png"), QtCore.Qt.gray + ) + icon = QtGui.QIcon(pix_enabled) + icon.addPixmap(pix_disabled, icon.Disabled, icon.Off) + remove_btn.setIcon(icon) + remove_btn.setEnabled(False) + + remove_btn.clicked.connect(self._on_remove_clicked) + self.customContextMenuRequested.connect(self._on_context_menu_request) + + self._remove_btn = remove_btn + + def setSelectionModel(self, *args, **kwargs): + """Catch selection model set to register signal callback. + + Selection model is not available during initialization. + """ + + super(FilesView, self).setSelectionModel(*args, **kwargs) + selection_model = self.selectionModel() + selection_model.selectionChanged.connect(self._on_selection_change) + + def set_multivalue(self, multivalue): + """Disable remove button on multivalue.""" + + self._remove_btn.setVisible(not multivalue) + + def has_selected_item_ids(self): + """Is any index selected.""" + for index in self.selectionModel().selectedIndexes(): + instance_id = index.data(ITEM_ID_ROLE) + if instance_id is not None: + return True + return False + + def get_selected_item_ids(self): + """Ids of selected instances.""" + + selected_item_ids = set() + for index in self.selectionModel().selectedIndexes(): + instance_id = index.data(ITEM_ID_ROLE) + if instance_id is not None: + selected_item_ids.add(instance_id) + return selected_item_ids + + def has_selected_sequence(self): + for index in self.selectionModel().selectedIndexes(): + if index.data(IS_SEQUENCE_ROLE): + return True + return False + + def event(self, event): + if event.type() == QtCore.QEvent.KeyPress: + if ( + event.key() == QtCore.Qt.Key_Delete + and self.has_selected_item_ids() + ): + self.remove_requested.emit() + return True + + return super(FilesView, self).event(event) + + def _on_context_menu_request(self, pos): + index = self.indexAt(pos) + if index.isValid(): + point = self.viewport().mapToGlobal(pos) + self.context_menu_requested.emit(point) + + def _on_selection_change(self): + self._remove_btn.setEnabled(self.has_selected_item_ids()) + + def _on_remove_clicked(self): + self.remove_requested.emit() + + def _update_remove_btn(self): + """Position remove button to bottom right.""" + + viewport = self.viewport() + height = viewport.height() + pos_x = viewport.width() - self._remove_btn.width() - 5 + pos_y = height - self._remove_btn.height() - 5 + self._remove_btn.move(max(0, pos_x), max(0, pos_y)) + + def resizeEvent(self, event): + super(FilesView, self).resizeEvent(event) + self._update_remove_btn() + + def showEvent(self, event): + super(FilesView, self).showEvent(event) + self._update_remove_btn() + + +class FilesWidget(QtWidgets.QFrame): + value_changed = QtCore.Signal() + + def __init__(self, single_item, allow_sequences, extensions_label, parent): + super(FilesWidget, self).__init__(parent) + self.setAcceptDrops(True) + + empty_widget = DropEmpty( + single_item, allow_sequences, extensions_label, self + ) + + files_model = FilesModel(single_item, allow_sequences) + files_proxy_model = FilesProxyModel() + files_proxy_model.setSourceModel(files_model) + files_view = FilesView(self) + files_view.setModel(files_proxy_model) + files_view.setVisible(False) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(empty_widget, 1) + layout.addWidget(files_view, 1) + + files_proxy_model.rowsInserted.connect(self._on_rows_inserted) + files_proxy_model.rowsRemoved.connect(self._on_rows_removed) + files_view.remove_requested.connect(self._on_remove_requested) + files_view.context_menu_requested.connect( + self._on_context_menu_requested + ) + + self._in_set_value = False + self._single_item = single_item + self._multivalue = False + + self._empty_widget = empty_widget + self._files_model = files_model + self._files_proxy_model = files_proxy_model + self._files_view = files_view + + self._widgets_by_id = {} + + def _set_multivalue(self, multivalue): + if self._multivalue == multivalue: + return + self._multivalue = multivalue + self._files_view.set_multivalue(multivalue) + self._files_model.set_multivalue(multivalue) + self._files_proxy_model.set_multivalue(multivalue) + + def set_value(self, value, multivalue): + self._in_set_value = True + + widget_ids = set(self._widgets_by_id.keys()) + self._remove_item_by_ids(widget_ids) + + self._set_multivalue(multivalue) + + self._add_filepaths(value) + + self._in_set_value = False + + def current_value(self): + model = self._files_proxy_model + item_ids = set() + for row in range(model.rowCount()): + index = model.index(row, 0) + item_ids.add(index.data(ITEM_ID_ROLE)) + + file_items = [] + for item_id in item_ids: + file_item = self._files_model.get_file_item_by_id(item_id) + if file_item is not None: + file_items.append(file_item.to_dict()) + + if not self._single_item: + return file_items + if file_items: + return file_items[0] + + empty_item = FileDefItem.create_empty_item() + return empty_item.to_dict() + + def set_filters(self, folders_allowed, exts_filter): + self._files_proxy_model.set_allow_folders(folders_allowed) + self._files_proxy_model.set_allowed_extensions(exts_filter) + self._empty_widget.set_extensions(exts_filter) + self._empty_widget.set_allow_folders(folders_allowed) + + def _on_rows_inserted(self, parent_index, start_row, end_row): + for row in range(start_row, end_row + 1): + index = self._files_proxy_model.index(row, 0, parent_index) + item_id = index.data(ITEM_ID_ROLE) + if item_id in self._widgets_by_id: + continue + label = index.data(ITEM_LABEL_ROLE) + pixmap_icon = index.data(ITEM_ICON_ROLE) + is_sequence = index.data(IS_SEQUENCE_ROLE) + + widget = ItemWidget( + item_id, + label, + pixmap_icon, + is_sequence, + self._multivalue + ) + widget.context_menu_requested.connect( + self._on_context_menu_requested + ) + self._files_view.setIndexWidget(index, widget) + self._files_proxy_model.setData( + index, widget.sizeHint(), QtCore.Qt.SizeHintRole + ) + self._widgets_by_id[item_id] = widget + + if not self._in_set_value: + self.value_changed.emit() + + def _on_rows_removed(self, parent_index, start_row, end_row): + available_item_ids = set() + for row in range(self._files_proxy_model.rowCount()): + index = self._files_proxy_model.index(row, 0) + item_id = index.data(ITEM_ID_ROLE) + available_item_ids.add(index.data(ITEM_ID_ROLE)) + + widget_ids = set(self._widgets_by_id.keys()) + for item_id in available_item_ids: + if item_id in widget_ids: + widget_ids.remove(item_id) + + for item_id in widget_ids: + widget = self._widgets_by_id.pop(item_id) + widget.setVisible(False) + widget.deleteLater() + + if not self._in_set_value: + self.value_changed.emit() + + def _on_split_request(self): + if self._multivalue: + return + + item_ids = self._files_view.get_selected_item_ids() + if not item_ids: + return + + for item_id in item_ids: + file_item = self._files_model.get_file_item_by_id(item_id) + if not file_item: + return + + new_items = file_item.split_sequence() + self._add_filepaths(new_items) + self._remove_item_by_ids(item_ids) + + def _on_remove_requested(self): + if self._multivalue: + return + + items_to_delete = self._files_view.get_selected_item_ids() + if items_to_delete: + self._remove_item_by_ids(items_to_delete) + + def _on_context_menu_requested(self, pos): + if self._multivalue: + return + + menu = QtWidgets.QMenu(self._files_view) + + if self._files_view.has_selected_sequence(): + split_action = QtWidgets.QAction("Split sequence", menu) + split_action.triggered.connect(self._on_split_request) + menu.addAction(split_action) + + remove_action = QtWidgets.QAction("Remove", menu) + remove_action.triggered.connect(self._on_remove_requested) + menu.addAction(remove_action) + + menu.popup(pos) + + def sizeHint(self): + # Get size hints of widget and visible widgets + result = super(FilesWidget, self).sizeHint() + if not self._files_view.isVisible(): + not_visible_hint = self._files_view.sizeHint() + else: + not_visible_hint = self._empty_widget.sizeHint() + + # Get margins of this widget + margins = self.layout().contentsMargins() + + # Change size hint based on result of maximum size hint of widgets + result.setWidth(max( + result.width(), + not_visible_hint.width() + margins.left() + margins.right() + )) + result.setHeight(max( + result.height(), + not_visible_hint.height() + margins.top() + margins.bottom() + )) + + return result + + def dragEnterEvent(self, event): + if self._multivalue: + return + + mime_data = event.mimeData() + if mime_data.hasUrls(): + filepaths = [] + for url in mime_data.urls(): + filepath = url.toLocalFile() + if os.path.exists(filepath): + filepaths.append(filepath) + + if self._files_proxy_model.are_valid_files(filepaths): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + + full_data_value = mime_data.data("files_widget/full_data") + if self._handle_full_data_drag(full_data_value): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + + def dragLeaveEvent(self, event): + event.accept() + + def dropEvent(self, event): + if self._multivalue: + return + + mime_data = event.mimeData() + if mime_data.hasUrls(): + event.accept() + # event.setDropAction(QtCore.Qt.CopyAction) + filepaths = [] + for url in mime_data.urls(): + filepath = url.toLocalFile() + if os.path.exists(filepath): + filepaths.append(filepath) + + # Filter filepaths before passing it to model + filepaths = self._files_proxy_model.filter_valid_files(filepaths) + if filepaths: + self._add_filepaths(filepaths) + + if self._handle_full_data_drop( + mime_data.data("files_widget/full_data") + ): + event.setDropAction(QtCore.Qt.CopyAction) + event.accept() + + super(FilesWidget, self).dropEvent(event) + + def _handle_full_data_drag(self, value): + if value is None: + return False + + full_data = convert_bytes_to_json(value) + if full_data is None: + return False + + if full_data["id"] == self._files_model.id: + return False + return True + + def _handle_full_data_drop(self, value): + if value is None: + return False + + full_data = convert_bytes_to_json(value) + if full_data is None: + return False + + if full_data["id"] == self._files_model.id: + return False + + for item in full_data["items"]: + filepaths = [ + os.path.join(item["directory"], filename) + for filename in item["filenames"] + ] + filepaths = self._files_proxy_model.filter_valid_files(filepaths) + if filepaths: + self._add_filepaths(filepaths) + + if self._copy_modifiers_enabled(): + return False + return True + + def _copy_modifiers_enabled(self): + if ( + QtWidgets.QApplication.keyboardModifiers() + & QtCore.Qt.ControlModifier + ): + return True + return False + + def _add_filepaths(self, filepaths): + self._files_model.add_filepaths(filepaths) + self._update_visibility() + + def _remove_item_by_ids(self, item_ids): + self._files_model.remove_item_by_ids(item_ids) + self._update_visibility() + + def _update_visibility(self): + files_exists = self._files_proxy_model.rowCount() > 0 + self._files_view.setVisible(files_exists) + self._empty_widget.setVisible(not files_exists) diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py new file mode 100644 index 0000000000..60ae952553 --- /dev/null +++ b/openpype/tools/attribute_defs/widgets.py @@ -0,0 +1,490 @@ +import uuid +import copy + +from Qt import QtWidgets, QtCore + +from openpype.lib.attribute_definitions import ( + AbtractAttrDef, + UnknownDef, + NumberDef, + TextDef, + EnumDef, + BoolDef, + FileDef, + UIDef, + UISeparatorDef, + UILabelDef +) +from openpype.tools.utils import CustomTextComboBox +from openpype.widgets.nice_checkbox import NiceCheckbox + +from .files_widget import FilesWidget + + +def create_widget_for_attr_def(attr_def, parent=None): + if not isinstance(attr_def, AbtractAttrDef): + raise TypeError("Unexpected type \"{}\" expected \"{}\"".format( + str(type(attr_def)), AbtractAttrDef + )) + + if isinstance(attr_def, NumberDef): + return NumberAttrWidget(attr_def, parent) + + if isinstance(attr_def, TextDef): + return TextAttrWidget(attr_def, parent) + + if isinstance(attr_def, EnumDef): + return EnumAttrWidget(attr_def, parent) + + if isinstance(attr_def, BoolDef): + return BoolAttrWidget(attr_def, parent) + + if isinstance(attr_def, UnknownDef): + return UnknownAttrWidget(attr_def, parent) + + if isinstance(attr_def, FileDef): + return FileAttrWidget(attr_def, parent) + + if isinstance(attr_def, UISeparatorDef): + return SeparatorAttrWidget(attr_def, parent) + + if isinstance(attr_def, UILabelDef): + return LabelAttrWidget(attr_def, parent) + + raise ValueError("Unknown attribute definition \"{}\"".format( + str(type(attr_def)) + )) + + +class AttributeDefinitionsWidget(QtWidgets.QWidget): + """Create widgets for attribute definitions in grid layout. + + Widget creates input widgets for passed attribute definitions. + + Widget can't handle multiselection values. + """ + + def __init__(self, attr_defs=None, parent=None): + super(AttributeDefinitionsWidget, self).__init__(parent) + + self._widgets = [] + self._current_keys = set() + + self.set_attr_defs(attr_defs) + + def clear_attr_defs(self): + """Remove all existing widgets and reset layout if needed.""" + self._widgets = [] + self._current_keys = set() + + layout = self.layout() + if layout is not None: + if layout.count() == 0: + return + + while layout.count(): + item = layout.takeAt(0) + widget = item.widget() + if widget: + widget.setVisible(False) + widget.deleteLater() + + layout.deleteLater() + + new_layout = QtWidgets.QGridLayout() + new_layout.setColumnStretch(0, 0) + new_layout.setColumnStretch(1, 1) + self.setLayout(new_layout) + + def set_attr_defs(self, attr_defs): + """Replace current attribute definitions with passed.""" + self.clear_attr_defs() + if attr_defs: + self.add_attr_defs(attr_defs) + + def add_attr_defs(self, attr_defs): + """Add attribute definitions to current.""" + layout = self.layout() + + row = 0 + for attr_def in attr_defs: + if attr_def.key in self._current_keys: + raise KeyError("Duplicated key \"{}\"".format(attr_def.key)) + + self._current_keys.add(attr_def.key) + widget = create_widget_for_attr_def(attr_def, self) + + expand_cols = 2 + if attr_def.is_value_def and attr_def.is_label_horizontal: + expand_cols = 1 + + col_num = 2 - expand_cols + + if attr_def.label: + label_widget = QtWidgets.QLabel(attr_def.label, self) + layout.addWidget( + label_widget, row, 0, 1, expand_cols + ) + if not attr_def.is_label_horizontal: + row += 1 + + layout.addWidget( + widget, row, col_num, 1, expand_cols + ) + self._widgets.append(widget) + row += 1 + + def set_value(self, value): + new_value = copy.deepcopy(value) + unused_keys = set(new_value.keys()) + for widget in self._widgets: + attr_def = widget.attr_def + if attr_def.key not in new_value: + continue + unused_keys.remove(attr_def.key) + + widget_value = new_value[attr_def.key] + if widget_value is None: + widget_value = copy.deepcopy(attr_def.default) + widget.set_value(widget_value) + + def current_value(self): + output = {} + for widget in self._widgets: + attr_def = widget.attr_def + if not isinstance(attr_def, UIDef): + output[attr_def.key] = widget.current_value() + + return output + + +class _BaseAttrDefWidget(QtWidgets.QWidget): + # Type 'object' may not work with older PySide versions + value_changed = QtCore.Signal(object, uuid.UUID) + + def __init__(self, attr_def, parent): + super(_BaseAttrDefWidget, self).__init__(parent) + + self.attr_def = attr_def + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + + self.main_layout = main_layout + + self._ui_init() + + def _ui_init(self): + raise NotImplementedError( + "Method '_ui_init' is not implemented. {}".format( + self.__class__.__name__ + ) + ) + + def current_value(self): + raise NotImplementedError( + "Method 'current_value' is not implemented. {}".format( + self.__class__.__name__ + ) + ) + + def set_value(self, value, multivalue=False): + raise NotImplementedError( + "Method 'set_value' is not implemented. {}".format( + self.__class__.__name__ + ) + ) + + +class SeparatorAttrWidget(_BaseAttrDefWidget): + def _ui_init(self): + input_widget = QtWidgets.QWidget(self) + input_widget.setObjectName("Separator") + input_widget.setMinimumHeight(2) + input_widget.setMaximumHeight(2) + + self._input_widget = input_widget + + self.main_layout.addWidget(input_widget, 0) + + +class LabelAttrWidget(_BaseAttrDefWidget): + def _ui_init(self): + input_widget = QtWidgets.QLabel(self) + label = self.attr_def.label + if label: + input_widget.setText(str(label)) + + self._input_widget = input_widget + + self.main_layout.addWidget(input_widget, 0) + + +class NumberAttrWidget(_BaseAttrDefWidget): + def _ui_init(self): + decimals = self.attr_def.decimals + if decimals > 0: + input_widget = QtWidgets.QDoubleSpinBox(self) + input_widget.setDecimals(decimals) + else: + input_widget = QtWidgets.QSpinBox(self) + + if self.attr_def.tooltip: + input_widget.setToolTip(self.attr_def.tooltip) + + input_widget.setMinimum(self.attr_def.minimum) + input_widget.setMaximum(self.attr_def.maximum) + input_widget.setValue(self.attr_def.default) + + input_widget.setButtonSymbols( + QtWidgets.QAbstractSpinBox.ButtonSymbols.NoButtons + ) + + input_widget.valueChanged.connect(self._on_value_change) + + self._input_widget = input_widget + + self.main_layout.addWidget(input_widget, 0) + + def _on_value_change(self, new_value): + self.value_changed.emit(new_value, self.attr_def.id) + + def current_value(self): + return self._input_widget.value() + + def set_value(self, value, multivalue=False): + if multivalue: + set_value = set(value) + if None in set_value: + set_value.remove(None) + set_value.add(self.attr_def.default) + + if len(set_value) > 1: + self._input_widget.setSpecialValueText("Multiselection") + return + value = tuple(set_value)[0] + + if self.current_value != value: + self._input_widget.setValue(value) + + +class TextAttrWidget(_BaseAttrDefWidget): + def _ui_init(self): + # TODO Solve how to handle regex + # self.attr_def.regex + + self.multiline = self.attr_def.multiline + if self.multiline: + input_widget = QtWidgets.QPlainTextEdit(self) + else: + input_widget = QtWidgets.QLineEdit(self) + + if ( + self.attr_def.placeholder + and hasattr(input_widget, "setPlaceholderText") + ): + input_widget.setPlaceholderText(self.attr_def.placeholder) + + if self.attr_def.tooltip: + input_widget.setToolTip(self.attr_def.tooltip) + + if self.attr_def.default: + if self.multiline: + input_widget.setPlainText(self.attr_def.default) + else: + input_widget.setText(self.attr_def.default) + + input_widget.textChanged.connect(self._on_value_change) + + self._input_widget = input_widget + + self.main_layout.addWidget(input_widget, 0) + + def _on_value_change(self): + if self.multiline: + new_value = self._input_widget.toPlainText() + else: + new_value = self._input_widget.text() + self.value_changed.emit(new_value, self.attr_def.id) + + def current_value(self): + if self.multiline: + return self._input_widget.toPlainText() + return self._input_widget.text() + + def set_value(self, value, multivalue=False): + if multivalue: + set_value = set(value) + if None in set_value: + set_value.remove(None) + set_value.add(self.attr_def.default) + + if len(set_value) == 1: + value = tuple(set_value)[0] + else: + value = "< Multiselection >" + + if value != self.current_value(): + if self.multiline: + self._input_widget.setPlainText(value) + else: + self._input_widget.setText(value) + + +class BoolAttrWidget(_BaseAttrDefWidget): + def _ui_init(self): + input_widget = NiceCheckbox(parent=self) + input_widget.setChecked(self.attr_def.default) + + if self.attr_def.tooltip: + input_widget.setToolTip(self.attr_def.tooltip) + + input_widget.stateChanged.connect(self._on_value_change) + + self._input_widget = input_widget + + self.main_layout.addWidget(input_widget, 0) + self.main_layout.addStretch(1) + + def _on_value_change(self): + new_value = self._input_widget.isChecked() + self.value_changed.emit(new_value, self.attr_def.id) + + def current_value(self): + return self._input_widget.isChecked() + + def set_value(self, value, multivalue=False): + if multivalue: + set_value = set(value) + if None in set_value: + set_value.remove(None) + set_value.add(self.attr_def.default) + + if len(set_value) > 1: + self._input_widget.setCheckState(QtCore.Qt.PartiallyChecked) + return + value = tuple(set_value)[0] + + if value != self.current_value(): + self._input_widget.setChecked(value) + + +class EnumAttrWidget(_BaseAttrDefWidget): + def __init__(self, *args, **kwargs): + self._multivalue = False + super(EnumAttrWidget, self).__init__(*args, **kwargs) + + def _ui_init(self): + input_widget = CustomTextComboBox(self) + combo_delegate = QtWidgets.QStyledItemDelegate(input_widget) + input_widget.setItemDelegate(combo_delegate) + + if self.attr_def.tooltip: + input_widget.setToolTip(self.attr_def.tooltip) + + items = self.attr_def.items + for key, label in items.items(): + input_widget.addItem(label, key) + + idx = input_widget.findData(self.attr_def.default) + if idx >= 0: + input_widget.setCurrentIndex(idx) + + input_widget.currentIndexChanged.connect(self._on_value_change) + + self._combo_delegate = combo_delegate + self._input_widget = input_widget + + self.main_layout.addWidget(input_widget, 0) + + def _on_value_change(self): + new_value = self.current_value() + if self._multivalue: + self._multivalue = False + self._input_widget.set_custom_text(None) + self.value_changed.emit(new_value, self.attr_def.id) + + def current_value(self): + idx = self._input_widget.currentIndex() + return self._input_widget.itemData(idx) + + def set_value(self, value, multivalue=False): + if multivalue: + set_value = set(value) + if len(set_value) == 1: + multivalue = False + value = tuple(set_value)[0] + + if not multivalue: + idx = self._input_widget.findData(value) + cur_idx = self._input_widget.currentIndex() + if idx != cur_idx and idx >= 0: + self._input_widget.setCurrentIndex(idx) + + custom_text = None + if multivalue: + custom_text = "< Multiselection >" + self._input_widget.set_custom_text(custom_text) + self._multivalue = multivalue + + +class UnknownAttrWidget(_BaseAttrDefWidget): + def _ui_init(self): + input_widget = QtWidgets.QLabel(self) + self._value = self.attr_def.default + input_widget.setText(str(self._value)) + + self._input_widget = input_widget + + self.main_layout.addWidget(input_widget, 0) + + def current_value(self): + raise ValueError( + "{} can't hold real value.".format(self.__class__.__name__) + ) + + def set_value(self, value, multivalue=False): + if multivalue: + set_value = set(value) + if len(set_value) == 1: + value = tuple(set_value)[0] + else: + value = "< Multiselection >" + + str_value = str(value) + if str_value != self._value: + self._value = str_value + self._input_widget.setText(str_value) + + +class FileAttrWidget(_BaseAttrDefWidget): + def _ui_init(self): + input_widget = FilesWidget( + self.attr_def.single_item, + self.attr_def.allow_sequences, + self.attr_def.extensions_label, + self + ) + + if self.attr_def.tooltip: + input_widget.setToolTip(self.attr_def.tooltip) + + input_widget.set_filters( + self.attr_def.folders, self.attr_def.extensions + ) + + input_widget.value_changed.connect(self._on_value_change) + + self._input_widget = input_widget + + self.main_layout.addWidget(input_widget, 0) + + def _on_value_change(self): + new_value = self.current_value() + self.value_changed.emit(new_value, self.attr_def.id) + + def current_value(self): + return self._input_widget.current_value() + + def set_value(self, value, multivalue=False): + self._input_widget.set_value(value, multivalue) From 790842080f7c5e1c2cbf1cb3b5ebafec85ddd961 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 7 Sep 2022 13:47:06 +0200 Subject: [PATCH 002/160] changed imports in publisher --- openpype/tools/publisher/widgets/precreate_widget.py | 2 +- openpype/tools/publisher/widgets/widgets.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/widgets/precreate_widget.py b/openpype/tools/publisher/widgets/precreate_widget.py index eaadfe890b..d41942dd60 100644 --- a/openpype/tools/publisher/widgets/precreate_widget.py +++ b/openpype/tools/publisher/widgets/precreate_widget.py @@ -1,6 +1,6 @@ from Qt import QtWidgets, QtCore -from openpype.widgets.attribute_defs import create_widget_for_attr_def +from openpype.tools.attribute_defs import create_widget_for_attr_def class PreCreateWidget(QtWidgets.QWidget): diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index aa7e3be687..5e52d7f50e 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -6,7 +6,7 @@ import collections from Qt import QtWidgets, QtCore, QtGui import qtawesome -from openpype.widgets.attribute_defs import create_widget_for_attr_def +from openpype.tools.attribute_defs import create_widget_for_attr_def from openpype.tools import resources from openpype.tools.flickcharm import FlickCharm from openpype.tools.utils import ( @@ -1219,7 +1219,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): Attributes are defined on creator so are dynamic. Their look and type is based on attribute definitions that are defined in `~/openpype/pipeline/lib/attribute_definitions.py` and their widget - representation in `~/openpype/widgets/attribute_defs/*`. + representation in `~/openpype/tools/attribute_defs/*`. Widgets are disabled if context of instance is not valid. @@ -1341,7 +1341,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): Look and type of attributes is based on attribute definitions that are defined in `~/openpype/pipeline/lib/attribute_definitions.py` and their - widget representation in `~/openpype/widgets/attribute_defs/*`. + widget representation in `~/openpype/tools/attribute_defs/*`. Widgets are disabled if context of instance is not valid. From c78b7d1d0cd51e1e2c411051948b8b70d02eb621 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 7 Sep 2022 13:47:24 +0200 Subject: [PATCH 003/160] added dialog for attribute defs --- openpype/tools/attribute_defs/__init__.py | 6 +++++ openpype/tools/attribute_defs/dialog.py | 32 +++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 openpype/tools/attribute_defs/dialog.py diff --git a/openpype/tools/attribute_defs/__init__.py b/openpype/tools/attribute_defs/__init__.py index ce6b80109e..f991fdec3d 100644 --- a/openpype/tools/attribute_defs/__init__.py +++ b/openpype/tools/attribute_defs/__init__.py @@ -3,8 +3,14 @@ from .widgets import ( AttributeDefinitionsWidget, ) +from .dialog import ( + AttributeDefinitionsDialog, +) + __all__ = ( "create_widget_for_attr_def", "AttributeDefinitionsWidget", + + "AttributeDefinitionsDialog", ) diff --git a/openpype/tools/attribute_defs/dialog.py b/openpype/tools/attribute_defs/dialog.py new file mode 100644 index 0000000000..e6c11516c8 --- /dev/null +++ b/openpype/tools/attribute_defs/dialog.py @@ -0,0 +1,32 @@ +from Qt import QtWidgets + +from .widgets import AttributeDefinitionsWidget + + +class AttributeDefinitionsDialog(QtWidgets.QDialog): + def __init__(self, attr_defs, parent=None): + super(AttributeDefinitionsDialog, self).__init__(parent) + + attrs_widget = AttributeDefinitionsWidget(attr_defs, self) + + btns_widget = QtWidgets.QWidget(self) + ok_btn = QtWidgets.QPushButton("OK", btns_widget) + cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(ok_btn, 0) + btns_layout.addWidget(cancel_btn, 0) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(attrs_widget, 1) + main_layout.addWidget(btns_widget, 0) + + ok_btn.clicked.connect(self.accept) + cancel_btn.clicked.connect(self.reject) + + self._attrs_widget = attrs_widget + + def get_values(self): + return self._attrs_widget.current_value() From 72e166066eb2a82d694f5c5e6dc66a5fbfeb4e6e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 7 Sep 2022 13:48:01 +0200 Subject: [PATCH 004/160] removed attribute defs from widgets --- openpype/widgets/attribute_defs/__init__.py | 10 - .../widgets/attribute_defs/files_widget.py | 968 ------------------ openpype/widgets/attribute_defs/widgets.py | 490 --------- 3 files changed, 1468 deletions(-) delete mode 100644 openpype/widgets/attribute_defs/__init__.py delete mode 100644 openpype/widgets/attribute_defs/files_widget.py delete mode 100644 openpype/widgets/attribute_defs/widgets.py diff --git a/openpype/widgets/attribute_defs/__init__.py b/openpype/widgets/attribute_defs/__init__.py deleted file mode 100644 index ce6b80109e..0000000000 --- a/openpype/widgets/attribute_defs/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .widgets import ( - create_widget_for_attr_def, - AttributeDefinitionsWidget, -) - - -__all__ = ( - "create_widget_for_attr_def", - "AttributeDefinitionsWidget", -) diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py deleted file mode 100644 index d29aa1b607..0000000000 --- a/openpype/widgets/attribute_defs/files_widget.py +++ /dev/null @@ -1,968 +0,0 @@ -import os -import collections -import uuid -import json - -from Qt import QtWidgets, QtCore, QtGui - -from openpype.lib import FileDefItem -from openpype.tools.utils import ( - paint_image_with_color, - ClickableLabel, -) -# TODO change imports -from openpype.tools.resources import get_image -from openpype.tools.utils import ( - IconButton, - PixmapLabel -) - -ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 -ITEM_LABEL_ROLE = QtCore.Qt.UserRole + 2 -ITEM_ICON_ROLE = QtCore.Qt.UserRole + 3 -FILENAMES_ROLE = QtCore.Qt.UserRole + 4 -DIRPATH_ROLE = QtCore.Qt.UserRole + 5 -IS_DIR_ROLE = QtCore.Qt.UserRole + 6 -IS_SEQUENCE_ROLE = QtCore.Qt.UserRole + 7 -EXT_ROLE = QtCore.Qt.UserRole + 8 - - -def convert_bytes_to_json(bytes_value): - if isinstance(bytes_value, QtCore.QByteArray): - # Raw data are already QByteArray and we don't have to load them - encoded_data = bytes_value - else: - encoded_data = QtCore.QByteArray.fromRawData(bytes_value) - stream = QtCore.QDataStream(encoded_data, QtCore.QIODevice.ReadOnly) - text = stream.readQString() - try: - return json.loads(text) - except Exception: - return None - - -def convert_data_to_bytes(data): - bytes_value = QtCore.QByteArray() - stream = QtCore.QDataStream(bytes_value, QtCore.QIODevice.WriteOnly) - stream.writeQString(json.dumps(data)) - return bytes_value - - -class SupportLabel(QtWidgets.QLabel): - pass - - -class DropEmpty(QtWidgets.QWidget): - _empty_extensions = "Any file" - - def __init__(self, single_item, allow_sequences, extensions_label, parent): - super(DropEmpty, self).__init__(parent) - - drop_label_widget = QtWidgets.QLabel("Drag & Drop files here", self) - - items_label_widget = SupportLabel(self) - items_label_widget.setWordWrap(True) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addSpacing(20) - layout.addWidget( - drop_label_widget, 0, alignment=QtCore.Qt.AlignCenter - ) - layout.addSpacing(30) - layout.addStretch(1) - layout.addWidget( - items_label_widget, 0, alignment=QtCore.Qt.AlignCenter - ) - layout.addSpacing(10) - - for widget in ( - drop_label_widget, - items_label_widget, - ): - widget.setAlignment(QtCore.Qt.AlignCenter) - widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - - update_size_timer = QtCore.QTimer() - update_size_timer.setInterval(10) - update_size_timer.setSingleShot(True) - - update_size_timer.timeout.connect(self._on_update_size_timer) - - self._update_size_timer = update_size_timer - - if extensions_label and not extensions_label.startswith(" "): - extensions_label = " " + extensions_label - - self._single_item = single_item - self._extensions_label = extensions_label - self._allow_sequences = allow_sequences - self._allowed_extensions = set() - self._allow_folders = None - - self._drop_label_widget = drop_label_widget - self._items_label_widget = items_label_widget - - self.set_allow_folders(False) - - def set_extensions(self, extensions): - if extensions: - extensions = { - ext.replace(".", "") - for ext in extensions - } - if extensions == self._allowed_extensions: - return - self._allowed_extensions = extensions - - self._update_items_label() - - def set_allow_folders(self, allowed): - if self._allow_folders == allowed: - return - - self._allow_folders = allowed - self._update_items_label() - - def _update_items_label(self): - allowed_items = [] - if self._allow_folders: - allowed_items.append("folder") - - if self._allowed_extensions: - allowed_items.append("file") - if self._allow_sequences: - allowed_items.append("sequence") - - if not self._single_item: - allowed_items = [item + "s" for item in allowed_items] - - if not allowed_items: - self._items_label_widget.setText( - "It is not allowed to add anything here!" - ) - return - - items_label = "Multiple " - if self._single_item: - items_label = "Single " - - if len(allowed_items) == 1: - extensions_label = allowed_items[0] - elif len(allowed_items) == 2: - extensions_label = " or ".join(allowed_items) - else: - last_item = allowed_items.pop(-1) - new_last_item = " or ".join(last_item, allowed_items.pop(-1)) - allowed_items.append(new_last_item) - extensions_label = ", ".join(allowed_items) - - allowed_items_label = extensions_label - - items_label += allowed_items_label - label_tooltip = None - if self._allowed_extensions: - items_label += " of\n{}".format( - ", ".join(sorted(self._allowed_extensions)) - ) - - if self._extensions_label: - label_tooltip = items_label - items_label = self._extensions_label - - if self._items_label_widget.text() == items_label: - return - - self._items_label_widget.setToolTip(label_tooltip) - self._items_label_widget.setText(items_label) - self._update_size_timer.start() - - def resizeEvent(self, event): - super(DropEmpty, self).resizeEvent(event) - self._update_size_timer.start() - - def _on_update_size_timer(self): - """Recalculate height of label with extensions. - - Dynamic QLabel with word wrap does not handle properly it's sizeHint - calculations on show. This way it is recalculated. It is good practice - to trigger this method with small offset using '_update_size_timer'. - """ - - width = self._items_label_widget.width() - height = self._items_label_widget.heightForWidth(width) - self._items_label_widget.setMinimumHeight(height) - self._items_label_widget.updateGeometry() - - def paintEvent(self, event): - super(DropEmpty, self).paintEvent(event) - painter = QtGui.QPainter(self) - pen = QtGui.QPen() - pen.setWidth(1) - pen.setBrush(QtCore.Qt.darkGray) - pen.setStyle(QtCore.Qt.DashLine) - painter.setPen(pen) - content_margins = self.layout().contentsMargins() - - left_m = content_margins.left() - top_m = content_margins.top() - rect = QtCore.QRect( - left_m, - top_m, - ( - self.rect().width() - - (left_m + content_margins.right() + pen.width()) - ), - ( - self.rect().height() - - (top_m + content_margins.bottom() + pen.width()) - ) - ) - painter.drawRect(rect) - - -class FilesModel(QtGui.QStandardItemModel): - def __init__(self, single_item, allow_sequences): - super(FilesModel, self).__init__() - - self._id = str(uuid.uuid4()) - self._single_item = single_item - self._multivalue = False - self._allow_sequences = allow_sequences - - self._items_by_id = {} - self._file_items_by_id = {} - self._filenames_by_dirpath = collections.defaultdict(set) - self._items_by_dirpath = collections.defaultdict(list) - - @property - def id(self): - return self._id - - def set_multivalue(self, multivalue): - """Disable filtering.""" - - if self._multivalue == multivalue: - return - self._multivalue = multivalue - - def add_filepaths(self, items): - if not items: - return - - file_items = FileDefItem.from_value(items, self._allow_sequences) - if not file_items: - return - - if not self._multivalue and self._single_item: - file_items = [file_items[0]] - current_ids = list(self._file_items_by_id.keys()) - if current_ids: - self.remove_item_by_ids(current_ids) - - new_model_items = [] - for file_item in file_items: - item_id, model_item = self._create_item(file_item) - new_model_items.append(model_item) - self._file_items_by_id[item_id] = file_item - self._items_by_id[item_id] = model_item - - if new_model_items: - roow_item = self.invisibleRootItem() - roow_item.appendRows(new_model_items) - - def remove_item_by_ids(self, item_ids): - if not item_ids: - return - - items = [] - for item_id in set(item_ids): - if item_id not in self._items_by_id: - continue - item = self._items_by_id.pop(item_id) - self._file_items_by_id.pop(item_id) - items.append(item) - - if items: - for item in items: - self.removeRows(item.row(), 1) - - def get_file_item_by_id(self, item_id): - return self._file_items_by_id.get(item_id) - - def _create_item(self, file_item): - if file_item.is_dir: - icon_pixmap = paint_image_with_color( - get_image(filename="folder.png"), QtCore.Qt.white - ) - else: - icon_pixmap = paint_image_with_color( - get_image(filename="file.png"), QtCore.Qt.white - ) - - item = QtGui.QStandardItem() - item_id = str(uuid.uuid4()) - item.setData(item_id, ITEM_ID_ROLE) - item.setData(file_item.label or "< empty >", ITEM_LABEL_ROLE) - item.setData(file_item.filenames, FILENAMES_ROLE) - item.setData(file_item.directory, DIRPATH_ROLE) - item.setData(icon_pixmap, ITEM_ICON_ROLE) - item.setData(file_item.ext, EXT_ROLE) - item.setData(file_item.is_dir, IS_DIR_ROLE) - item.setData(file_item.is_sequence, IS_SEQUENCE_ROLE) - - return item_id, item - - def mimeData(self, indexes): - item_ids = [ - index.data(ITEM_ID_ROLE) - for index in indexes - ] - - item_ids_data = convert_data_to_bytes(item_ids) - mime_data = super(FilesModel, self).mimeData(indexes) - mime_data.setData("files_widget/internal_move", item_ids_data) - - file_items = [] - for item_id in item_ids: - file_item = self.get_file_item_by_id(item_id) - if file_item: - file_items.append(file_item.to_dict()) - - full_item_data = convert_data_to_bytes({ - "items": file_items, - "id": self._id - }) - mime_data.setData("files_widget/full_data", full_item_data) - return mime_data - - def dropMimeData(self, mime_data, action, row, col, index): - item_ids = convert_bytes_to_json( - mime_data.data("files_widget/internal_move") - ) - if item_ids is None: - return False - - # Find matching item after which will be items moved - # - store item before moved items are removed - root = self.invisibleRootItem() - if row >= 0: - src_item = self.item(row) - else: - src_item_id = index.data(ITEM_ID_ROLE) - src_item = self._items_by_id.get(src_item_id) - - # Take out items that should be moved - items = [] - for item_id in item_ids: - item = self._items_by_id.get(item_id) - if item: - self.takeRow(item.row()) - items.append(item) - - # Skip if there are not items that can be moved - if not items: - return False - - # Calculate row where items should be inserted - if src_item: - src_row = src_item.row() - else: - src_row = root.rowCount() - - root.insertRow(src_row, items) - return True - - -class FilesProxyModel(QtCore.QSortFilterProxyModel): - def __init__(self, *args, **kwargs): - super(FilesProxyModel, self).__init__(*args, **kwargs) - self._allow_folders = False - self._allowed_extensions = None - self._multivalue = False - - def set_multivalue(self, multivalue): - """Disable filtering.""" - - if self._multivalue == multivalue: - return - self._multivalue = multivalue - self.invalidateFilter() - - def set_allow_folders(self, allow=None): - if allow is None: - allow = not self._allow_folders - - if allow == self._allow_folders: - return - self._allow_folders = allow - self.invalidateFilter() - - def set_allowed_extensions(self, extensions=None): - if extensions is not None: - _extensions = set() - for ext in set(extensions): - if not ext.startswith("."): - ext = ".{}".format(ext) - _extensions.add(ext.lower()) - extensions = _extensions - - if self._allowed_extensions != extensions: - self._allowed_extensions = extensions - self.invalidateFilter() - - def are_valid_files(self, filepaths): - for filepath in filepaths: - if os.path.isfile(filepath): - _, ext = os.path.splitext(filepath) - if ext in self._allowed_extensions: - return True - - elif self._allow_folders: - return True - return False - - def filter_valid_files(self, filepaths): - filtered_paths = [] - for filepath in filepaths: - if os.path.isfile(filepath): - _, ext = os.path.splitext(filepath) - if ext in self._allowed_extensions: - filtered_paths.append(filepath) - - elif self._allow_folders: - filtered_paths.append(filepath) - return filtered_paths - - def filterAcceptsRow(self, row, parent_index): - # Skip filtering if multivalue is set - if self._multivalue: - return True - - model = self.sourceModel() - index = model.index(row, self.filterKeyColumn(), parent_index) - # First check if item is folder and if folders are enabled - if index.data(IS_DIR_ROLE): - if not self._allow_folders: - return False - return True - - # Check if there are any allowed extensions - if self._allowed_extensions is None: - return False - - if index.data(EXT_ROLE) not in self._allowed_extensions: - return False - return True - - def lessThan(self, left, right): - left_comparison = left.data(DIRPATH_ROLE) - right_comparison = right.data(DIRPATH_ROLE) - if left_comparison == right_comparison: - left_comparison = left.data(ITEM_LABEL_ROLE) - right_comparison = right.data(ITEM_LABEL_ROLE) - - if sorted((left_comparison, right_comparison))[0] == left_comparison: - return True - return False - - -class ItemWidget(QtWidgets.QWidget): - context_menu_requested = QtCore.Signal(QtCore.QPoint) - - def __init__( - self, item_id, label, pixmap_icon, is_sequence, multivalue, parent=None - ): - self._item_id = item_id - - super(ItemWidget, self).__init__(parent) - - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - - icon_widget = PixmapLabel(pixmap_icon, self) - label_widget = QtWidgets.QLabel(label, self) - - label_size_hint = label_widget.sizeHint() - height = label_size_hint.height() - actions_menu_pix = paint_image_with_color( - get_image(filename="menu.png"), QtCore.Qt.white - ) - - split_btn = ClickableLabel(self) - split_btn.setFixedSize(height, height) - split_btn.setPixmap(actions_menu_pix) - if multivalue: - split_btn.setVisible(False) - else: - split_btn.setVisible(is_sequence) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(5, 5, 5, 5) - layout.addWidget(icon_widget, 0) - layout.addWidget(label_widget, 1) - layout.addWidget(split_btn, 0) - - split_btn.clicked.connect(self._on_actions_clicked) - - self._icon_widget = icon_widget - self._label_widget = label_widget - self._split_btn = split_btn - self._actions_menu_pix = actions_menu_pix - self._last_scaled_pix_height = None - - def _update_btn_size(self): - label_size_hint = self._label_widget.sizeHint() - height = label_size_hint.height() - if height == self._last_scaled_pix_height: - return - self._last_scaled_pix_height = height - self._split_btn.setFixedSize(height, height) - pix = self._actions_menu_pix.scaled( - height, height, - QtCore.Qt.KeepAspectRatio, - QtCore.Qt.SmoothTransformation - ) - self._split_btn.setPixmap(pix) - - def showEvent(self, event): - super(ItemWidget, self).showEvent(event) - self._update_btn_size() - - def resizeEvent(self, event): - super(ItemWidget, self).resizeEvent(event) - self._update_btn_size() - - def _on_actions_clicked(self): - pos = self._split_btn.rect().bottomLeft() - point = self._split_btn.mapToGlobal(pos) - self.context_menu_requested.emit(point) - - -class InViewButton(IconButton): - pass - - -class FilesView(QtWidgets.QListView): - """View showing instances and their groups.""" - - remove_requested = QtCore.Signal() - context_menu_requested = QtCore.Signal(QtCore.QPoint) - - def __init__(self, *args, **kwargs): - super(FilesView, self).__init__(*args, **kwargs) - - self.setEditTriggers(QtWidgets.QListView.NoEditTriggers) - self.setSelectionMode( - QtWidgets.QAbstractItemView.ExtendedSelection - ) - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.setAcceptDrops(True) - self.setDragEnabled(True) - self.setDragDropMode(self.InternalMove) - - remove_btn = InViewButton(self) - pix_enabled = paint_image_with_color( - get_image(filename="delete.png"), QtCore.Qt.white - ) - pix_disabled = paint_image_with_color( - get_image(filename="delete.png"), QtCore.Qt.gray - ) - icon = QtGui.QIcon(pix_enabled) - icon.addPixmap(pix_disabled, icon.Disabled, icon.Off) - remove_btn.setIcon(icon) - remove_btn.setEnabled(False) - - remove_btn.clicked.connect(self._on_remove_clicked) - self.customContextMenuRequested.connect(self._on_context_menu_request) - - self._remove_btn = remove_btn - - def setSelectionModel(self, *args, **kwargs): - """Catch selection model set to register signal callback. - - Selection model is not available during initialization. - """ - - super(FilesView, self).setSelectionModel(*args, **kwargs) - selection_model = self.selectionModel() - selection_model.selectionChanged.connect(self._on_selection_change) - - def set_multivalue(self, multivalue): - """Disable remove button on multivalue.""" - - self._remove_btn.setVisible(not multivalue) - - def has_selected_item_ids(self): - """Is any index selected.""" - for index in self.selectionModel().selectedIndexes(): - instance_id = index.data(ITEM_ID_ROLE) - if instance_id is not None: - return True - return False - - def get_selected_item_ids(self): - """Ids of selected instances.""" - - selected_item_ids = set() - for index in self.selectionModel().selectedIndexes(): - instance_id = index.data(ITEM_ID_ROLE) - if instance_id is not None: - selected_item_ids.add(instance_id) - return selected_item_ids - - def has_selected_sequence(self): - for index in self.selectionModel().selectedIndexes(): - if index.data(IS_SEQUENCE_ROLE): - return True - return False - - def event(self, event): - if event.type() == QtCore.QEvent.KeyPress: - if ( - event.key() == QtCore.Qt.Key_Delete - and self.has_selected_item_ids() - ): - self.remove_requested.emit() - return True - - return super(FilesView, self).event(event) - - def _on_context_menu_request(self, pos): - index = self.indexAt(pos) - if index.isValid(): - point = self.viewport().mapToGlobal(pos) - self.context_menu_requested.emit(point) - - def _on_selection_change(self): - self._remove_btn.setEnabled(self.has_selected_item_ids()) - - def _on_remove_clicked(self): - self.remove_requested.emit() - - def _update_remove_btn(self): - """Position remove button to bottom right.""" - - viewport = self.viewport() - height = viewport.height() - pos_x = viewport.width() - self._remove_btn.width() - 5 - pos_y = height - self._remove_btn.height() - 5 - self._remove_btn.move(max(0, pos_x), max(0, pos_y)) - - def resizeEvent(self, event): - super(FilesView, self).resizeEvent(event) - self._update_remove_btn() - - def showEvent(self, event): - super(FilesView, self).showEvent(event) - self._update_remove_btn() - - -class FilesWidget(QtWidgets.QFrame): - value_changed = QtCore.Signal() - - def __init__(self, single_item, allow_sequences, extensions_label, parent): - super(FilesWidget, self).__init__(parent) - self.setAcceptDrops(True) - - empty_widget = DropEmpty( - single_item, allow_sequences, extensions_label, self - ) - - files_model = FilesModel(single_item, allow_sequences) - files_proxy_model = FilesProxyModel() - files_proxy_model.setSourceModel(files_model) - files_view = FilesView(self) - files_view.setModel(files_proxy_model) - files_view.setVisible(False) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(empty_widget, 1) - layout.addWidget(files_view, 1) - - files_proxy_model.rowsInserted.connect(self._on_rows_inserted) - files_proxy_model.rowsRemoved.connect(self._on_rows_removed) - files_view.remove_requested.connect(self._on_remove_requested) - files_view.context_menu_requested.connect( - self._on_context_menu_requested - ) - - self._in_set_value = False - self._single_item = single_item - self._multivalue = False - - self._empty_widget = empty_widget - self._files_model = files_model - self._files_proxy_model = files_proxy_model - self._files_view = files_view - - self._widgets_by_id = {} - - def _set_multivalue(self, multivalue): - if self._multivalue == multivalue: - return - self._multivalue = multivalue - self._files_view.set_multivalue(multivalue) - self._files_model.set_multivalue(multivalue) - self._files_proxy_model.set_multivalue(multivalue) - - def set_value(self, value, multivalue): - self._in_set_value = True - - widget_ids = set(self._widgets_by_id.keys()) - self._remove_item_by_ids(widget_ids) - - self._set_multivalue(multivalue) - - self._add_filepaths(value) - - self._in_set_value = False - - def current_value(self): - model = self._files_proxy_model - item_ids = set() - for row in range(model.rowCount()): - index = model.index(row, 0) - item_ids.add(index.data(ITEM_ID_ROLE)) - - file_items = [] - for item_id in item_ids: - file_item = self._files_model.get_file_item_by_id(item_id) - if file_item is not None: - file_items.append(file_item.to_dict()) - - if not self._single_item: - return file_items - if file_items: - return file_items[0] - - empty_item = FileDefItem.create_empty_item() - return empty_item.to_dict() - - def set_filters(self, folders_allowed, exts_filter): - self._files_proxy_model.set_allow_folders(folders_allowed) - self._files_proxy_model.set_allowed_extensions(exts_filter) - self._empty_widget.set_extensions(exts_filter) - self._empty_widget.set_allow_folders(folders_allowed) - - def _on_rows_inserted(self, parent_index, start_row, end_row): - for row in range(start_row, end_row + 1): - index = self._files_proxy_model.index(row, 0, parent_index) - item_id = index.data(ITEM_ID_ROLE) - if item_id in self._widgets_by_id: - continue - label = index.data(ITEM_LABEL_ROLE) - pixmap_icon = index.data(ITEM_ICON_ROLE) - is_sequence = index.data(IS_SEQUENCE_ROLE) - - widget = ItemWidget( - item_id, - label, - pixmap_icon, - is_sequence, - self._multivalue - ) - widget.context_menu_requested.connect( - self._on_context_menu_requested - ) - self._files_view.setIndexWidget(index, widget) - self._files_proxy_model.setData( - index, widget.sizeHint(), QtCore.Qt.SizeHintRole - ) - self._widgets_by_id[item_id] = widget - - if not self._in_set_value: - self.value_changed.emit() - - def _on_rows_removed(self, parent_index, start_row, end_row): - available_item_ids = set() - for row in range(self._files_proxy_model.rowCount()): - index = self._files_proxy_model.index(row, 0) - item_id = index.data(ITEM_ID_ROLE) - available_item_ids.add(index.data(ITEM_ID_ROLE)) - - widget_ids = set(self._widgets_by_id.keys()) - for item_id in available_item_ids: - if item_id in widget_ids: - widget_ids.remove(item_id) - - for item_id in widget_ids: - widget = self._widgets_by_id.pop(item_id) - widget.setVisible(False) - widget.deleteLater() - - if not self._in_set_value: - self.value_changed.emit() - - def _on_split_request(self): - if self._multivalue: - return - - item_ids = self._files_view.get_selected_item_ids() - if not item_ids: - return - - for item_id in item_ids: - file_item = self._files_model.get_file_item_by_id(item_id) - if not file_item: - return - - new_items = file_item.split_sequence() - self._add_filepaths(new_items) - self._remove_item_by_ids(item_ids) - - def _on_remove_requested(self): - if self._multivalue: - return - - items_to_delete = self._files_view.get_selected_item_ids() - if items_to_delete: - self._remove_item_by_ids(items_to_delete) - - def _on_context_menu_requested(self, pos): - if self._multivalue: - return - - menu = QtWidgets.QMenu(self._files_view) - - if self._files_view.has_selected_sequence(): - split_action = QtWidgets.QAction("Split sequence", menu) - split_action.triggered.connect(self._on_split_request) - menu.addAction(split_action) - - remove_action = QtWidgets.QAction("Remove", menu) - remove_action.triggered.connect(self._on_remove_requested) - menu.addAction(remove_action) - - menu.popup(pos) - - def sizeHint(self): - # Get size hints of widget and visible widgets - result = super(FilesWidget, self).sizeHint() - if not self._files_view.isVisible(): - not_visible_hint = self._files_view.sizeHint() - else: - not_visible_hint = self._empty_widget.sizeHint() - - # Get margins of this widget - margins = self.layout().contentsMargins() - - # Change size hint based on result of maximum size hint of widgets - result.setWidth(max( - result.width(), - not_visible_hint.width() + margins.left() + margins.right() - )) - result.setHeight(max( - result.height(), - not_visible_hint.height() + margins.top() + margins.bottom() - )) - - return result - - def dragEnterEvent(self, event): - if self._multivalue: - return - - mime_data = event.mimeData() - if mime_data.hasUrls(): - filepaths = [] - for url in mime_data.urls(): - filepath = url.toLocalFile() - if os.path.exists(filepath): - filepaths.append(filepath) - - if self._files_proxy_model.are_valid_files(filepaths): - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - - full_data_value = mime_data.data("files_widget/full_data") - if self._handle_full_data_drag(full_data_value): - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - - def dragLeaveEvent(self, event): - event.accept() - - def dropEvent(self, event): - if self._multivalue: - return - - mime_data = event.mimeData() - if mime_data.hasUrls(): - event.accept() - # event.setDropAction(QtCore.Qt.CopyAction) - filepaths = [] - for url in mime_data.urls(): - filepath = url.toLocalFile() - if os.path.exists(filepath): - filepaths.append(filepath) - - # Filter filepaths before passing it to model - filepaths = self._files_proxy_model.filter_valid_files(filepaths) - if filepaths: - self._add_filepaths(filepaths) - - if self._handle_full_data_drop( - mime_data.data("files_widget/full_data") - ): - event.setDropAction(QtCore.Qt.CopyAction) - event.accept() - - super(FilesWidget, self).dropEvent(event) - - def _handle_full_data_drag(self, value): - if value is None: - return False - - full_data = convert_bytes_to_json(value) - if full_data is None: - return False - - if full_data["id"] == self._files_model.id: - return False - return True - - def _handle_full_data_drop(self, value): - if value is None: - return False - - full_data = convert_bytes_to_json(value) - if full_data is None: - return False - - if full_data["id"] == self._files_model.id: - return False - - for item in full_data["items"]: - filepaths = [ - os.path.join(item["directory"], filename) - for filename in item["filenames"] - ] - filepaths = self._files_proxy_model.filter_valid_files(filepaths) - if filepaths: - self._add_filepaths(filepaths) - - if self._copy_modifiers_enabled(): - return False - return True - - def _copy_modifiers_enabled(self): - if ( - QtWidgets.QApplication.keyboardModifiers() - & QtCore.Qt.ControlModifier - ): - return True - return False - - def _add_filepaths(self, filepaths): - self._files_model.add_filepaths(filepaths) - self._update_visibility() - - def _remove_item_by_ids(self, item_ids): - self._files_model.remove_item_by_ids(item_ids) - self._update_visibility() - - def _update_visibility(self): - files_exists = self._files_proxy_model.rowCount() > 0 - self._files_view.setVisible(files_exists) - self._empty_widget.setVisible(not files_exists) diff --git a/openpype/widgets/attribute_defs/widgets.py b/openpype/widgets/attribute_defs/widgets.py deleted file mode 100644 index 60ae952553..0000000000 --- a/openpype/widgets/attribute_defs/widgets.py +++ /dev/null @@ -1,490 +0,0 @@ -import uuid -import copy - -from Qt import QtWidgets, QtCore - -from openpype.lib.attribute_definitions import ( - AbtractAttrDef, - UnknownDef, - NumberDef, - TextDef, - EnumDef, - BoolDef, - FileDef, - UIDef, - UISeparatorDef, - UILabelDef -) -from openpype.tools.utils import CustomTextComboBox -from openpype.widgets.nice_checkbox import NiceCheckbox - -from .files_widget import FilesWidget - - -def create_widget_for_attr_def(attr_def, parent=None): - if not isinstance(attr_def, AbtractAttrDef): - raise TypeError("Unexpected type \"{}\" expected \"{}\"".format( - str(type(attr_def)), AbtractAttrDef - )) - - if isinstance(attr_def, NumberDef): - return NumberAttrWidget(attr_def, parent) - - if isinstance(attr_def, TextDef): - return TextAttrWidget(attr_def, parent) - - if isinstance(attr_def, EnumDef): - return EnumAttrWidget(attr_def, parent) - - if isinstance(attr_def, BoolDef): - return BoolAttrWidget(attr_def, parent) - - if isinstance(attr_def, UnknownDef): - return UnknownAttrWidget(attr_def, parent) - - if isinstance(attr_def, FileDef): - return FileAttrWidget(attr_def, parent) - - if isinstance(attr_def, UISeparatorDef): - return SeparatorAttrWidget(attr_def, parent) - - if isinstance(attr_def, UILabelDef): - return LabelAttrWidget(attr_def, parent) - - raise ValueError("Unknown attribute definition \"{}\"".format( - str(type(attr_def)) - )) - - -class AttributeDefinitionsWidget(QtWidgets.QWidget): - """Create widgets for attribute definitions in grid layout. - - Widget creates input widgets for passed attribute definitions. - - Widget can't handle multiselection values. - """ - - def __init__(self, attr_defs=None, parent=None): - super(AttributeDefinitionsWidget, self).__init__(parent) - - self._widgets = [] - self._current_keys = set() - - self.set_attr_defs(attr_defs) - - def clear_attr_defs(self): - """Remove all existing widgets and reset layout if needed.""" - self._widgets = [] - self._current_keys = set() - - layout = self.layout() - if layout is not None: - if layout.count() == 0: - return - - while layout.count(): - item = layout.takeAt(0) - widget = item.widget() - if widget: - widget.setVisible(False) - widget.deleteLater() - - layout.deleteLater() - - new_layout = QtWidgets.QGridLayout() - new_layout.setColumnStretch(0, 0) - new_layout.setColumnStretch(1, 1) - self.setLayout(new_layout) - - def set_attr_defs(self, attr_defs): - """Replace current attribute definitions with passed.""" - self.clear_attr_defs() - if attr_defs: - self.add_attr_defs(attr_defs) - - def add_attr_defs(self, attr_defs): - """Add attribute definitions to current.""" - layout = self.layout() - - row = 0 - for attr_def in attr_defs: - if attr_def.key in self._current_keys: - raise KeyError("Duplicated key \"{}\"".format(attr_def.key)) - - self._current_keys.add(attr_def.key) - widget = create_widget_for_attr_def(attr_def, self) - - expand_cols = 2 - if attr_def.is_value_def and attr_def.is_label_horizontal: - expand_cols = 1 - - col_num = 2 - expand_cols - - if attr_def.label: - label_widget = QtWidgets.QLabel(attr_def.label, self) - layout.addWidget( - label_widget, row, 0, 1, expand_cols - ) - if not attr_def.is_label_horizontal: - row += 1 - - layout.addWidget( - widget, row, col_num, 1, expand_cols - ) - self._widgets.append(widget) - row += 1 - - def set_value(self, value): - new_value = copy.deepcopy(value) - unused_keys = set(new_value.keys()) - for widget in self._widgets: - attr_def = widget.attr_def - if attr_def.key not in new_value: - continue - unused_keys.remove(attr_def.key) - - widget_value = new_value[attr_def.key] - if widget_value is None: - widget_value = copy.deepcopy(attr_def.default) - widget.set_value(widget_value) - - def current_value(self): - output = {} - for widget in self._widgets: - attr_def = widget.attr_def - if not isinstance(attr_def, UIDef): - output[attr_def.key] = widget.current_value() - - return output - - -class _BaseAttrDefWidget(QtWidgets.QWidget): - # Type 'object' may not work with older PySide versions - value_changed = QtCore.Signal(object, uuid.UUID) - - def __init__(self, attr_def, parent): - super(_BaseAttrDefWidget, self).__init__(parent) - - self.attr_def = attr_def - - main_layout = QtWidgets.QHBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - - self.main_layout = main_layout - - self._ui_init() - - def _ui_init(self): - raise NotImplementedError( - "Method '_ui_init' is not implemented. {}".format( - self.__class__.__name__ - ) - ) - - def current_value(self): - raise NotImplementedError( - "Method 'current_value' is not implemented. {}".format( - self.__class__.__name__ - ) - ) - - def set_value(self, value, multivalue=False): - raise NotImplementedError( - "Method 'set_value' is not implemented. {}".format( - self.__class__.__name__ - ) - ) - - -class SeparatorAttrWidget(_BaseAttrDefWidget): - def _ui_init(self): - input_widget = QtWidgets.QWidget(self) - input_widget.setObjectName("Separator") - input_widget.setMinimumHeight(2) - input_widget.setMaximumHeight(2) - - self._input_widget = input_widget - - self.main_layout.addWidget(input_widget, 0) - - -class LabelAttrWidget(_BaseAttrDefWidget): - def _ui_init(self): - input_widget = QtWidgets.QLabel(self) - label = self.attr_def.label - if label: - input_widget.setText(str(label)) - - self._input_widget = input_widget - - self.main_layout.addWidget(input_widget, 0) - - -class NumberAttrWidget(_BaseAttrDefWidget): - def _ui_init(self): - decimals = self.attr_def.decimals - if decimals > 0: - input_widget = QtWidgets.QDoubleSpinBox(self) - input_widget.setDecimals(decimals) - else: - input_widget = QtWidgets.QSpinBox(self) - - if self.attr_def.tooltip: - input_widget.setToolTip(self.attr_def.tooltip) - - input_widget.setMinimum(self.attr_def.minimum) - input_widget.setMaximum(self.attr_def.maximum) - input_widget.setValue(self.attr_def.default) - - input_widget.setButtonSymbols( - QtWidgets.QAbstractSpinBox.ButtonSymbols.NoButtons - ) - - input_widget.valueChanged.connect(self._on_value_change) - - self._input_widget = input_widget - - self.main_layout.addWidget(input_widget, 0) - - def _on_value_change(self, new_value): - self.value_changed.emit(new_value, self.attr_def.id) - - def current_value(self): - return self._input_widget.value() - - def set_value(self, value, multivalue=False): - if multivalue: - set_value = set(value) - if None in set_value: - set_value.remove(None) - set_value.add(self.attr_def.default) - - if len(set_value) > 1: - self._input_widget.setSpecialValueText("Multiselection") - return - value = tuple(set_value)[0] - - if self.current_value != value: - self._input_widget.setValue(value) - - -class TextAttrWidget(_BaseAttrDefWidget): - def _ui_init(self): - # TODO Solve how to handle regex - # self.attr_def.regex - - self.multiline = self.attr_def.multiline - if self.multiline: - input_widget = QtWidgets.QPlainTextEdit(self) - else: - input_widget = QtWidgets.QLineEdit(self) - - if ( - self.attr_def.placeholder - and hasattr(input_widget, "setPlaceholderText") - ): - input_widget.setPlaceholderText(self.attr_def.placeholder) - - if self.attr_def.tooltip: - input_widget.setToolTip(self.attr_def.tooltip) - - if self.attr_def.default: - if self.multiline: - input_widget.setPlainText(self.attr_def.default) - else: - input_widget.setText(self.attr_def.default) - - input_widget.textChanged.connect(self._on_value_change) - - self._input_widget = input_widget - - self.main_layout.addWidget(input_widget, 0) - - def _on_value_change(self): - if self.multiline: - new_value = self._input_widget.toPlainText() - else: - new_value = self._input_widget.text() - self.value_changed.emit(new_value, self.attr_def.id) - - def current_value(self): - if self.multiline: - return self._input_widget.toPlainText() - return self._input_widget.text() - - def set_value(self, value, multivalue=False): - if multivalue: - set_value = set(value) - if None in set_value: - set_value.remove(None) - set_value.add(self.attr_def.default) - - if len(set_value) == 1: - value = tuple(set_value)[0] - else: - value = "< Multiselection >" - - if value != self.current_value(): - if self.multiline: - self._input_widget.setPlainText(value) - else: - self._input_widget.setText(value) - - -class BoolAttrWidget(_BaseAttrDefWidget): - def _ui_init(self): - input_widget = NiceCheckbox(parent=self) - input_widget.setChecked(self.attr_def.default) - - if self.attr_def.tooltip: - input_widget.setToolTip(self.attr_def.tooltip) - - input_widget.stateChanged.connect(self._on_value_change) - - self._input_widget = input_widget - - self.main_layout.addWidget(input_widget, 0) - self.main_layout.addStretch(1) - - def _on_value_change(self): - new_value = self._input_widget.isChecked() - self.value_changed.emit(new_value, self.attr_def.id) - - def current_value(self): - return self._input_widget.isChecked() - - def set_value(self, value, multivalue=False): - if multivalue: - set_value = set(value) - if None in set_value: - set_value.remove(None) - set_value.add(self.attr_def.default) - - if len(set_value) > 1: - self._input_widget.setCheckState(QtCore.Qt.PartiallyChecked) - return - value = tuple(set_value)[0] - - if value != self.current_value(): - self._input_widget.setChecked(value) - - -class EnumAttrWidget(_BaseAttrDefWidget): - def __init__(self, *args, **kwargs): - self._multivalue = False - super(EnumAttrWidget, self).__init__(*args, **kwargs) - - def _ui_init(self): - input_widget = CustomTextComboBox(self) - combo_delegate = QtWidgets.QStyledItemDelegate(input_widget) - input_widget.setItemDelegate(combo_delegate) - - if self.attr_def.tooltip: - input_widget.setToolTip(self.attr_def.tooltip) - - items = self.attr_def.items - for key, label in items.items(): - input_widget.addItem(label, key) - - idx = input_widget.findData(self.attr_def.default) - if idx >= 0: - input_widget.setCurrentIndex(idx) - - input_widget.currentIndexChanged.connect(self._on_value_change) - - self._combo_delegate = combo_delegate - self._input_widget = input_widget - - self.main_layout.addWidget(input_widget, 0) - - def _on_value_change(self): - new_value = self.current_value() - if self._multivalue: - self._multivalue = False - self._input_widget.set_custom_text(None) - self.value_changed.emit(new_value, self.attr_def.id) - - def current_value(self): - idx = self._input_widget.currentIndex() - return self._input_widget.itemData(idx) - - def set_value(self, value, multivalue=False): - if multivalue: - set_value = set(value) - if len(set_value) == 1: - multivalue = False - value = tuple(set_value)[0] - - if not multivalue: - idx = self._input_widget.findData(value) - cur_idx = self._input_widget.currentIndex() - if idx != cur_idx and idx >= 0: - self._input_widget.setCurrentIndex(idx) - - custom_text = None - if multivalue: - custom_text = "< Multiselection >" - self._input_widget.set_custom_text(custom_text) - self._multivalue = multivalue - - -class UnknownAttrWidget(_BaseAttrDefWidget): - def _ui_init(self): - input_widget = QtWidgets.QLabel(self) - self._value = self.attr_def.default - input_widget.setText(str(self._value)) - - self._input_widget = input_widget - - self.main_layout.addWidget(input_widget, 0) - - def current_value(self): - raise ValueError( - "{} can't hold real value.".format(self.__class__.__name__) - ) - - def set_value(self, value, multivalue=False): - if multivalue: - set_value = set(value) - if len(set_value) == 1: - value = tuple(set_value)[0] - else: - value = "< Multiselection >" - - str_value = str(value) - if str_value != self._value: - self._value = str_value - self._input_widget.setText(str_value) - - -class FileAttrWidget(_BaseAttrDefWidget): - def _ui_init(self): - input_widget = FilesWidget( - self.attr_def.single_item, - self.attr_def.allow_sequences, - self.attr_def.extensions_label, - self - ) - - if self.attr_def.tooltip: - input_widget.setToolTip(self.attr_def.tooltip) - - input_widget.set_filters( - self.attr_def.folders, self.attr_def.extensions - ) - - input_widget.value_changed.connect(self._on_value_change) - - self._input_widget = input_widget - - self.main_layout.addWidget(input_widget, 0) - - def _on_value_change(self): - new_value = self.current_value() - self.value_changed.emit(new_value, self.attr_def.id) - - def current_value(self): - return self._input_widget.current_value() - - def set_value(self, value, multivalue=False): - self._input_widget.set_value(value, multivalue) From 44757580b78970566fb8e3a5ac3871425dacf408 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 9 Sep 2022 15:08:10 +0200 Subject: [PATCH 005/160] Fix very slow `get_container_members` calls for instances --- openpype/hosts/maya/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 58e160cb2f..06faa123f5 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -1532,7 +1532,7 @@ def get_container_members(container): if ref.rsplit(":", 1)[-1].startswith("_UNKNOWN_REF_NODE_"): continue - reference_members = cmds.referenceQuery(ref, nodes=True) + reference_members = cmds.referenceQuery(ref, nodes=True, dagPath=True) reference_members = cmds.ls(reference_members, long=True, objectsOnly=True) From b54333086be343fc1524861f69bdd050e81caa8a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 21 Oct 2022 12:41:13 +0200 Subject: [PATCH 006/160] :bug: fix wrong path in loader --- .../hosts/houdini/plugins/load/load_ass.py | 74 +++++++------------ 1 file changed, 28 insertions(+), 46 deletions(-) diff --git a/openpype/hosts/houdini/plugins/load/load_ass.py b/openpype/hosts/houdini/plugins/load/load_ass.py index 0144bbaefd..0e23259079 100644 --- a/openpype/hosts/houdini/plugins/load/load_ass.py +++ b/openpype/hosts/houdini/plugins/load/load_ass.py @@ -32,7 +32,12 @@ class AssLoader(load.LoaderPlugin): # Create a new geo node procedural = obj.createNode("arnold::procedural", node_name=node_name) - procedural.setParms({"ar_filename": self.get_path(self.fname)}) + + procedural.setParms( + { + "ar_filename": self.format_path( + self.fname, context["representation"]) + }) nodes = [procedural] self[:] = nodes @@ -46,57 +51,14 @@ class AssLoader(load.LoaderPlugin): suffix="", ) - def get_path(self, path): - - # Find all frames in the folder - ext = ".ass.gz" if path.endswith(".ass.gz") else ".ass" - folder = os.path.dirname(path) - frames = [f for f in os.listdir(folder) if f.endswith(ext)] - - # Get the collection of frames to detect frame padding - patterns = [clique.PATTERNS["frames"]] - collections, remainder = clique.assemble(frames, - minimum_items=1, - patterns=patterns) - self.log.debug("Detected collections: {}".format(collections)) - self.log.debug("Detected remainder: {}".format(remainder)) - - if not collections and remainder: - if len(remainder) != 1: - raise ValueError("Frames not correctly detected " - "in: {}".format(remainder)) - - # A single frame without frame range detected - filepath = remainder[0] - return os.path.normpath(filepath).replace("\\", "/") - - # Frames detected with a valid "frame" number pattern - # Then we don't want to have any remainder files found - assert len(collections) == 1 and not remainder - collection = collections[0] - - num_frames = len(collection.indexes) - if num_frames == 1: - # Return the input path without dynamic $F variable - result = path - else: - # More than a single frame detected - use $F{padding} - fname = "{}$F{}{}".format(collection.head, - collection.padding, - collection.tail) - result = os.path.join(folder, fname) - - # Format file name, Houdini only wants forward slashes - return os.path.normpath(result).replace("\\", "/") - def update(self, container, representation): # Update the file path file_path = get_representation_path(representation) - file_path = file_path.replace("\\", "/") + file_path = self.format_path(file_path, representation) procedural = container["node"] - procedural.setParms({"ar_filename": self.get_path(file_path)}) + procedural.setParms({"ar_filename": file_path}) # Update attribute procedural.setParms({"representation": str(representation["_id"])}) @@ -105,3 +67,23 @@ class AssLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + @staticmethod + def format_path(path, representation): + """Format file path correctly for single bgeo or bgeo sequence.""" + if not os.path.exists(path): + raise RuntimeError("Path does not exist: %s" % path) + + is_sequence = bool(representation["context"].get("frame")) + # The path is either a single file or sequence in a folder. + if not is_sequence: + filename = path + else: + filename = re.sub(r"(.*)\.(\d+)\.(ass.*)", "\\1.$F4.\\3", path) + + filename = os.path.join(path, filename) + + filename = os.path.normpath(filename) + filename = filename.replace("\\", "/") + + return filename \ No newline at end of file From 2c3e66c18a23f345ea877d16b104c4ae714cfe2a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 21 Oct 2022 12:45:56 +0200 Subject: [PATCH 007/160] :dog: fix hound --- openpype/hosts/houdini/plugins/load/load_ass.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/load/load_ass.py b/openpype/hosts/houdini/plugins/load/load_ass.py index 0e23259079..57e2d34d7c 100644 --- a/openpype/hosts/houdini/plugins/load/load_ass.py +++ b/openpype/hosts/houdini/plugins/load/load_ass.py @@ -1,6 +1,6 @@ import os -import clique +import re from openpype.pipeline import ( load, get_representation_path, @@ -86,4 +86,4 @@ class AssLoader(load.LoaderPlugin): filename = os.path.normpath(filename) filename = filename.replace("\\", "/") - return filename \ No newline at end of file + return filename From 250cdd32cec889336565a831d247471894c9a941 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 14:13:36 +0200 Subject: [PATCH 008/160] import lib functions from 'openpype.lib' --- .../plugins/publish/extract_thumbnail.py | 4 ++-- openpype/plugins/publish/collect_scene_version.py | 5 +++-- openpype/plugins/publish/extract_otio_audio_tracks.py | 11 ++++------- openpype/plugins/publish/extract_review.py | 7 +++---- openpype/plugins/publish/extract_scanline_exr.py | 8 ++++---- .../tools/settings/local_settings/mongo_widget.py | 2 +- 6 files changed, 17 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index 3ee2f70809..1e894f9dbb 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -1,11 +1,11 @@ import os import tempfile import pyblish.api -import openpype.api from openpype.lib import ( get_ffmpeg_tool_path, get_ffprobe_streams, path_to_subprocess_arg, + run_subprocess, ) @@ -96,7 +96,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): # run subprocess self.log.debug("Executing: {}".format(subprocess_jpeg)) - openpype.api.run_subprocess( + run_subprocess( subprocess_jpeg, shell=True, logger=self.log ) diff --git a/openpype/plugins/publish/collect_scene_version.py b/openpype/plugins/publish/collect_scene_version.py index 917647c61a..a7cea6093a 100644 --- a/openpype/plugins/publish/collect_scene_version.py +++ b/openpype/plugins/publish/collect_scene_version.py @@ -1,6 +1,7 @@ import os import pyblish.api -import openpype.api as pype + +from openpype.lib import get_version_from_path class CollectSceneVersion(pyblish.api.ContextPlugin): @@ -46,7 +47,7 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): if '' in filename: return - version = pype.get_version_from_path(filename) + version = get_version_from_path(filename) assert version, "Cannot determine version" rootVersion = int(version) diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py index ed30a2f0f5..e19b7eeb13 100644 --- a/openpype/plugins/publish/extract_otio_audio_tracks.py +++ b/openpype/plugins/publish/extract_otio_audio_tracks.py @@ -1,9 +1,8 @@ import os import pyblish -import openpype.api from openpype.lib import ( get_ffmpeg_tool_path, - path_to_subprocess_arg + run_subprocess ) import tempfile import opentimelineio as otio @@ -102,9 +101,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): # run subprocess self.log.debug("Executing: {}".format(" ".join(cmd))) - openpype.api.run_subprocess( - cmd, logger=self.log - ) + run_subprocess(cmd, logger=self.log) else: audio_fpath = recycling_file.pop() @@ -225,7 +222,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): # run subprocess self.log.debug("Executing: {}".format(" ".join(cmd))) - openpype.api.run_subprocess( + run_subprocess( cmd, logger=self.log ) @@ -308,7 +305,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): # run subprocess self.log.debug("Executing: {}".format(args)) - openpype.api.run_subprocess(args, logger=self.log) + run_subprocess(args, logger=self.log) os.remove(filters_tmp_filepath) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 27117510b2..1e46b47c5f 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -10,12 +10,13 @@ import six import clique import pyblish.api -import openpype.api + from openpype.lib import ( get_ffmpeg_tool_path, get_ffprobe_streams, path_to_subprocess_arg, + run_subprocess, should_convert_for_ffmpeg, convert_input_paths_for_ffmpeg, @@ -350,9 +351,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # run subprocess self.log.debug("Executing: {}".format(subprcs_cmd)) - openpype.api.run_subprocess( - subprcs_cmd, shell=True, logger=self.log - ) + run_subprocess(subprcs_cmd, shell=True, logger=self.log) # delete files added to fill gaps if files_to_clean: diff --git a/openpype/plugins/publish/extract_scanline_exr.py b/openpype/plugins/publish/extract_scanline_exr.py index a7f7de5188..0e4c0ca65f 100644 --- a/openpype/plugins/publish/extract_scanline_exr.py +++ b/openpype/plugins/publish/extract_scanline_exr.py @@ -4,8 +4,8 @@ import os import shutil import pyblish.api -import openpype.api -import openpype.lib + +from openpype.lib import run_subprocess, get_oiio_tools_path class ExtractScanlineExr(pyblish.api.InstancePlugin): @@ -45,7 +45,7 @@ class ExtractScanlineExr(pyblish.api.InstancePlugin): stagingdir = os.path.normpath(repre.get("stagingDir")) - oiio_tool_path = openpype.lib.get_oiio_tools_path() + oiio_tool_path = get_oiio_tools_path() if not os.path.exists(oiio_tool_path): self.log.error( "OIIO tool not found in {}".format(oiio_tool_path)) @@ -65,7 +65,7 @@ class ExtractScanlineExr(pyblish.api.InstancePlugin): subprocess_exr = " ".join(oiio_cmd) self.log.info(f"running: {subprocess_exr}") - openpype.api.run_subprocess(subprocess_exr, logger=self.log) + run_subprocess(subprocess_exr, logger=self.log) # raise error if there is no ouptput if not os.path.exists(os.path.join(stagingdir, original_name)): diff --git a/openpype/tools/settings/local_settings/mongo_widget.py b/openpype/tools/settings/local_settings/mongo_widget.py index 3d3dbd0a5d..600ab79242 100644 --- a/openpype/tools/settings/local_settings/mongo_widget.py +++ b/openpype/tools/settings/local_settings/mongo_widget.py @@ -5,7 +5,7 @@ import traceback from Qt import QtWidgets from pymongo.errors import ServerSelectionTimeoutError -from openpype.api import change_openpype_mongo_url +from openpype.lib import change_openpype_mongo_url from openpype.tools.utils import PlaceholderLineEdit From d29cd8edcdd678370b18ae57815942c82d7e6611 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Oct 2022 14:32:16 +0200 Subject: [PATCH 009/160] workflows: adding milestone creator and assigner --- .github/workflows/milestone_assign.yml | 28 ++++++++++++ .github/workflows/milestone_create.yml | 62 ++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 .github/workflows/milestone_assign.yml create mode 100644 .github/workflows/milestone_create.yml diff --git a/.github/workflows/milestone_assign.yml b/.github/workflows/milestone_assign.yml new file mode 100644 index 0000000000..b41886816b --- /dev/null +++ b/.github/workflows/milestone_assign.yml @@ -0,0 +1,28 @@ +name: Milestone - assign to PRs + +on: + pull_request_target: + types: [opened, reopened, edited] + +jobs: + run_if_release: + if: startsWith(github.base_ref, 'release/') + runs-on: ubuntu-latest + steps: + - name: 'Assign Milestone [next-minor]' + if: github.event.pull_request.milestone == null + uses: zoispag/action-assign-milestone@v1 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + milestone: 'next-minor' + + run_if_develop: + if: ${{ github.base_ref == 'develop' }} + runs-on: ubuntu-latest + steps: + - name: 'Assign Milestone [next-patch]' + if: github.event.pull_request.milestone == null + uses: zoispag/action-assign-milestone@v1 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + milestone: 'next-patch' \ No newline at end of file diff --git a/.github/workflows/milestone_create.yml b/.github/workflows/milestone_create.yml new file mode 100644 index 0000000000..b56ca81dc1 --- /dev/null +++ b/.github/workflows/milestone_create.yml @@ -0,0 +1,62 @@ +name: Milestone - create default + +on: + milestone: + types: [closed, edited] + +jobs: + generate-next-patch: + runs-on: ubuntu-latest + steps: + - name: 'Get Milestones' + uses: "WyriHaximus/github-action-get-milestones@master" + id: milestones + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number') + id: querymilestone + env: + MILESTONES: ${{ steps.milestones.outputs.milestones }} + MILESTONE: "next-patch" + + - name: Read output + run: | + echo "${{ steps.querymilestone.outputs.number }}" + + - name: 'Create `next-patch` milestone' + if: steps.querymilestone.outputs.number == '' + id: createmilestone + uses: "WyriHaximus/github-action-create-milestone@v1" + with: + title: 'next-patch' + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + generate-next-minor: + runs-on: ubuntu-latest + steps: + - name: 'Get Milestones' + uses: "WyriHaximus/github-action-get-milestones@master" + id: milestones + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - run: printf "name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number') + id: querymilestone + env: + MILESTONES: ${{ steps.milestones.outputs.milestones }} + MILESTONE: "next-minor" + + - name: Read output + run: | + echo "${{ steps.querymilestone.outputs.number }}" + + - name: 'Create `next-minor` milestone' + if: steps.querymilestone.outputs.number == '' + id: createmilestone + uses: "WyriHaximus/github-action-create-milestone@v1" + with: + title: 'next-minor' + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" \ No newline at end of file From 260573506b56d83d73ea785b335aa9134d652d96 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 14:56:09 +0200 Subject: [PATCH 010/160] Created simple item representing conversion requirement --- openpype/pipeline/create/context.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 3e09ff287d..918bc66cb0 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -852,6 +852,29 @@ class CreatedInstance: self[key] = new_value +class LegacyInstancesItem(object): + """Item representing convertor for legacy instances. + + Args: + identifier (str): Identifier of convertor. + label (str): Label which will be shown in UI. + """ + + def __init__(self, identifier, label): + self.identifier = identifier + self.label = label + + def to_data(self): + return { + "identifier": self.identifier, + "label": self.label + } + + @classmethod + def from_data(cls, data): + return cls(data["identifier"], data["label"]) + + class CreateContext: """Context of instance creation. From 8e99d9128a622956299e9dfdd5e22f22460e63d5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 14:56:56 +0200 Subject: [PATCH 011/160] implemented basic of convertor --- openpype/pipeline/create/creator_plugins.py | 90 +++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 97ee94c449..62562e4428 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -33,6 +33,96 @@ class CreatorError(Exception): super(CreatorError, self).__init__(message) +@six.add_metaclass(ABCMeta) +class LegacyInstanceConvertor(object): + """Helper for conversion of instances created using legacy creators. + + Conversion from legacy creators would mean to loose legacy instances, + convert them automatically or write a script which must user run. All of + these solutions are workign but will happen without asking or user must + know about them. This plugin can be used to show legacy instances in + Publisher and give user ability to run conversion script. + + Convertor logic should be very simple. Method 'find_instances' is to + look for legacy instances in scene a possibly call + pre-implemented 'add_legacy_item'. + + User will have ability to trigger conversion which is executed by calling + 'convert' which should call 'remove_legacy_item' when is done. + + It does make sense to add only one or none legacy item to create context + for convertor as it's not possible to choose which instace are converted + and which are not. + + Convertor can use 'collection_shared_data' property like creators. Also + can store any information to it's object for conversion purposes. + + Args: + create_context + """ + + def __init__(self, create_context): + self._create_context = create_context + + @abstractproperty + def identifier(self): + """Converted identifier. + + Returns: + str: Converted identifier unique for all converters in host. + """ + + pass + + @abstractmethod + def find_instances(self): + """Look for legacy instances in the scene. + + Should call 'add_legacy_item' if there is at least one item. + """ + + pass + + @abstractmethod + def convert(self): + """Conversion code.""" + + pass + + @property + def create_context(self): + """Quick access to create context.""" + + return self._create_context + + @property + def collection_shared_data(self): + """Access to shared data that can be used during 'find_instances'. + + Retruns: + Dict[str, Any]: Shared data. + + Raises: + UnavailableSharedData: When called out of collection phase. + """ + + return self._create_context.collection_shared_data + + def add_legacy_item(self, label): + """Add item to CreateContext. + + Args: + label (str): Label of item which will show in UI. + """ + + self._create_context.add_legacy_item(self.identifier, label) + + def remove_legacy_item(self): + """Remove legacy item from create context when conversion finished.""" + + self._create_context.remove_legacy_item(self.identifier) + + @six.add_metaclass(ABCMeta) class BaseCreator: """Plugin that create and modify instance data before publishing process. From 971e4a23bd67fb4ec214bed4b39f32e9f0943715 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 15:19:40 +0200 Subject: [PATCH 012/160] split reset of plugins to more methods --- openpype/pipeline/create/context.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 918bc66cb0..565fdbdf89 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1074,6 +1074,11 @@ class CreateContext: Reloads creators from preregistered paths and can load publish plugins if it's enabled on context. """ + + self._reset_publish_plugins(discover_publish_plugins) + self._reset_creator_plugins() + + def _reset_publish_plugins(self, discover_publish_plugins): import pyblish.logic from openpype.pipeline import OpenPypePyblishPluginMixin @@ -1115,6 +1120,7 @@ class CreateContext: self.publish_plugins = plugins_by_targets self.plugins_with_defs = plugins_with_defs + def _reset_creator_plugins(self): # Prepare settings system_settings = get_system_settings() project_settings = get_project_settings(self.project_name) From cff9990c6fc59ee5d142ce14db206951a5620fdf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 15:20:07 +0200 Subject: [PATCH 013/160] added logic to discover convertors and find legacy items --- openpype/pipeline/create/context.py | 51 +++++++++++++++++++++ openpype/pipeline/create/creator_plugins.py | 12 +++++ 2 files changed, 63 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 565fdbdf89..783b599aef 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -22,6 +22,7 @@ from .creator_plugins import ( Creator, AutoCreator, discover_creator_plugins, + discover_legacy_convertor_plugins, ) UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"]) @@ -940,6 +941,9 @@ class CreateContext: # Manual creators self.manual_creators = {} + self.legacy_convertors = {} + self.legacy_items_by_id = {} + self.publish_discover_result = None self.publish_plugins_mismatch_targets = [] self.publish_plugins = [] @@ -1020,6 +1024,7 @@ class CreateContext: with self.bulk_instances_collection(): self.reset_instances() + self.find_legacy_items() self.execute_autocreators() self.reset_finalization() @@ -1077,6 +1082,7 @@ class CreateContext: self._reset_publish_plugins(discover_publish_plugins) self._reset_creator_plugins() + self._reset_legacy_convertor_plugins() def _reset_publish_plugins(self, discover_publish_plugins): import pyblish.logic @@ -1172,6 +1178,29 @@ class CreateContext: self.creators = creators + def _reset_legacy_convertor_plugins(self): + legacy_convertors = {} + for convertor_class in discover_legacy_convertor_plugins(): + if inspect.isabstract(convertor_class): + self.log.info( + "Skipping abstract Creator {}".format(str(convertor_class)) + ) + continue + + convertor_identifier = convertor_class.identifier + if convertor_identifier in legacy_convertors: + self.log.warning(( + "Duplicated Converter identifier. " + "Using first and skipping following" + )) + continue + + legacy_convertors[convertor_identifier] = ( + convertor_identifier(self) + ) + + self.legacy_convertors = legacy_convertors + def reset_context_data(self): """Reload context data using host implementation. @@ -1243,6 +1272,14 @@ class CreateContext: def creator_removed_instance(self, instance): self._instances_by_id.pop(instance.id, None) + def add_legacy_item(self, convertor_identifier, label): + self.legacy_items_by_id[convertor_identifier] = ( + LegacyInstancesItem(convertor_identifier, label) + ) + + def remove_legacy_item(self, convertor_identifier): + self.legacy_items_by_id.pop(convertor_identifier, None) + @contextmanager def bulk_instances_collection(self): """Validate context of instances in bulk. @@ -1278,6 +1315,20 @@ class CreateContext: for creator in self.creators.values(): creator.collect_instances() + def find_legacy_items(self): + self.legacy_items_by_id = {} + + for convertor in self.legacy_convertors.values(): + try: + convertor.find_instances() + except: + self.log.warning( + "Failed to find instances of convertor \"{}\"".format( + convertor.identifier + ), + exc_info=True + ) + def execute_autocreators(self): """Execute discovered AutoCreator plugins. diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 62562e4428..ff9326693e 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -559,6 +559,10 @@ def discover_creator_plugins(): return discover(BaseCreator) +def discover_legacy_convertor_plugins(): + return discover(LegacyInstanceConvertor) + + def discover_legacy_creator_plugins(): from openpype.lib import Logger @@ -616,6 +620,9 @@ def register_creator_plugin(plugin): elif issubclass(plugin, LegacyCreator): register_plugin(LegacyCreator, plugin) + elif issubclass(plugin, LegacyInstanceConvertor): + register_plugin(LegacyInstanceConvertor, plugin) + def deregister_creator_plugin(plugin): if issubclass(plugin, BaseCreator): @@ -624,12 +631,17 @@ def deregister_creator_plugin(plugin): elif issubclass(plugin, LegacyCreator): deregister_plugin(LegacyCreator, plugin) + elif issubclass(plugin, LegacyInstanceConvertor): + deregister_plugin(LegacyInstanceConvertor, plugin) + def register_creator_plugin_path(path): register_plugin_path(BaseCreator, path) register_plugin_path(LegacyCreator, path) + register_plugin_path(LegacyInstanceConvertor, path) def deregister_creator_plugin_path(path): deregister_plugin_path(BaseCreator, path) deregister_plugin_path(LegacyCreator, path) + deregister_plugin_path(LegacyInstanceConvertor, path) From fe0ab169f7e8c25c6dcaf47323fd76078062170d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 15:34:43 +0200 Subject: [PATCH 014/160] import 'ApplicationManager' from lib --- openpype/modules/ftrack/ftrack_module.py | 2 +- openpype/plugins/load/open_djv.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index 75ffd7f864..678af0e577 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -195,7 +195,7 @@ class FtrackModule( app_definitions_from_app_manager, tool_definitions_from_app_manager ) - from openpype.api import ApplicationManager + from openpype.lib import ApplicationManager query_keys = [ "id", "key", diff --git a/openpype/plugins/load/open_djv.py b/openpype/plugins/load/open_djv.py index 273c77c93f..bc5fd64b87 100644 --- a/openpype/plugins/load/open_djv.py +++ b/openpype/plugins/load/open_djv.py @@ -1,5 +1,5 @@ import os -from openpype.api import ApplicationManager +from openpype.lib import ApplicationManager from openpype.pipeline import load From b5503372c0d41b6b4d3fb841112307b03d1955a7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 15:34:57 +0200 Subject: [PATCH 015/160] fix docstring import --- .../ftrack/event_handlers_server/event_user_assigment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_user_assigment.py b/openpype/modules/ftrack/event_handlers_server/event_user_assigment.py index 88d252e8cf..c4e48b92f0 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_user_assigment.py +++ b/openpype/modules/ftrack/event_handlers_server/event_user_assigment.py @@ -132,7 +132,7 @@ class UserAssigmentEvent(BaseEvent): """ Get data to fill template from task - .. seealso:: :mod:`openpype.api.Anatomy` + .. seealso:: :mod:`openpype.pipeline.Anatomy` :param task: Task entity :type task: dict From 24ebd76bd90ef5705434b6ff26c34f294ce96dc5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 17:55:56 +0200 Subject: [PATCH 016/160] fix convertor creation --- openpype/pipeline/create/context.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 783b599aef..5f39d7a0d0 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1195,9 +1195,7 @@ class CreateContext: )) continue - legacy_convertors[convertor_identifier] = ( - convertor_identifier(self) - ) + legacy_convertors[convertor_identifier] = convertor_class(self) self.legacy_convertors = legacy_convertors From 3bdaf89a791a88e0a8fed5f3938aad697b7d08d2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 17:56:03 +0200 Subject: [PATCH 017/160] added id to legacy item --- openpype/pipeline/create/context.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 5f39d7a0d0..e0c5e49e40 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -862,18 +862,26 @@ class LegacyInstancesItem(object): """ def __init__(self, identifier, label): + self._id = str(uuid4()) self.identifier = identifier self.label = label + @property + def id(self): + return self._id + def to_data(self): return { + "id": self.id, "identifier": self.identifier, "label": self.label } @classmethod def from_data(cls, data): - return cls(data["identifier"], data["label"]) + obj = cls(data["identifier"], data["label"]) + obj._id = data["id"] + return obj class CreateContext: From e484df219d6e9cf8031a6f1268575cc2060b75d1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 19:28:12 +0200 Subject: [PATCH 018/160] Define constant for context group --- openpype/tools/publisher/constants.py | 3 +++ .../tools/publisher/widgets/card_view_widgets.py | 15 ++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index dc44aade45..866792aa32 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -3,6 +3,9 @@ from Qt import QtCore # ID of context item in instance view CONTEXT_ID = "context" CONTEXT_LABEL = "Options" +# Not showed anywhere - used as identifier +CONTEXT_GROUP = "__ContextGroup__" + # Allowed symbols for subset name (and variant) # - characters, numbers, unsercore and dash diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 5daf8059b0..55e2249496 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -37,7 +37,8 @@ from .widgets import ( ) from ..constants import ( CONTEXT_ID, - CONTEXT_LABEL + CONTEXT_LABEL, + CONTEXT_GROUP, ) @@ -284,7 +285,7 @@ class ContextCardWidget(CardWidget): super(ContextCardWidget, self).__init__(parent) self._id = CONTEXT_ID - self._group_identifier = "" + self._group_identifier = CONTEXT_GROUP icon_widget = PublishPixmapLabel(None, self) icon_widget.setObjectName("FamilyIconLabel") @@ -595,7 +596,7 @@ class InstanceCardView(AbstractInstanceView): instances_by_group[group_name] ) - ordered_group_names = [""] + ordered_group_names = [CONTEXT_GROUP] for idx in range(self._content_layout.count()): if idx > 0: item = self._content_layout.itemAt(idx) @@ -749,7 +750,7 @@ class InstanceCardView(AbstractInstanceView): # If start group is not set then use context item group name if start_group is None: - start_group = "" + start_group = CONTEXT_GROUP # If start instance id is not filled then use context id (similar to # group) @@ -777,7 +778,7 @@ class InstanceCardView(AbstractInstanceView): # Go through ordered groups (from top to bottom) and change selection for name in self._ordered_groups: # Prepare sorted instance widgets - if name == "": + if name == CONTEXT_GROUP: sorted_widgets = [self._context_widget] else: group_widget = self._widgets_by_group[name] @@ -916,13 +917,13 @@ class InstanceCardView(AbstractInstanceView): selected_groups = [] selected_instances = [] if context_selected: - selected_groups.append("") + selected_groups.append(CONTEXT_GROUP) selected_instances.append(CONTEXT_ID) self._context_widget.set_selected(context_selected) for group_name in self._ordered_groups: - if group_name == "": + if group_name == CONTEXT_GROUP: continue group_widget = self._widgets_by_group[group_name] From 3a6bc00a5344c1e0a2124e5a62bda8bfa4d96a2d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 19:30:42 +0200 Subject: [PATCH 019/160] controller has access to convertor items --- openpype/tools/publisher/control.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index d2d01e7921..9abc53675d 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1234,6 +1234,14 @@ class AbstractPublisherController(object): pass + @abstractproperty + def legacy_items(self): + pass + + @abstractmethod + def convert_legacy_items(self, convertor_identifiers): + pass + @abstractmethod def set_comment(self, comment): """Set comment on pyblish context. @@ -1598,6 +1606,10 @@ class PublisherController(BasePublisherController): """Current instances in create context.""" return self._create_context.instances_by_id + @property + def legacy_items(self): + return self._create_context.legacy_items_by_id + @property def _creators(self): """All creators loaded in create context.""" @@ -1716,6 +1728,7 @@ class PublisherController(BasePublisherController): self._create_context.reset_context_data() with self._create_context.bulk_instances_collection(): self._create_context.reset_instances() + self._create_context.find_legacy_items() self._create_context.execute_autocreators() self._resetting_instances = False @@ -1841,6 +1854,12 @@ class PublisherController(BasePublisherController): variant, task_name, asset_doc, project_name, instance=instance ) + def convert_legacy_items(self, convertor_identifiers): + for convertor_identifier in convertor_identifiers: + self._create_context.run_convertor(convertor_identifier) + self._on_create_instance_change() + self.emit_card_message("Conversion finished") + def create( self, creator_identifier, subset_name, instance_data, options ): From b8e5e5e75f7ce5c85c702c757a65b2f6d9ed5e56 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 19:31:31 +0200 Subject: [PATCH 020/160] create context has function to run convertor --- openpype/pipeline/create/context.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index e0c5e49e40..250193f511 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1500,3 +1500,8 @@ class CreateContext: "Accessed Collection shared data out of collection phase" ) return self._collection_shared_data + + def run_convertor(self, convertor_identifier): + convertor = self.legacy_convertors.get(convertor_identifier) + if convertor is not None: + convertor.convert() From e19268c4a1606cff38ab018556bc63a261624578 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 19:31:57 +0200 Subject: [PATCH 021/160] implemented basic implementation of converter --- openpype/style/data.json | 5 +- openpype/style/style.css | 12 + openpype/tools/publisher/constants.py | 3 + .../publisher/widgets/card_view_widgets.py | 292 ++++++++++++---- .../publisher/widgets/list_view_widgets.py | 312 +++++++++++++----- .../publisher/widgets/overview_widget.py | 23 +- openpype/tools/publisher/widgets/widgets.py | 60 +++- 7 files changed, 538 insertions(+), 169 deletions(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index fef69071ed..44c0d51999 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -100,7 +100,10 @@ "bg-expander": "#2C313A", "bg-expander-hover": "#2d6c9f", "bg-expander-selected-hover": "#3784c5" - } + }, + "bg-legacy": "rgb(17, 17, 17)", + "bg-legacy-hover": "rgb(41, 41, 41)", + "bg-legacy-selected": "rgba(42, 123, 174, .4)" }, "settings": { "invalid-light": "#C93636", diff --git a/openpype/style/style.css b/openpype/style/style.css index a6818a5792..983f2c886f 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -965,6 +965,18 @@ VariantInputsWidget QToolButton { background: {color:bg-view-selection}; } +#CardViewLegacyItemWidget { + background: {color:publisher:bg-legacy}; + border-radius: 0.2em; + +} +#CardViewLegacyItemWidget:hover { + background: {color:publisher:bg-legacy-hover}; +} +#CardViewLegacyItemWidget[state="selected"] { + background: {color:publisher:bg-legacy-selected}; +} + #ListViewSubsetName[state="invalid"] { color: {color:publisher:error}; } diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index 866792aa32..3c192bf8a3 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -6,6 +6,7 @@ CONTEXT_LABEL = "Options" # Not showed anywhere - used as identifier CONTEXT_GROUP = "__ContextGroup__" +LEGACY_ITEM_GROUP = "Legacy instances" # Allowed symbols for subset name (and variant) # - characters, numbers, unsercore and dash @@ -20,6 +21,8 @@ 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 +LEGACY_CONVERTER_IDENTIFIER = QtCore.Qt.UserRole + 7 __all__ = ( diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 55e2249496..58a7bbc509 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -39,6 +39,7 @@ from ..constants import ( CONTEXT_ID, CONTEXT_LABEL, CONTEXT_GROUP, + LEGACY_ITEM_GROUP, ) @@ -58,15 +59,12 @@ class SelectionTypes: extend_to = SelectionType("extend_to") -class GroupWidget(QtWidgets.QWidget): - """Widget wrapping instances under group.""" - +class BaseGroupWidget(QtWidgets.QWidget): selected = QtCore.Signal(str, str, SelectionType) - active_changed = QtCore.Signal() removed_selected = QtCore.Signal() - def __init__(self, group_name, group_icons, parent): - super(GroupWidget, self).__init__(parent) + def __init__(self, group_name, parent): + super(BaseGroupWidget, self).__init__(parent) label_widget = QtWidgets.QLabel(group_name, self) @@ -87,10 +85,9 @@ class GroupWidget(QtWidgets.QWidget): layout.addLayout(label_layout, 0) self._group = group_name - self._group_icons = group_icons self._widgets_by_id = {} - self._ordered_instance_ids = [] + self._ordered_item_ids = [] self._label_widget = label_widget self._content_layout = layout @@ -105,7 +102,12 @@ class GroupWidget(QtWidgets.QWidget): return self._group - def get_selected_instance_ids(self): + def get_widget_by_item_id(self, item_id): + """Get instance widget by it's id.""" + + return self._widgets_by_id.get(item_id) + + def get_selected_item_ids(self): """Selected instance ids. Returns: @@ -140,13 +142,80 @@ class GroupWidget(QtWidgets.QWidget): return [ self._widgets_by_id[instance_id] - for instance_id in self._ordered_instance_ids + for instance_id in self._ordered_item_ids ] - def get_widget_by_instance_id(self, instance_id): - """Get instance widget by it's id.""" + def _remove_all_except(self, item_ids): + item_ids = set(item_ids) + # Remove instance widgets that are not in passed instances + for item_id in tuple(self._widgets_by_id.keys()): + if item_id in item_ids: + continue - return self._widgets_by_id.get(instance_id) + widget = self._widgets_by_id.pop(item_id) + if widget.is_selected: + self.removed_selected.emit() + + widget.setVisible(False) + self._content_layout.removeWidget(widget) + widget.deleteLater() + + def _update_ordered_item_ids(self): + ordered_item_ids = [] + for idx in range(self._content_layout.count()): + if idx > 0: + item = self._content_layout.itemAt(idx) + widget = item.widget() + if widget is not None: + ordered_item_ids.append(widget.id) + + self._ordered_item_ids = ordered_item_ids + + def _on_widget_selection(self, instance_id, group_id, selection_type): + self.selected.emit(instance_id, group_id, selection_type) + + +class LegacyItemsGroupWidget(BaseGroupWidget): + def update_items(self, items_by_id): + items_by_label = collections.defaultdict(list) + for item_id, item in items_by_id.items(): + items_by_label[item.label].append(item) + + # Remove instance widgets that are not in passed instances + self._remove_all_except(items_by_id.keys()) + + # Sort instances by subset name + sorted_labels = list(sorted(items_by_label.keys())) + + # Add new instances to widget + widget_idx = 1 + for label in sorted_labels: + for item in items_by_label[label]: + if item.id in self._widgets_by_id: + widget = self._widgets_by_id[item.id] + widget.update_item(item) + else: + widget = LegacyItemCardWidget(item, self) + widget.selected.connect(self._on_widget_selection) + self._widgets_by_id[item.id] = widget + self._content_layout.insertWidget(widget_idx, widget) + widget_idx += 1 + + self._update_ordered_item_ids() + + +class InstanceGroupWidget(BaseGroupWidget): + """Widget wrapping instances under group.""" + + active_changed = QtCore.Signal() + + def __init__(self, group_icons, *args, **kwargs): + super(InstanceGroupWidget, self).__init__(*args, **kwargs) + + self._group_icons = group_icons + + def update_icons(self, group_icons): + self._group_icons = group_icons def update_instance_values(self): """Trigger update on instance widgets.""" @@ -154,14 +223,6 @@ class GroupWidget(QtWidgets.QWidget): for widget in self._widgets_by_id.values(): widget.update_instance_values() - def confirm_remove_instance_id(self, instance_id): - """Delete widget by instance id.""" - - widget = self._widgets_by_id.pop(instance_id) - widget.setVisible(False) - self._content_layout.removeWidget(widget) - widget.deleteLater() - def update_instances(self, instances): """Update instances for the group. @@ -179,17 +240,7 @@ class GroupWidget(QtWidgets.QWidget): instances_by_subset_name[subset_name].append(instance) # Remove instance widgets that are not in passed instances - for instance_id in tuple(self._widgets_by_id.keys()): - if instance_id in instances_by_id: - continue - - widget = self._widgets_by_id.pop(instance_id) - if widget.is_selected: - self.removed_selected.emit() - - widget.setVisible(False) - self._content_layout.removeWidget(widget) - widget.deleteLater() + self._remove_all_except(instances_by_id.keys()) # Sort instances by subset name sorted_subset_names = list(sorted(instances_by_subset_name.keys())) @@ -212,18 +263,7 @@ class GroupWidget(QtWidgets.QWidget): self._content_layout.insertWidget(widget_idx, widget) widget_idx += 1 - ordered_instance_ids = [] - for idx in range(self._content_layout.count()): - if idx > 0: - item = self._content_layout.itemAt(idx) - widget = item.widget() - if widget is not None: - ordered_instance_ids.append(widget.id) - - self._ordered_instance_ids = ordered_instance_ids - - def _on_widget_selection(self, instance_id, group_id, selection_type): - self.selected.emit(instance_id, group_id, selection_type) + self._update_ordered_item_ids() class CardWidget(BaseClickableFrame): @@ -305,6 +345,41 @@ class ContextCardWidget(CardWidget): self._label_widget = label_widget +class LegacyItemCardWidget(CardWidget): + """Card for global context. + + Is not visually under group widget and is always at the top of card view. + """ + + def __init__(self, item, parent): + super(LegacyItemCardWidget, self).__init__(parent) + self.setObjectName("CardViewLegacyItemWidget") + + self._id = item.id + self.identifier = item.identifier + self._group_identifier = LEGACY_ITEM_GROUP + + icon_widget = PublishPixmapLabel(None, self) + icon_widget.setObjectName("FamilyIconLabel") + + label_widget = QtWidgets.QLabel(item.label, self) + + icon_layout = QtWidgets.QHBoxLayout() + icon_layout.setContentsMargins(5, 5, 5, 5) + icon_layout.addWidget(icon_widget) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 5, 10, 5) + layout.addLayout(icon_layout, 0) + layout.addWidget(label_widget, 1) + + self._icon_widget = icon_widget + self._label_widget = label_widget + + def update_instance_values(self): + pass + + class InstanceCardWidget(CardWidget): """Card widget representing instance.""" @@ -482,6 +557,7 @@ class InstanceCardView(AbstractInstanceView): self._content_widget = content_widget self._context_widget = None + self._legacy_items_group = None self._widgets_by_group = {} self._ordered_groups = [] @@ -514,6 +590,9 @@ class InstanceCardView(AbstractInstanceView): ): output.append(self._context_widget) + if self._legacy_items_group is not None: + output.extend(self._legacy_items_group.get_selected_widgets()) + for group_widget in self._widgets_by_group.values(): for widget in group_widget.get_selected_widgets(): output.append(widget) @@ -527,23 +606,19 @@ class InstanceCardView(AbstractInstanceView): ): output.append(CONTEXT_ID) + if self._legacy_items_group is not None: + output.extend(self._legacy_items_group.get_selected_item_ids()) + for group_widget in self._widgets_by_group.values(): - output.extend(group_widget.get_selected_instance_ids()) + output.extend(group_widget.get_selected_item_ids()) return output def refresh(self): """Refresh instances in view based on CreatedContext.""" - # Create context item if is not already existing - # - this must be as first thing to do as context item should be at the - # top - if self._context_widget is None: - widget = ContextCardWidget(self._content_widget) - widget.selected.connect(self._on_widget_selection) - self._context_widget = widget + self._make_sure_context_widget_exists() - self.selection_changed.emit() - self._content_layout.insertWidget(0, widget) + self._update_legacy_items_group() # Prepare instances by group and identifiers by group instances_by_group = collections.defaultdict(list) @@ -574,17 +649,21 @@ class InstanceCardView(AbstractInstanceView): # Keep track of widget indexes # - we start with 1 because Context item as at the top widget_idx = 1 + if self._legacy_items_group is not None: + widget_idx += 1 + for group_name in sorted_group_names: + group_icons = { + idenfier: self._controller.get_creator_icon(idenfier) + for idenfier in identifiers_by_group[group_name] + } if group_name in self._widgets_by_group: group_widget = self._widgets_by_group[group_name] - else: - group_icons = { - idenfier: self._controller.get_creator_icon(idenfier) - for idenfier in identifiers_by_group[group_name] - } + group_widget.update_icons(group_icons) - group_widget = GroupWidget( - group_name, group_icons, self._content_widget + else: + group_widget = InstanceGroupWidget( + group_icons, group_name, self._content_widget ) group_widget.active_changed.connect(self._on_active_changed) group_widget.selected.connect(self._on_widget_selection) @@ -596,6 +675,9 @@ class InstanceCardView(AbstractInstanceView): instances_by_group[group_name] ) + self._update_ordered_group_nameS() + + def _update_ordered_group_nameS(self): ordered_group_names = [CONTEXT_GROUP] for idx in range(self._content_layout.count()): if idx > 0: @@ -606,6 +688,43 @@ class InstanceCardView(AbstractInstanceView): self._ordered_groups = ordered_group_names + def _make_sure_context_widget_exists(self): + # Create context item if is not already existing + # - this must be as first thing to do as context item should be at the + # top + if self._context_widget is not None: + return + + widget = ContextCardWidget(self._content_widget) + widget.selected.connect(self._on_widget_selection) + + self._context_widget = widget + + self.selection_changed.emit() + self._content_layout.insertWidget(0, widget) + + def _update_legacy_items_group(self): + legacy_items = self._controller.legacy_items + if not legacy_items and self._legacy_items_group is None: + return + + if not legacy_items: + self._legacy_items_group.setVisible(False) + self._content_layout.removeWidget(self._legacy_items_group) + self._legacy_items_group.deleteLater() + self._legacy_items_group = None + return + + if self._legacy_items_group is None: + group_widget = LegacyItemsGroupWidget( + LEGACY_ITEM_GROUP, self._content_widget + ) + group_widget.selected.connect(self._on_widget_selection) + self._content_layout.insertWidget(1, group_widget) + self._legacy_items_group = group_widget + + self._legacy_items_group.update_items(legacy_items) + def refresh_instance_states(self): """Trigger update of instances on group widgets.""" for widget in self._widgets_by_group.values(): @@ -622,9 +741,13 @@ class InstanceCardView(AbstractInstanceView): """ if instance_id == CONTEXT_ID: new_widget = self._context_widget + else: - group_widget = self._widgets_by_group[group_name] - new_widget = group_widget.get_widget_by_instance_id(instance_id) + if group_name == LEGACY_ITEM_GROUP: + group_widget = self._legacy_items_group + else: + group_widget = self._widgets_by_group[group_name] + new_widget = group_widget.get_widget_by_item_id(instance_id) if selection_type is SelectionTypes.clear: self._select_item_clear(instance_id, group_name, new_widget) @@ -669,7 +792,10 @@ class InstanceCardView(AbstractInstanceView): if instance_id == CONTEXT_ID: remove_group = True else: - group_widget = self._widgets_by_group[group_name] + if group_name == LEGACY_ITEM_GROUP: + group_widget = self._legacy_items_group + else: + group_widget = self._widgets_by_group[group_name] if not group_widget.get_selected_widgets(): remove_group = True @@ -781,7 +907,10 @@ class InstanceCardView(AbstractInstanceView): if name == CONTEXT_GROUP: sorted_widgets = [self._context_widget] else: - group_widget = self._widgets_by_group[name] + if name == LEGACY_ITEM_GROUP: + group_widget = self._legacy_items_group + else: + group_widget = self._widgets_by_group[name] sorted_widgets = group_widget.get_ordered_widgets() # Change selection based on explicit selection if start group @@ -893,6 +1022,8 @@ class InstanceCardView(AbstractInstanceView): def get_selected_items(self): """Get selected instance ids and context.""" + + convertor_identifiers = [] instances = [] selected_widgets = self._get_selected_widgets() @@ -900,17 +1031,27 @@ class InstanceCardView(AbstractInstanceView): for widget in selected_widgets: if widget is self._context_widget: context_selected = True - else: + + elif isinstance(widget, InstanceCardWidget): instances.append(widget.id) - return instances, context_selected + elif isinstance(widget, LegacyItemCardWidget): + convertor_identifiers.append(widget.identifier) - def set_selected_items(self, instance_ids, context_selected): + return instances, context_selected, convertor_identifiers + + def set_selected_items( + self, instance_ids, context_selected, convertor_identifiers + ): s_instance_ids = set(instance_ids) - cur_ids, cur_context = self.get_selected_items() + s_convertor_identifiers = set(convertor_identifiers) + cur_ids, cur_context, cur_convertor_identifiers = ( + self.get_selected_items() + ) if ( set(cur_ids) == s_instance_ids and cur_context == context_selected + and set(cur_convertor_identifiers) == s_convertor_identifiers ): return @@ -926,11 +1067,20 @@ class InstanceCardView(AbstractInstanceView): if group_name == CONTEXT_GROUP: continue - group_widget = self._widgets_by_group[group_name] + legacy_group = group_name == LEGACY_ITEM_GROUP + if legacy_group: + group_widget = self._legacy_items_group + else: + group_widget = self._widgets_by_group[group_name] + group_selected = False for widget in group_widget.get_ordered_widgets(): select = False - if widget.id in s_instance_ids: + if legacy_group: + is_in = widget.identifier in s_convertor_identifiers + else: + is_in = widget.id in s_instance_ids + if is_in: selected_instances.append(widget.id) group_selected = True select = True diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index c329ca0e8c..df07470f1d 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -35,7 +35,10 @@ from ..constants import ( SORT_VALUE_ROLE, IS_GROUP_ROLE, CONTEXT_ID, - CONTEXT_LABEL + CONTEXT_LABEL, + GROUP_ROLE, + LEGACY_CONVERTER_IDENTIFIER, + LEGACY_ITEM_GROUP, ) @@ -330,6 +333,9 @@ class InstanceTreeView(QtWidgets.QTreeView): """Ids of selected instances.""" instance_ids = set() for index in self.selectionModel().selectedIndexes(): + if index.data(LEGACY_CONVERTER_IDENTIFIER) is not None: + continue + instance_id = index.data(INSTANCE_ID_ROLE) if instance_id is not None: instance_ids.add(instance_id) @@ -439,26 +445,36 @@ class InstanceListView(AbstractInstanceView): self._group_items = {} self._group_widgets = {} self._widgets_by_id = {} + # Group by instance id for handling of active state self._group_by_instance_id = {} self._context_item = None self._context_widget = None + self._legacy_group_item = None + self._legacy_group_widget = None + self._legacy_widgets_by_id = {} + self._legacy_items_by_id = {} + self._instance_view = instance_view self._instance_delegate = instance_delegate self._instance_model = instance_model self._proxy_model = proxy_model def _on_expand(self, index): - group_name = index.data(SORT_VALUE_ROLE) - group_widget = self._group_widgets.get(group_name) - if group_widget: - group_widget.set_expanded(True) + self._update_widget_expand_state(index, True) def _on_collapse(self, index): - group_name = index.data(SORT_VALUE_ROLE) - group_widget = self._group_widgets.get(group_name) + self._update_widget_expand_state(index, False) + + def _update_widget_expand_state(self, index, expanded): + group_name = index.data(GROUP_ROLE) + if group_name == LEGACY_ITEM_GROUP: + group_widget = self._legacy_group_widget + else: + group_widget = self._group_widgets.get(group_name) + if group_widget: - group_widget.set_expanded(False) + group_widget.set_expanded(expanded) def _on_toggle_request(self, toggle): selected_instance_ids = self._instance_view.get_selected_instance_ids() @@ -517,6 +533,16 @@ class InstanceListView(AbstractInstanceView): def refresh(self): """Refresh instances in the view.""" + # Sort view at the end of refresh + # - is turned off until any change in view happens + sort_at_the_end = False + # Create or use already existing context item + # - context widget does not change so we don't have to update anything + if self._make_sure_context_item_exists(): + sort_at_the_end = True + + self._update_legacy_items_group() + # Prepare instances by their groups instances_by_group_name = collections.defaultdict(list) group_names = set() @@ -525,75 +551,12 @@ class InstanceListView(AbstractInstanceView): group_names.add(group_label) instances_by_group_name[group_label].append(instance) - # Sort view at the end of refresh - # - is turned off until any change in view happens - sort_at_the_end = False - - # Access to root item of main model - root_item = self._instance_model.invisibleRootItem() - - # Create or use already existing context item - # - context widget does not change so we don't have to update anything - context_item = None - if self._context_item is None: - sort_at_the_end = True - context_item = QtGui.QStandardItem() - context_item.setData(0, SORT_VALUE_ROLE) - context_item.setData(CONTEXT_ID, INSTANCE_ID_ROLE) - - root_item.appendRow(context_item) - - index = self._instance_model.index( - context_item.row(), context_item.column() - ) - proxy_index = self._proxy_model.mapFromSource(index) - widget = ListContextWidget(self._instance_view) - self._instance_view.setIndexWidget(proxy_index, widget) - - self._context_widget = widget - self._context_item = context_item - # Create new groups based on prepared `instances_by_group_name` - new_group_items = [] - for group_name in group_names: - if group_name in self._group_items: - continue - - group_item = QtGui.QStandardItem() - group_item.setData(group_name, SORT_VALUE_ROLE) - group_item.setData(True, IS_GROUP_ROLE) - group_item.setFlags(QtCore.Qt.ItemIsEnabled) - self._group_items[group_name] = group_item - new_group_items.append(group_item) - - # Add new group items to root item if there are any - if new_group_items: - # Trigger sort at the end + if self._make_sure_groups_exists(group_names): sort_at_the_end = True - root_item.appendRows(new_group_items) - - # Create widget for each new group item and store it for future usage - for group_item in new_group_items: - index = self._instance_model.index( - group_item.row(), group_item.column() - ) - proxy_index = self._proxy_model.mapFromSource(index) - group_name = group_item.data(SORT_VALUE_ROLE) - widget = InstanceListGroupWidget(group_name, self._instance_view) - widget.expand_changed.connect(self._on_group_expand_request) - widget.toggle_requested.connect(self._on_group_toggle_request) - self._group_widgets[group_name] = widget - self._instance_view.setIndexWidget(proxy_index, widget) # Remove groups that are not available anymore - for group_name in tuple(self._group_items.keys()): - if group_name in group_names: - continue - - group_item = self._group_items.pop(group_name) - root_item.removeRow(group_item.row()) - widget = self._group_widgets.pop(group_name) - widget.deleteLater() + self._remove_groups_except(group_names) # Store which groups should be expanded at the end expand_groups = set() @@ -652,6 +615,7 @@ class InstanceListView(AbstractInstanceView): # Create new item and store it as new item = QtGui.QStandardItem() item.setData(instance["subset"], SORT_VALUE_ROLE) + item.setData(instance["subset"], GROUP_ROLE) item.setData(instance_id, INSTANCE_ID_ROLE) new_items.append(item) new_items_with_instance.append((item, instance)) @@ -717,13 +681,147 @@ class InstanceListView(AbstractInstanceView): self._instance_view.expand(proxy_index) + def _make_sure_context_item_exists(self): + if self._context_item is not None: + return False + + root_item = self._instance_model.invisibleRootItem() + context_item = QtGui.QStandardItem() + context_item.setData(0, SORT_VALUE_ROLE) + context_item.setData(CONTEXT_ID, INSTANCE_ID_ROLE) + + root_item.appendRow(context_item) + + index = self._instance_model.index( + context_item.row(), context_item.column() + ) + proxy_index = self._proxy_model.mapFromSource(index) + widget = ListContextWidget(self._instance_view) + self._instance_view.setIndexWidget(proxy_index, widget) + + self._context_widget = widget + self._context_item = context_item + return True + + def _update_legacy_items_group(self): + created_new_items = False + legacy_items_by_id = self._controller.legacy_items + group_item = self._legacy_group_item + if not legacy_items_by_id and group_item is None: + return created_new_items + + root_item = self._instance_model.invisibleRootItem() + if not legacy_items_by_id: + root_item.removeRow(group_item.row()) + self._legacy_group_widget.deleteLater() + self._legacy_group_widget = None + return created_new_items + + if group_item is None: + created_new_items = True + group_item = QtGui.QStandardItem() + group_item.setData(LEGACY_ITEM_GROUP, GROUP_ROLE) + group_item.setData(1, SORT_VALUE_ROLE) + group_item.setData(True, IS_GROUP_ROLE) + group_item.setFlags(QtCore.Qt.ItemIsEnabled) + + root_item.appendRow(group_item) + + index = self._instance_model.index( + group_item.row(), group_item.column() + ) + proxy_index = self._proxy_model.mapFromSource(index) + widget = InstanceListGroupWidget( + LEGACY_ITEM_GROUP, self._instance_view + ) + widget.toggle_checkbox.setVisible(False) + widget.expand_changed.connect(self._on_legacy_group_expand_request) + self._instance_view.setIndexWidget(proxy_index, widget) + + self._legacy_group_item = group_item + self._legacy_group_widget = widget + + for row in reversed(range(group_item.rowCount())): + child_item = group_item.child(row) + child_identifier = child_item.data(LEGACY_CONVERTER_IDENTIFIER) + if child_identifier not in legacy_items_by_id: + group_item.removeRows(row, 1) + + new_items = [] + for identifier, convertor_item in legacy_items_by_id.items(): + item = self._legacy_items_by_id.get(identifier) + if item is None: + created_new_items = True + item = QtGui.QStandardItem(convertor_item.label) + new_items.append(item) + item.setData(convertor_item.id, INSTANCE_ID_ROLE) + item.setData(convertor_item.label, SORT_VALUE_ROLE) + item.setData(LEGACY_ITEM_GROUP, GROUP_ROLE) + item.setData( + convertor_item.identifier, LEGACY_CONVERTER_IDENTIFIER + ) + + if new_items: + group_item.appendRows(new_items) + + return created_new_items + + def _make_sure_groups_exists(self, group_names): + new_group_items = [] + for group_name in group_names: + if group_name in self._group_items: + continue + + group_item = QtGui.QStandardItem() + group_item.setData(group_name, GROUP_ROLE) + group_item.setData(group_name, SORT_VALUE_ROLE) + group_item.setData(True, IS_GROUP_ROLE) + group_item.setFlags(QtCore.Qt.ItemIsEnabled) + self._group_items[group_name] = group_item + new_group_items.append(group_item) + + # Add new group items to root item if there are any + if not new_group_items: + return False + + # Access to root item of main model + root_item = self._instance_model.invisibleRootItem() + root_item.appendRows(new_group_items) + + # Create widget for each new group item and store it for future usage + for group_item in new_group_items: + index = self._instance_model.index( + group_item.row(), group_item.column() + ) + proxy_index = self._proxy_model.mapFromSource(index) + group_name = group_item.data(GROUP_ROLE) + widget = InstanceListGroupWidget(group_name, self._instance_view) + widget.expand_changed.connect(self._on_group_expand_request) + widget.toggle_requested.connect(self._on_group_toggle_request) + self._group_widgets[group_name] = widget + self._instance_view.setIndexWidget(proxy_index, widget) + + return True + + def _remove_groups_except(self, group_names): + # Remove groups that are not available anymore + root_item = self._instance_model.invisibleRootItem() + for group_name in tuple(self._group_items.keys()): + if group_name in group_names: + continue + + group_item = self._group_items.pop(group_name) + root_item.removeRow(group_item.row()) + widget = self._group_widgets.pop(group_name) + widget.deleteLater() + def refresh_instance_states(self): """Trigger update of all instances.""" for widget in self._widgets_by_id.values(): widget.update_instance_values() def _on_active_changed(self, changed_instance_id, new_value): - selected_instance_ids, _ = self.get_selected_items() + selected_instance_ids, _, _ = self.get_selected_items() selected_ids = set() found = False @@ -774,6 +872,16 @@ class InstanceListView(AbstractInstanceView): proxy_index = self._proxy_model.mapFromSource(group_index) self._instance_view.setExpanded(proxy_index, expanded) + def _on_legacy_group_expand_request(self, _, expanded): + group_item = self._legacy_group_item + if not group_item: + return + group_index = self._instance_model.index( + group_item.row(), group_item.column() + ) + proxy_index = self._proxy_model.mapFromSource(group_index) + self._instance_view.setExpanded(proxy_index, expanded) + def _on_group_toggle_request(self, group_name, state): if state == QtCore.Qt.PartiallyChecked: return @@ -807,10 +915,17 @@ class InstanceListView(AbstractInstanceView): tuple: Selected instance ids and boolean if context is selected. """ + instance_ids = [] + convertor_identifiers = [] context_selected = False for index in self._instance_view.selectionModel().selectedIndexes(): + convertor_identifier = index.data(LEGACY_CONVERTER_IDENTIFIER) + if convertor_identifier is not None: + convertor_identifiers.append(convertor_identifier) + continue + instance_id = index.data(INSTANCE_ID_ROLE) if not context_selected and instance_id == CONTEXT_ID: context_selected = True @@ -818,14 +933,20 @@ class InstanceListView(AbstractInstanceView): elif instance_id is not None: instance_ids.append(instance_id) - return instance_ids, context_selected + return instance_ids, context_selected, convertor_identifiers - def set_selected_items(self, instance_ids, context_selected): + def set_selected_items( + self, instance_ids, context_selected, convertor_identifiers + ): s_instance_ids = set(instance_ids) - cur_ids, cur_context = self.get_selected_items() + s_convertor_identifiers = set(convertor_identifiers) + cur_ids, cur_context, cur_convertor_identifiers = ( + self.get_selected_items() + ) if ( set(cur_ids) == s_instance_ids and cur_context == context_selected + and set(cur_convertor_identifiers) == s_convertor_identifiers ): return @@ -851,20 +972,35 @@ class InstanceListView(AbstractInstanceView): (item.child(row), list(new_parent_items)) ) - instance_id = item.data(INSTANCE_ID_ROLE) - if not instance_id: + convertor_identifier = item.data(LEGACY_CONVERTER_IDENTIFIER) + + select = False + expand_parent = True + if convertor_identifier is not None: + if convertor_identifier in s_convertor_identifiers: + select = True + else: + instance_id = item.data(INSTANCE_ID_ROLE) + if instance_id == CONTEXT_ID: + if context_selected: + select = True + expand_parent = False + + elif instance_id in s_instance_ids: + select = True + + if not select: continue - if instance_id in s_instance_ids: - select_indexes.append(item.index()) - for parent_item in parent_items: - index = parent_item.index() - proxy_index = proxy_model.mapFromSource(index) - if not view.isExpanded(proxy_index): - view.expand(proxy_index) + select_indexes.append(item.index()) + if not expand_parent: + continue - elif context_selected and instance_id == CONTEXT_ID: - select_indexes.append(item.index()) + for parent_item in parent_items: + index = parent_item.index() + proxy_index = proxy_model.mapFromSource(index) + if not view.isExpanded(proxy_index): + view.expand(proxy_index) selection_model = view.selectionModel() if not select_indexes: diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 5bd3017c2a..e208786fc7 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -124,6 +124,9 @@ class OverviewWidget(QtWidgets.QFrame): subset_attributes_widget.instance_context_changed.connect( self._on_instance_context_change ) + subset_attributes_widget.convert_requested.connect( + self._on_convert_requested + ) # --- Controller callbacks --- controller.event_system.add_callback( @@ -201,7 +204,7 @@ class OverviewWidget(QtWidgets.QFrame): self.create_requested.emit() def _on_delete_clicked(self): - instance_ids, _ = self.get_selected_items() + instance_ids, _, _ = self.get_selected_items() # Ask user if he really wants to remove instances dialog = QtWidgets.QMessageBox(self) @@ -235,7 +238,9 @@ class OverviewWidget(QtWidgets.QFrame): if self._refreshing_instances: return - instance_ids, context_selected = self.get_selected_items() + instance_ids, context_selected, convertor_identifiers = ( + self.get_selected_items() + ) # Disable delete button if nothing is selected self._delete_btn.setEnabled(len(instance_ids) > 0) @@ -246,7 +251,7 @@ class OverviewWidget(QtWidgets.QFrame): for instance_id in instance_ids ] self._subset_attributes_widget.set_current_instances( - instances, context_selected + instances, context_selected, convertor_identifiers ) def _on_active_changed(self): @@ -314,6 +319,10 @@ class OverviewWidget(QtWidgets.QFrame): self.instance_context_changed.emit() + def _on_convert_requested(self): + _, _, convertor_identifiers = self.get_selected_items() + self._controller.convert_legacy_items(convertor_identifiers) + def get_selected_items(self): view = self._subset_views_layout.currentWidget() return view.get_selected_items() @@ -331,8 +340,12 @@ class OverviewWidget(QtWidgets.QFrame): else: new_view.refresh_instance_states() - instance_ids, context_selected = old_view.get_selected_items() - new_view.set_selected_items(instance_ids, context_selected) + instance_ids, context_selected, convertor_identifiers = ( + old_view.get_selected_items() + ) + new_view.set_selected_items( + instance_ids, context_selected, convertor_identifiers + ) self._subset_views_layout.setCurrentIndex(new_idx) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index ddbe1eb6b7..b01fed25a5 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1461,6 +1461,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget): └───────────────────────────────┘ """ instance_context_changed = QtCore.Signal() + convert_requested = QtCore.Signal() def __init__(self, controller, parent): super(SubsetAttributesWidget, self).__init__(parent) @@ -1479,9 +1480,48 @@ class SubsetAttributesWidget(QtWidgets.QWidget): # BOTTOM PART bottom_widget = QtWidgets.QWidget(self) - creator_attrs_widget = CreatorAttrsWidget( - controller, bottom_widget + + # Wrap Creator attributes to widget to be able add convert button + creator_widget = QtWidgets.QWidget(bottom_widget) + + # Convert button widget (with layout to handle stretch) + convert_widget = QtWidgets.QWidget(creator_widget) + convert_label = QtWidgets.QLabel( + ( + "Found instances created with legacy creators." + "\nDo you with to convert them?" + ), + creator_widget ) + convert_label.setWordWrap(True) + convert_label.setAlignment(QtCore.Qt.AlignCenter) + + convert_btn = QtWidgets.QPushButton( + "Convert legacy instances", convert_widget + ) + convert_separator = QtWidgets.QFrame(convert_widget) + convert_separator.setObjectName("Separator") + convert_separator.setMinimumHeight(2) + convert_separator.setMaximumHeight(2) + + convert_layout = QtWidgets.QGridLayout(convert_widget) + convert_layout.setContentsMargins(0, 0, 0, 0) + convert_layout.addWidget(convert_label, 0, 0, 1, 3) + convert_layout.addWidget(convert_btn, 1, 1) + convert_layout.addWidget(convert_separator, 2, 0, 1, 3) + convert_layout.setColumnStretch(0, 1) + convert_layout.setColumnStretch(1, 0) + convert_layout.setColumnStretch(2, 1) + + # Creator attributes widget + creator_attrs_widget = CreatorAttrsWidget( + controller, creator_widget + ) + creator_layout = QtWidgets.QVBoxLayout(creator_widget) + creator_layout.setContentsMargins(0, 0, 0, 0) + creator_layout.addWidget(convert_widget, 0) + creator_layout.addWidget(creator_attrs_widget, 1) + publish_attrs_widget = PublishPluginAttrsWidget( controller, bottom_widget ) @@ -1492,7 +1532,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget): bottom_layout = QtWidgets.QHBoxLayout(bottom_widget) bottom_layout.setContentsMargins(0, 0, 0, 0) - bottom_layout.addWidget(creator_attrs_widget, 1) + bottom_layout.addWidget(creator_widget, 1) bottom_layout.addWidget(bottom_separator, 0) bottom_layout.addWidget(publish_attrs_widget, 1) @@ -1505,6 +1545,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget): layout.addWidget(top_bottom, 0) layout.addWidget(bottom_widget, 1) + self._convertor_identifiers = None self._current_instances = None self._context_selected = False self._all_instances_valid = True @@ -1512,9 +1553,12 @@ class SubsetAttributesWidget(QtWidgets.QWidget): global_attrs_widget.instance_context_changed.connect( self._on_instance_context_changed ) + convert_btn.clicked.connect(self._on_convert_click) self._controller = controller + self._convert_widget = convert_widget + self.global_attrs_widget = global_attrs_widget self.creator_attrs_widget = creator_attrs_widget @@ -1537,7 +1581,12 @@ class SubsetAttributesWidget(QtWidgets.QWidget): self.instance_context_changed.emit() - def set_current_instances(self, instances, context_selected): + def _on_convert_click(self): + self.convert_requested.emit() + + def set_current_instances( + self, instances, context_selected, convertor_identifiers + ): """Change currently selected items. Args: @@ -1551,10 +1600,13 @@ class SubsetAttributesWidget(QtWidgets.QWidget): all_valid = False break + s_convertor_identifiers = set(convertor_identifiers) + self._convertor_identifiers = s_convertor_identifiers self._current_instances = instances self._context_selected = context_selected self._all_instances_valid = all_valid + self._convert_widget.setVisible(len(s_convertor_identifiers) > 0) self.global_attrs_widget.set_current_instances(instances) self.creator_attrs_widget.set_current_instances(instances) self.publish_attrs_widget.set_current_instances( From 45c944816c42d2593b61fc18f78ca321e6b3d120 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 21 Oct 2022 19:45:17 +0200 Subject: [PATCH 022/160] removed unused variable --- openpype/tools/publisher/widgets/card_view_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 58a7bbc509..96802087ee 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -178,7 +178,7 @@ class BaseGroupWidget(QtWidgets.QWidget): class LegacyItemsGroupWidget(BaseGroupWidget): def update_items(self, items_by_id): items_by_label = collections.defaultdict(list) - for item_id, item in items_by_id.items(): + for item in items_by_id.values(): items_by_label[item.label].append(item) # Remove instance widgets that are not in passed instances From 245c5e9afb81f231bcc884f5c503ae6c812421b8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 17:16:45 +0200 Subject: [PATCH 023/160] changed label of legacy group --- openpype/tools/publisher/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index 3c192bf8a3..e5969160c1 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -6,7 +6,7 @@ CONTEXT_LABEL = "Options" # Not showed anywhere - used as identifier CONTEXT_GROUP = "__ContextGroup__" -LEGACY_ITEM_GROUP = "Legacy instances" +LEGACY_ITEM_GROUP = "Incompatible subsets" # Allowed symbols for subset name (and variant) # - characters, numbers, unsercore and dash From 080deda3167c2b40fcd10582b6c4e99498cf2ff1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 17:18:06 +0200 Subject: [PATCH 024/160] fix list view update --- openpype/tools/publisher/widgets/list_view_widgets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index df07470f1d..53951e3cba 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -452,7 +452,6 @@ class InstanceListView(AbstractInstanceView): self._legacy_group_item = None self._legacy_group_widget = None - self._legacy_widgets_by_id = {} self._legacy_items_by_id = {} self._instance_view = instance_view @@ -715,6 +714,7 @@ class InstanceListView(AbstractInstanceView): root_item.removeRow(group_item.row()) self._legacy_group_widget.deleteLater() self._legacy_group_widget = None + self._legacy_items_by_id = {} return created_new_items if group_item is None: @@ -745,6 +745,7 @@ class InstanceListView(AbstractInstanceView): child_item = group_item.child(row) child_identifier = child_item.data(LEGACY_CONVERTER_IDENTIFIER) if child_identifier not in legacy_items_by_id: + self._legacy_items_by_id.pop(child_identifier, None) group_item.removeRows(row, 1) new_items = [] @@ -760,6 +761,7 @@ class InstanceListView(AbstractInstanceView): item.setData( convertor_item.identifier, LEGACY_CONVERTER_IDENTIFIER ) + self._legacy_items_by_id[identifier] = item if new_items: group_item.appendRows(new_items) From 2787351f03aa7eb7c0220a8f60ba85e4b6a91166 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 17:18:23 +0200 Subject: [PATCH 025/160] change labels of the message for user --- openpype/tools/publisher/widgets/widgets.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index b01fed25a5..ec63509dfa 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1486,18 +1486,22 @@ class SubsetAttributesWidget(QtWidgets.QWidget): # Convert button widget (with layout to handle stretch) convert_widget = QtWidgets.QWidget(creator_widget) - convert_label = QtWidgets.QLabel( + convert_label = QtWidgets.QLabel(creator_widget) + # Set the label text with 'setText' to apply html + convert_label.setText( ( - "Found instances created with legacy creators." - "\nDo you with to convert them?" - ), - creator_widget + "Found old publishable subsets" + " incompatible with new publisher." + "

Press the update subsets button" + " to automatically update them" + " to be able to publish again." + ) ) convert_label.setWordWrap(True) convert_label.setAlignment(QtCore.Qt.AlignCenter) convert_btn = QtWidgets.QPushButton( - "Convert legacy instances", convert_widget + "Update subsets", convert_widget ) convert_separator = QtWidgets.QFrame(convert_widget) convert_separator.setObjectName("Separator") From e94cd00ad7adc36adf48f9c05a752e94611778d3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 17:18:31 +0200 Subject: [PATCH 026/160] change separator size --- openpype/tools/publisher/widgets/widgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index ec63509dfa..e091e76fab 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1505,8 +1505,8 @@ class SubsetAttributesWidget(QtWidgets.QWidget): ) convert_separator = QtWidgets.QFrame(convert_widget) convert_separator.setObjectName("Separator") - convert_separator.setMinimumHeight(2) - convert_separator.setMaximumHeight(2) + convert_separator.setMinimumHeight(1) + convert_separator.setMaximumHeight(1) convert_layout = QtWidgets.QGridLayout(convert_widget) convert_layout.setContentsMargins(0, 0, 0, 0) From a98085704ff5a2ebaa205b715bf72024bea0e6bd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 17:21:39 +0200 Subject: [PATCH 027/160] added some padding and spacing --- openpype/tools/publisher/widgets/widgets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index e091e76fab..d4c2623790 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1509,7 +1509,8 @@ class SubsetAttributesWidget(QtWidgets.QWidget): convert_separator.setMaximumHeight(1) convert_layout = QtWidgets.QGridLayout(convert_widget) - convert_layout.setContentsMargins(0, 0, 0, 0) + convert_layout.setContentsMargins(5, 0, 5, 0) + convert_layout.setVerticalSpacing(10) convert_layout.addWidget(convert_label, 0, 0, 1, 3) convert_layout.addWidget(convert_btn, 1, 1) convert_layout.addWidget(convert_separator, 2, 0, 1, 3) From 271a0056bcd988a2371124879e86805cc379cbca Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 17:31:38 +0200 Subject: [PATCH 028/160] change the item look --- openpype/style/data.json | 5 +---- openpype/style/style.css | 12 ------------ .../tools/publisher/widgets/card_view_widgets.py | 5 ++--- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index 44c0d51999..fef69071ed 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -100,10 +100,7 @@ "bg-expander": "#2C313A", "bg-expander-hover": "#2d6c9f", "bg-expander-selected-hover": "#3784c5" - }, - "bg-legacy": "rgb(17, 17, 17)", - "bg-legacy-hover": "rgb(41, 41, 41)", - "bg-legacy-selected": "rgba(42, 123, 174, .4)" + } }, "settings": { "invalid-light": "#C93636", diff --git a/openpype/style/style.css b/openpype/style/style.css index 983f2c886f..a6818a5792 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -965,18 +965,6 @@ VariantInputsWidget QToolButton { background: {color:bg-view-selection}; } -#CardViewLegacyItemWidget { - background: {color:publisher:bg-legacy}; - border-radius: 0.2em; - -} -#CardViewLegacyItemWidget:hover { - background: {color:publisher:bg-legacy-hover}; -} -#CardViewLegacyItemWidget[state="selected"] { - background: {color:publisher:bg-legacy-selected}; -} - #ListViewSubsetName[state="invalid"] { color: {color:publisher:error}; } diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 96802087ee..95fa8cd5d2 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -353,19 +353,18 @@ class LegacyItemCardWidget(CardWidget): def __init__(self, item, parent): super(LegacyItemCardWidget, self).__init__(parent) - self.setObjectName("CardViewLegacyItemWidget") self._id = item.id self.identifier = item.identifier self._group_identifier = LEGACY_ITEM_GROUP - icon_widget = PublishPixmapLabel(None, self) + icon_widget = IconValuePixmapLabel("fa.magic", self) icon_widget.setObjectName("FamilyIconLabel") label_widget = QtWidgets.QLabel(item.label, self) icon_layout = QtWidgets.QHBoxLayout() - icon_layout.setContentsMargins(5, 5, 5, 5) + icon_layout.setContentsMargins(10, 5, 5, 5) icon_layout.addWidget(icon_widget) layout = QtWidgets.QHBoxLayout(self) From 7afb2b2e9fea0ca8cc4fd3d48c16069d052c50df Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 17:56:27 +0200 Subject: [PATCH 029/160] change variable to use convertor instead of legacy --- openpype/tools/publisher/constants.py | 4 +- openpype/tools/publisher/control.py | 4 +- .../publisher/widgets/card_view_widgets.py | 74 +++++++++---------- .../publisher/widgets/list_view_widgets.py | 70 +++++++++--------- 4 files changed, 77 insertions(+), 75 deletions(-) diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index e5969160c1..8bea69c812 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -6,7 +6,7 @@ CONTEXT_LABEL = "Options" # Not showed anywhere - used as identifier CONTEXT_GROUP = "__ContextGroup__" -LEGACY_ITEM_GROUP = "Incompatible subsets" +CONVERTOR_ITEM_GROUP = "Incompatible subsets" # Allowed symbols for subset name (and variant) # - characters, numbers, unsercore and dash @@ -22,7 +22,7 @@ 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 -LEGACY_CONVERTER_IDENTIFIER = QtCore.Qt.UserRole + 7 +CONVERTER_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 7 __all__ = ( diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 9abc53675d..b867bddc9d 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1235,7 +1235,7 @@ class AbstractPublisherController(object): pass @abstractproperty - def legacy_items(self): + def convertor_items(self): pass @abstractmethod @@ -1607,7 +1607,7 @@ class PublisherController(BasePublisherController): return self._create_context.instances_by_id @property - def legacy_items(self): + def convertor_items(self): return self._create_context.legacy_items_by_id @property diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 95fa8cd5d2..9fd2bf0824 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -39,7 +39,7 @@ from ..constants import ( CONTEXT_ID, CONTEXT_LABEL, CONTEXT_GROUP, - LEGACY_ITEM_GROUP, + CONVERTOR_ITEM_GROUP, ) @@ -175,7 +175,7 @@ class BaseGroupWidget(QtWidgets.QWidget): self.selected.emit(instance_id, group_id, selection_type) -class LegacyItemsGroupWidget(BaseGroupWidget): +class ConvertorItemsGroupWidget(BaseGroupWidget): def update_items(self, items_by_id): items_by_label = collections.defaultdict(list) for item in items_by_id.values(): @@ -195,7 +195,7 @@ class LegacyItemsGroupWidget(BaseGroupWidget): widget = self._widgets_by_id[item.id] widget.update_item(item) else: - widget = LegacyItemCardWidget(item, self) + widget = ConvertorItemCardWidget(item, self) widget.selected.connect(self._on_widget_selection) self._widgets_by_id[item.id] = widget self._content_layout.insertWidget(widget_idx, widget) @@ -345,18 +345,18 @@ class ContextCardWidget(CardWidget): self._label_widget = label_widget -class LegacyItemCardWidget(CardWidget): +class ConvertorItemCardWidget(CardWidget): """Card for global context. Is not visually under group widget and is always at the top of card view. """ def __init__(self, item, parent): - super(LegacyItemCardWidget, self).__init__(parent) + super(ConvertorItemCardWidget, self).__init__(parent) self._id = item.id self.identifier = item.identifier - self._group_identifier = LEGACY_ITEM_GROUP + self._group_identifier = CONVERTOR_ITEM_GROUP icon_widget = IconValuePixmapLabel("fa.magic", self) icon_widget.setObjectName("FamilyIconLabel") @@ -556,7 +556,7 @@ class InstanceCardView(AbstractInstanceView): self._content_widget = content_widget self._context_widget = None - self._legacy_items_group = None + self._convertor_items_group = None self._widgets_by_group = {} self._ordered_groups = [] @@ -589,8 +589,8 @@ class InstanceCardView(AbstractInstanceView): ): output.append(self._context_widget) - if self._legacy_items_group is not None: - output.extend(self._legacy_items_group.get_selected_widgets()) + if self._convertor_items_group is not None: + output.extend(self._convertor_items_group.get_selected_widgets()) for group_widget in self._widgets_by_group.values(): for widget in group_widget.get_selected_widgets(): @@ -605,8 +605,8 @@ class InstanceCardView(AbstractInstanceView): ): output.append(CONTEXT_ID) - if self._legacy_items_group is not None: - output.extend(self._legacy_items_group.get_selected_item_ids()) + if self._convertor_items_group is not None: + output.extend(self._convertor_items_group.get_selected_item_ids()) for group_widget in self._widgets_by_group.values(): output.extend(group_widget.get_selected_item_ids()) @@ -617,7 +617,7 @@ class InstanceCardView(AbstractInstanceView): self._make_sure_context_widget_exists() - self._update_legacy_items_group() + self._update_convertor_items_group() # Prepare instances by group and identifiers by group instances_by_group = collections.defaultdict(list) @@ -648,7 +648,7 @@ class InstanceCardView(AbstractInstanceView): # Keep track of widget indexes # - we start with 1 because Context item as at the top widget_idx = 1 - if self._legacy_items_group is not None: + if self._convertor_items_group is not None: widget_idx += 1 for group_name in sorted_group_names: @@ -702,27 +702,27 @@ class InstanceCardView(AbstractInstanceView): self.selection_changed.emit() self._content_layout.insertWidget(0, widget) - def _update_legacy_items_group(self): - legacy_items = self._controller.legacy_items - if not legacy_items and self._legacy_items_group is None: + def _update_convertor_items_group(self): + convertor_items = self._controller.convertor_items + if not convertor_items and self._convertor_items_group is None: return - if not legacy_items: - self._legacy_items_group.setVisible(False) - self._content_layout.removeWidget(self._legacy_items_group) - self._legacy_items_group.deleteLater() - self._legacy_items_group = None + if not convertor_items: + self._convertor_items_group.setVisible(False) + self._content_layout.removeWidget(self._convertor_items_group) + self._convertor_items_group.deleteLater() + self._convertor_items_group = None return - if self._legacy_items_group is None: - group_widget = LegacyItemsGroupWidget( - LEGACY_ITEM_GROUP, self._content_widget + if self._convertor_items_group is None: + group_widget = ConvertorItemsGroupWidget( + CONVERTOR_ITEM_GROUP, self._content_widget ) group_widget.selected.connect(self._on_widget_selection) self._content_layout.insertWidget(1, group_widget) - self._legacy_items_group = group_widget + self._convertor_items_group = group_widget - self._legacy_items_group.update_items(legacy_items) + self._convertor_items_group.update_items(convertor_items) def refresh_instance_states(self): """Trigger update of instances on group widgets.""" @@ -742,8 +742,8 @@ class InstanceCardView(AbstractInstanceView): new_widget = self._context_widget else: - if group_name == LEGACY_ITEM_GROUP: - group_widget = self._legacy_items_group + if group_name == CONVERTOR_ITEM_GROUP: + group_widget = self._convertor_items_group else: group_widget = self._widgets_by_group[group_name] new_widget = group_widget.get_widget_by_item_id(instance_id) @@ -791,8 +791,8 @@ class InstanceCardView(AbstractInstanceView): if instance_id == CONTEXT_ID: remove_group = True else: - if group_name == LEGACY_ITEM_GROUP: - group_widget = self._legacy_items_group + if group_name == CONVERTOR_ITEM_GROUP: + group_widget = self._convertor_items_group else: group_widget = self._widgets_by_group[group_name] if not group_widget.get_selected_widgets(): @@ -906,8 +906,8 @@ class InstanceCardView(AbstractInstanceView): if name == CONTEXT_GROUP: sorted_widgets = [self._context_widget] else: - if name == LEGACY_ITEM_GROUP: - group_widget = self._legacy_items_group + if name == CONVERTOR_ITEM_GROUP: + group_widget = self._convertor_items_group else: group_widget = self._widgets_by_group[name] sorted_widgets = group_widget.get_ordered_widgets() @@ -1034,7 +1034,7 @@ class InstanceCardView(AbstractInstanceView): elif isinstance(widget, InstanceCardWidget): instances.append(widget.id) - elif isinstance(widget, LegacyItemCardWidget): + elif isinstance(widget, ConvertorItemCardWidget): convertor_identifiers.append(widget.identifier) return instances, context_selected, convertor_identifiers @@ -1066,16 +1066,16 @@ class InstanceCardView(AbstractInstanceView): if group_name == CONTEXT_GROUP: continue - legacy_group = group_name == LEGACY_ITEM_GROUP - if legacy_group: - group_widget = self._legacy_items_group + is_convertor_group = group_name == CONVERTOR_ITEM_GROUP + if is_convertor_group: + group_widget = self._convertor_items_group else: group_widget = self._widgets_by_group[group_name] group_selected = False for widget in group_widget.get_ordered_widgets(): select = False - if legacy_group: + if is_convertor_group: is_in = widget.identifier in s_convertor_identifiers else: is_in = widget.id in s_instance_ids diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index 53951e3cba..32d84862f0 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -37,8 +37,8 @@ from ..constants import ( CONTEXT_ID, CONTEXT_LABEL, GROUP_ROLE, - LEGACY_CONVERTER_IDENTIFIER, - LEGACY_ITEM_GROUP, + CONVERTER_IDENTIFIER_ROLE, + CONVERTOR_ITEM_GROUP, ) @@ -333,7 +333,7 @@ class InstanceTreeView(QtWidgets.QTreeView): """Ids of selected instances.""" instance_ids = set() for index in self.selectionModel().selectedIndexes(): - if index.data(LEGACY_CONVERTER_IDENTIFIER) is not None: + if index.data(CONVERTER_IDENTIFIER_ROLE) is not None: continue instance_id = index.data(INSTANCE_ID_ROLE) @@ -450,9 +450,9 @@ class InstanceListView(AbstractInstanceView): self._context_item = None self._context_widget = None - self._legacy_group_item = None - self._legacy_group_widget = None - self._legacy_items_by_id = {} + self._convertor_group_item = None + self._convertor_group_widget = None + self._convertor_items_by_id = {} self._instance_view = instance_view self._instance_delegate = instance_delegate @@ -467,8 +467,8 @@ class InstanceListView(AbstractInstanceView): def _update_widget_expand_state(self, index, expanded): group_name = index.data(GROUP_ROLE) - if group_name == LEGACY_ITEM_GROUP: - group_widget = self._legacy_group_widget + if group_name == CONVERTOR_ITEM_GROUP: + group_widget = self._convertor_group_widget else: group_widget = self._group_widgets.get(group_name) @@ -540,7 +540,7 @@ class InstanceListView(AbstractInstanceView): if self._make_sure_context_item_exists(): sort_at_the_end = True - self._update_legacy_items_group() + self._update_convertor_items_group() # Prepare instances by their groups instances_by_group_name = collections.defaultdict(list) @@ -702,25 +702,25 @@ class InstanceListView(AbstractInstanceView): self._context_item = context_item return True - def _update_legacy_items_group(self): + def _update_convertor_items_group(self): created_new_items = False - legacy_items_by_id = self._controller.legacy_items - group_item = self._legacy_group_item - if not legacy_items_by_id and group_item is None: + convertor_items_by_id = self._controller.convertor_items + group_item = self._convertor_group_item + if not convertor_items_by_id and group_item is None: return created_new_items root_item = self._instance_model.invisibleRootItem() - if not legacy_items_by_id: + if not convertor_items_by_id: root_item.removeRow(group_item.row()) - self._legacy_group_widget.deleteLater() - self._legacy_group_widget = None - self._legacy_items_by_id = {} + self._convertor_group_widget.deleteLater() + self._convertor_group_widget = None + self._convertor_items_by_id = {} return created_new_items if group_item is None: created_new_items = True group_item = QtGui.QStandardItem() - group_item.setData(LEGACY_ITEM_GROUP, GROUP_ROLE) + group_item.setData(CONVERTOR_ITEM_GROUP, GROUP_ROLE) group_item.setData(1, SORT_VALUE_ROLE) group_item.setData(True, IS_GROUP_ROLE) group_item.setFlags(QtCore.Qt.ItemIsEnabled) @@ -732,36 +732,38 @@ class InstanceListView(AbstractInstanceView): ) proxy_index = self._proxy_model.mapFromSource(index) widget = InstanceListGroupWidget( - LEGACY_ITEM_GROUP, self._instance_view + CONVERTOR_ITEM_GROUP, self._instance_view ) widget.toggle_checkbox.setVisible(False) - widget.expand_changed.connect(self._on_legacy_group_expand_request) + widget.expand_changed.connect( + self._on_convertor_group_expand_request + ) self._instance_view.setIndexWidget(proxy_index, widget) - self._legacy_group_item = group_item - self._legacy_group_widget = widget + self._convertor_group_item = group_item + self._convertor_group_widget = widget for row in reversed(range(group_item.rowCount())): child_item = group_item.child(row) - child_identifier = child_item.data(LEGACY_CONVERTER_IDENTIFIER) - if child_identifier not in legacy_items_by_id: - self._legacy_items_by_id.pop(child_identifier, None) + child_identifier = child_item.data(CONVERTER_IDENTIFIER_ROLE) + if child_identifier not in convertor_items_by_id: + self._convertor_items_by_id.pop(child_identifier, None) group_item.removeRows(row, 1) new_items = [] - for identifier, convertor_item in legacy_items_by_id.items(): - item = self._legacy_items_by_id.get(identifier) + for identifier, convertor_item in convertor_items_by_id.items(): + item = self._convertor_items_by_id.get(identifier) if item is None: created_new_items = True item = QtGui.QStandardItem(convertor_item.label) new_items.append(item) item.setData(convertor_item.id, INSTANCE_ID_ROLE) item.setData(convertor_item.label, SORT_VALUE_ROLE) - item.setData(LEGACY_ITEM_GROUP, GROUP_ROLE) + item.setData(CONVERTOR_ITEM_GROUP, GROUP_ROLE) item.setData( - convertor_item.identifier, LEGACY_CONVERTER_IDENTIFIER + convertor_item.identifier, CONVERTER_IDENTIFIER_ROLE ) - self._legacy_items_by_id[identifier] = item + self._convertor_items_by_id[identifier] = item if new_items: group_item.appendRows(new_items) @@ -874,8 +876,8 @@ class InstanceListView(AbstractInstanceView): proxy_index = self._proxy_model.mapFromSource(group_index) self._instance_view.setExpanded(proxy_index, expanded) - def _on_legacy_group_expand_request(self, _, expanded): - group_item = self._legacy_group_item + def _on_convertor_group_expand_request(self, _, expanded): + group_item = self._convertor_group_item if not group_item: return group_index = self._instance_model.index( @@ -923,7 +925,7 @@ class InstanceListView(AbstractInstanceView): context_selected = False for index in self._instance_view.selectionModel().selectedIndexes(): - convertor_identifier = index.data(LEGACY_CONVERTER_IDENTIFIER) + convertor_identifier = index.data(CONVERTER_IDENTIFIER_ROLE) if convertor_identifier is not None: convertor_identifiers.append(convertor_identifier) continue @@ -974,7 +976,7 @@ class InstanceListView(AbstractInstanceView): (item.child(row), list(new_parent_items)) ) - convertor_identifier = item.data(LEGACY_CONVERTER_IDENTIFIER) + convertor_identifier = item.data(CONVERTER_IDENTIFIER_ROLE) select = False expand_parent = True From be54ff4d27978079855c99e5f8f9f1d188742b53 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 18:00:26 +0200 Subject: [PATCH 030/160] rename 'convert_legacy_items' to 'trigger_convertor_items' --- openpype/tools/publisher/control.py | 4 ++-- openpype/tools/publisher/widgets/overview_widget.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index b867bddc9d..245d328be4 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1239,7 +1239,7 @@ class AbstractPublisherController(object): pass @abstractmethod - def convert_legacy_items(self, convertor_identifiers): + def trigger_convertor_items(self, convertor_identifiers): pass @abstractmethod @@ -1854,7 +1854,7 @@ class PublisherController(BasePublisherController): variant, task_name, asset_doc, project_name, instance=instance ) - def convert_legacy_items(self, convertor_identifiers): + def trigger_convertor_items(self, convertor_identifiers): for convertor_identifier in convertor_identifiers: self._create_context.run_convertor(convertor_identifier) self._on_create_instance_change() diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index e208786fc7..7c1755b3eb 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -321,7 +321,7 @@ class OverviewWidget(QtWidgets.QFrame): def _on_convert_requested(self): _, _, convertor_identifiers = self.get_selected_items() - self._controller.convert_legacy_items(convertor_identifiers) + self._controller.trigger_convertor_items(convertor_identifiers) def get_selected_items(self): view = self._subset_views_layout.currentWidget() From 81f7aa5525e52f229cf4ec340f8a125358d0afeb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 18:15:24 +0200 Subject: [PATCH 031/160] get rid of 'legacy' from variables --- openpype/pipeline/create/context.py | 44 ++++++++++----------- openpype/pipeline/create/creator_plugins.py | 33 ++++++++-------- openpype/tools/publisher/control.py | 4 +- 3 files changed, 41 insertions(+), 40 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 250193f511..56d7447a0b 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -22,7 +22,7 @@ from .creator_plugins import ( Creator, AutoCreator, discover_creator_plugins, - discover_legacy_convertor_plugins, + discover_convertor_plugins, ) UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"]) @@ -853,8 +853,8 @@ class CreatedInstance: self[key] = new_value -class LegacyInstancesItem(object): - """Item representing convertor for legacy instances. +class ConvertorItem(object): + """Item representing convertor plugin. Args: identifier (str): Identifier of convertor. @@ -949,8 +949,8 @@ class CreateContext: # Manual creators self.manual_creators = {} - self.legacy_convertors = {} - self.legacy_items_by_id = {} + self.convertors_plugins = {} + self.convertor_items_by_id = {} self.publish_discover_result = None self.publish_plugins_mismatch_targets = [] @@ -1032,7 +1032,7 @@ class CreateContext: with self.bulk_instances_collection(): self.reset_instances() - self.find_legacy_items() + self.find_convertor_items() self.execute_autocreators() self.reset_finalization() @@ -1090,7 +1090,7 @@ class CreateContext: self._reset_publish_plugins(discover_publish_plugins) self._reset_creator_plugins() - self._reset_legacy_convertor_plugins() + self._reset_convertor_plugins() def _reset_publish_plugins(self, discover_publish_plugins): import pyblish.logic @@ -1186,9 +1186,9 @@ class CreateContext: self.creators = creators - def _reset_legacy_convertor_plugins(self): - legacy_convertors = {} - for convertor_class in discover_legacy_convertor_plugins(): + def _reset_convertor_plugins(self): + convertors_plugins = {} + for convertor_class in discover_convertor_plugins(): if inspect.isabstract(convertor_class): self.log.info( "Skipping abstract Creator {}".format(str(convertor_class)) @@ -1196,16 +1196,16 @@ class CreateContext: continue convertor_identifier = convertor_class.identifier - if convertor_identifier in legacy_convertors: + if convertor_identifier in convertors_plugins: self.log.warning(( "Duplicated Converter identifier. " "Using first and skipping following" )) continue - legacy_convertors[convertor_identifier] = convertor_class(self) + convertors_plugins[convertor_identifier] = convertor_class(self) - self.legacy_convertors = legacy_convertors + self.convertors_plugins = convertors_plugins def reset_context_data(self): """Reload context data using host implementation. @@ -1278,13 +1278,13 @@ class CreateContext: def creator_removed_instance(self, instance): self._instances_by_id.pop(instance.id, None) - def add_legacy_item(self, convertor_identifier, label): - self.legacy_items_by_id[convertor_identifier] = ( - LegacyInstancesItem(convertor_identifier, label) + def add_convertor_item(self, convertor_identifier, label): + self.convertor_items_by_id[convertor_identifier] = ConvertorItem( + convertor_identifier, label ) - def remove_legacy_item(self, convertor_identifier): - self.legacy_items_by_id.pop(convertor_identifier, None) + def remove_convertor_item(self, convertor_identifier): + self.convertor_items_by_id.pop(convertor_identifier, None) @contextmanager def bulk_instances_collection(self): @@ -1321,10 +1321,10 @@ class CreateContext: for creator in self.creators.values(): creator.collect_instances() - def find_legacy_items(self): - self.legacy_items_by_id = {} + def find_convertor_items(self): + self.convertor_items_by_id = {} - for convertor in self.legacy_convertors.values(): + for convertor in self.convertors_plugins.values(): try: convertor.find_instances() except: @@ -1502,6 +1502,6 @@ class CreateContext: return self._collection_shared_data def run_convertor(self, convertor_identifier): - convertor = self.legacy_convertors.get(convertor_identifier) + convertor = self.convertors_plugins.get(convertor_identifier) if convertor is not None: convertor.convert() diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index ff9326693e..2e7d8709a2 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -34,7 +34,7 @@ class CreatorError(Exception): @six.add_metaclass(ABCMeta) -class LegacyInstanceConvertor(object): +class LegacySubsetConvertor(object): """Helper for conversion of instances created using legacy creators. Conversion from legacy creators would mean to loose legacy instances, @@ -45,10 +45,10 @@ class LegacyInstanceConvertor(object): Convertor logic should be very simple. Method 'find_instances' is to look for legacy instances in scene a possibly call - pre-implemented 'add_legacy_item'. + pre-implemented 'add_convertor_item'. User will have ability to trigger conversion which is executed by calling - 'convert' which should call 'remove_legacy_item' when is done. + 'convert' which should call 'remove_convertor_item' when is done. It does make sense to add only one or none legacy item to create context for convertor as it's not possible to choose which instace are converted @@ -78,7 +78,8 @@ class LegacyInstanceConvertor(object): def find_instances(self): """Look for legacy instances in the scene. - Should call 'add_legacy_item' if there is at least one item. + Should call 'add_convertor_item' if there is at least one instance to + convert. """ pass @@ -108,19 +109,19 @@ class LegacyInstanceConvertor(object): return self._create_context.collection_shared_data - def add_legacy_item(self, label): + def add_convertor_item(self, label): """Add item to CreateContext. Args: label (str): Label of item which will show in UI. """ - self._create_context.add_legacy_item(self.identifier, label) + self._create_context.add_convertor_item(self.identifier, label) - def remove_legacy_item(self): + def remove_convertor_item(self): """Remove legacy item from create context when conversion finished.""" - self._create_context.remove_legacy_item(self.identifier) + self._create_context.remove_convertor_item(self.identifier) @six.add_metaclass(ABCMeta) @@ -559,8 +560,8 @@ def discover_creator_plugins(): return discover(BaseCreator) -def discover_legacy_convertor_plugins(): - return discover(LegacyInstanceConvertor) +def discover_convertor_plugins(): + return discover(LegacySubsetConvertor) def discover_legacy_creator_plugins(): @@ -620,8 +621,8 @@ def register_creator_plugin(plugin): elif issubclass(plugin, LegacyCreator): register_plugin(LegacyCreator, plugin) - elif issubclass(plugin, LegacyInstanceConvertor): - register_plugin(LegacyInstanceConvertor, plugin) + elif issubclass(plugin, LegacySubsetConvertor): + register_plugin(LegacySubsetConvertor, plugin) def deregister_creator_plugin(plugin): @@ -631,17 +632,17 @@ def deregister_creator_plugin(plugin): elif issubclass(plugin, LegacyCreator): deregister_plugin(LegacyCreator, plugin) - elif issubclass(plugin, LegacyInstanceConvertor): - deregister_plugin(LegacyInstanceConvertor, plugin) + elif issubclass(plugin, LegacySubsetConvertor): + deregister_plugin(LegacySubsetConvertor, plugin) def register_creator_plugin_path(path): register_plugin_path(BaseCreator, path) register_plugin_path(LegacyCreator, path) - register_plugin_path(LegacyInstanceConvertor, path) + register_plugin_path(LegacySubsetConvertor, path) def deregister_creator_plugin_path(path): deregister_plugin_path(BaseCreator, path) deregister_plugin_path(LegacyCreator, path) - deregister_plugin_path(LegacyInstanceConvertor, path) + deregister_plugin_path(LegacySubsetConvertor, path) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 245d328be4..107ddbbb93 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1608,7 +1608,7 @@ class PublisherController(BasePublisherController): @property def convertor_items(self): - return self._create_context.legacy_items_by_id + return self._create_context.convertor_items_by_id @property def _creators(self): @@ -1728,7 +1728,7 @@ class PublisherController(BasePublisherController): self._create_context.reset_context_data() with self._create_context.bulk_instances_collection(): self._create_context.reset_instances() - self._create_context.find_legacy_items() + self._create_context.find_convertor_items() self._create_context.execute_autocreators() self._resetting_instances = False From 4f70a58d5c7e9c604c1d6dabbeb80c4b74ab83b3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 18:17:27 +0200 Subject: [PATCH 032/160] renamed 'LegacySubsetConvertor' to 'SubsetConvertorPlugin' --- openpype/pipeline/create/creator_plugins.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 2e7d8709a2..584e082221 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -34,7 +34,7 @@ class CreatorError(Exception): @six.add_metaclass(ABCMeta) -class LegacySubsetConvertor(object): +class SubsetConvertorPlugin(object): """Helper for conversion of instances created using legacy creators. Conversion from legacy creators would mean to loose legacy instances, @@ -561,7 +561,7 @@ def discover_creator_plugins(): def discover_convertor_plugins(): - return discover(LegacySubsetConvertor) + return discover(SubsetConvertorPlugin) def discover_legacy_creator_plugins(): @@ -621,8 +621,8 @@ def register_creator_plugin(plugin): elif issubclass(plugin, LegacyCreator): register_plugin(LegacyCreator, plugin) - elif issubclass(plugin, LegacySubsetConvertor): - register_plugin(LegacySubsetConvertor, plugin) + elif issubclass(plugin, SubsetConvertorPlugin): + register_plugin(SubsetConvertorPlugin, plugin) def deregister_creator_plugin(plugin): @@ -632,17 +632,17 @@ def deregister_creator_plugin(plugin): elif issubclass(plugin, LegacyCreator): deregister_plugin(LegacyCreator, plugin) - elif issubclass(plugin, LegacySubsetConvertor): - deregister_plugin(LegacySubsetConvertor, plugin) + elif issubclass(plugin, SubsetConvertorPlugin): + deregister_plugin(SubsetConvertorPlugin, plugin) def register_creator_plugin_path(path): register_plugin_path(BaseCreator, path) register_plugin_path(LegacyCreator, path) - register_plugin_path(LegacySubsetConvertor, path) + register_plugin_path(SubsetConvertorPlugin, path) def deregister_creator_plugin_path(path): deregister_plugin_path(BaseCreator, path) deregister_plugin_path(LegacyCreator, path) - deregister_plugin_path(LegacySubsetConvertor, path) + deregister_plugin_path(SubsetConvertorPlugin, path) From 87671bcfd6905e7e1bf729c6aa0fef42f47d6d9c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 19:16:54 +0200 Subject: [PATCH 033/160] added style for errored card message --- openpype/style/data.json | 4 +++- openpype/style/style.css | 21 +++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index fef69071ed..146af84663 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -64,7 +64,9 @@ "overlay-messages": { "close-btn": "#D3D8DE", "bg-success": "#458056", - "bg-success-hover": "#55a066" + "bg-success-hover": "#55a066", + "bg-error": "#AD2E2E", + "bg-error-hover": "#C93636" }, "tab-widget": { "bg": "#21252B", diff --git a/openpype/style/style.css b/openpype/style/style.css index a6818a5792..9919973b06 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -688,22 +688,23 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { } /* Messages overlay */ -#OverlayMessageWidget { +OverlayMessageWidget { border-radius: 0.2em; - background: {color:bg-buttons}; -} - -#OverlayMessageWidget:hover { - background: {color:bg-button-hover}; -} -#OverlayMessageWidget { background: {color:overlay-messages:bg-success}; } -#OverlayMessageWidget:hover { + +OverlayMessageWidget:hover { background: {color:overlay-messages:bg-success-hover}; } -#OverlayMessageWidget QWidget { +OverlayMessageWidget[type="error"] { + background: {color:overlay-messages:bg-error}; +} +OverlayMessageWidget[type="error"]:hover { + background: {color:overlay-messages:bg-error-hover}; +} + +OverlayMessageWidget QWidget { background: transparent; } From 0fd54454192ffec16170b1cca574825955f7397f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 19:25:35 +0200 Subject: [PATCH 034/160] wrap convertor callbacks by custom exceptions --- openpype/pipeline/create/context.py | 94 +++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index b6dce4c03d..c87803c5c4 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -71,6 +71,41 @@ class HostMissRequiredMethod(Exception): super(HostMissRequiredMethod, self).__init__(msg) +class ConvertorsOperationFailed(Exception): + def __init__(self, msg, failed_info): + super(ConvertorsOperationFailed, self).__init__(msg) + self.failed_info = failed_info + + +class ConvertorsFindFailed(ConvertorsOperationFailed): + def __init__(self, failed_info): + msg = "Failed to find incompatible subsets" + super(ConvertorsFindFailed, self).__init__( + msg, failed_info + ) + + +class ConvertorsConversionFailed(ConvertorsOperationFailed): + def __init__(self, failed_info): + msg = "Failed to convert incompatible subsets" + super(ConvertorsConversionFailed, self).__init__( + msg, failed_info + ) + + +def prepare_failed_convertor_operation_info(identifier, exc_info): + exc_type, exc_value, exc_traceback = exc_info + formatted_traceback = "".join(traceback.format_exception( + exc_type, exc_value, exc_traceback + )) + + return { + "convertor_identifier": identifier, + "message": str(exc_value), + "traceback": formatted_traceback + } + + class CreatorsOperationFailed(Exception): """Raised when a creator process crashes in 'CreateContext'. @@ -1486,12 +1521,26 @@ class CreateContext: raise CreatorsCollectionFailed(failed_info) def find_convertor_items(self): + """Go through convertor plugins to look for items to convert. + + Raises: + ConvertorsFindFailed: When one or more convertors fails during + finding. + """ + self.convertor_items_by_id = {} + failed_info = [] for convertor in self.convertors_plugins.values(): try: convertor.find_instances() + except: + failed_info.append( + prepare_failed_convertor_operation_info( + convertor.identifier, sys.exc_info() + ) + ) self.log.warning( "Failed to find instances of convertor \"{}\"".format( convertor.identifier @@ -1499,6 +1548,9 @@ class CreateContext: exc_info=True ) + if failed_info: + raise ConvertorsFindFailed(failed_info) + def execute_autocreators(self): """Execute discovered AutoCreator plugins. @@ -1756,6 +1808,48 @@ class CreateContext: return self._collection_shared_data def run_convertor(self, convertor_identifier): + """Run convertor plugin by it's idenfitifier. + + Conversion is skipped if convertor is not available. + + Args: + convertor_identifier (str): Identifier of convertor. + """ + convertor = self.convertors_plugins.get(convertor_identifier) if convertor is not None: convertor.convert() + + def run_convertors(self, convertor_identifiers): + """Run convertor plugins by idenfitifiers. + + Conversion is skipped if convertor is not available. + + Args: + convertor_identifiers (Iterator[str]): Identifiers of convertors + to run. + + Raises: + ConvertorsConversionFailed: When one or more convertors fails. + """ + + failed_info = [] + for convertor_identifier in convertor_identifiers: + try: + self.run_convertor(convertor_identifier) + + except: + failed_info.append( + prepare_failed_convertor_operation_info( + convertor_identifier, sys.exc_info() + ) + ) + self.log.warning( + "Failed to convert instances of convertor \"{}\"".format( + convertor_identifier + ), + exc_info=True + ) + + if failed_info: + raise ConvertorsConversionFailed(failed_info) From 9774c507f20623697dbeae1de747ca99d990fded Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 19:26:27 +0200 Subject: [PATCH 035/160] Error message box is less creator's specific --- openpype/tools/publisher/window.py | 105 ++++++++++++++++------------- 1 file changed, 57 insertions(+), 48 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index b6bd506c18..58c73f4821 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -1,4 +1,5 @@ import collections +import copy from Qt import QtWidgets, QtCore, QtGui from openpype import ( @@ -224,10 +225,10 @@ class PublisherWindow(QtWidgets.QDialog): # Floating publish frame publish_frame = PublishFrame(controller, self.footer_border, self) - creators_dialog_message_timer = QtCore.QTimer() - creators_dialog_message_timer.setInterval(100) - creators_dialog_message_timer.timeout.connect( - self._on_creators_message_timeout + errors_dialog_message_timer = QtCore.QTimer() + errors_dialog_message_timer.setInterval(100) + errors_dialog_message_timer.timeout.connect( + self._on_errors_message_timeout ) help_btn.clicked.connect(self._on_help_click) @@ -268,16 +269,16 @@ class PublisherWindow(QtWidgets.QDialog): "show.card.message", self._on_overlay_message ) controller.event_system.add_callback( - "instances.collection.failed", self._instance_collection_failed + "instances.collection.failed", self._on_creator_error ) controller.event_system.add_callback( - "instances.save.failed", self._instance_save_failed + "instances.save.failed", self._on_creator_error ) controller.event_system.add_callback( - "instances.remove.failed", self._instance_remove_failed + "instances.remove.failed", self._on_creator_error ) controller.event_system.add_callback( - "instances.create.failed", self._instance_create_failed + "instances.create.failed", self._on_creator_error ) # Store extra header widget for TrayPublisher @@ -325,8 +326,8 @@ class PublisherWindow(QtWidgets.QDialog): self._restart_timer = None self._publish_frame_visible = None - self._creators_messages_to_show = collections.deque() - self._creators_dialog_message_timer = creators_dialog_message_timer + self._error_messages_to_show = collections.deque() + self._errors_dialog_message_timer = errors_dialog_message_timer self._set_publish_visibility(False) @@ -357,7 +358,10 @@ class PublisherWindow(QtWidgets.QDialog): self._update_publish_frame_rect() def _on_overlay_message(self, event): - self._overlay_object.add_message(event["message"]) + self._overlay_object.add_message( + event["message"], + event.get("message_type") + ) def _on_first_show(self): self.resize(self.default_width, self.default_height) @@ -604,37 +608,39 @@ class PublisherWindow(QtWidgets.QDialog): 0, window_size.height() - height ) - def add_message_dialog(self, title, failed_info): - self._creators_messages_to_show.append((title, failed_info)) - self._creators_dialog_message_timer.start() + def add_error_message_dialog(self, title, failed_info, message_start=None): + self._error_messages_to_show.append( + (title, failed_info, message_start) + ) + self._errors_dialog_message_timer.start() - def _on_creators_message_timeout(self): - if not self._creators_messages_to_show: - self._creators_dialog_message_timer.stop() + def _on_errors_message_timeout(self): + if not self._error_messages_to_show: + self._errors_dialog_message_timer.stop() return - item = self._creators_messages_to_show.popleft() - title, failed_info = item - dialog = CreatorsErrorMessageBox(title, failed_info, self) + item = self._error_messages_to_show.popleft() + title, failed_info, message_start = item + dialog = ErrorsMessageBox( + title, failed_info, message_start, self + ) dialog.exec_() dialog.deleteLater() - def _instance_collection_failed(self, event): - self.add_message_dialog(event["title"], event["failed_info"]) - - def _instance_save_failed(self, event): - self.add_message_dialog(event["title"], event["failed_info"]) - - def _instance_remove_failed(self, event): - self.add_message_dialog(event["title"], event["failed_info"]) - - def _instance_create_failed(self, event): - self.add_message_dialog(event["title"], event["failed_info"]) + def _on_creator_error(self, event): + new_failed_info = [] + for item in event["failed_info"]: + new_item = copy.deepcopy(item) + new_item["label"] = new_item.pop("creator_label") + new_item["identifier"] = new_item.pop("creator_identifier") + new_failed_info.append(new_item) + self.add_error_message_dialog(event["title"], new_failed_info, "Creator:") -class CreatorsErrorMessageBox(ErrorMessageBox): - def __init__(self, error_title, failed_info, parent): +class ErrorsMessageBox(ErrorMessageBox): + def __init__(self, error_title, failed_info, message_start, parent): self._failed_info = failed_info + self._message_start = message_start self._info_with_id = [ # Id must be string when used in tab widget {"id": str(idx), "info": info} @@ -644,7 +650,7 @@ class CreatorsErrorMessageBox(ErrorMessageBox): self._tabs_widget = None self._stack_layout = None - super(CreatorsErrorMessageBox, self).__init__(error_title, parent) + super(ErrorsMessageBox, self).__init__(error_title, parent) layout = self.layout() layout.setContentsMargins(0, 0, 0, 0) @@ -659,17 +665,21 @@ class CreatorsErrorMessageBox(ErrorMessageBox): def _get_report_data(self): output = [] for info in self._failed_info: - creator_label = info["creator_label"] - creator_identifier = info["creator_identifier"] - report_message = "Creator:" - if creator_label: - report_message += " {} ({})".format( - creator_label, creator_identifier) + item_label = info.get("label") + item_identifier = info["identifier"] + if item_label: + report_message = "{} ({})".format( + item_label, item_identifier) else: - report_message += " {}".format(creator_identifier) + report_message = "{}".format(item_identifier) + + if self._message_start: + report_message = "{} {}".format( + self._message_start, report_message + ) report_message += "\n\nError: {}".format(info["message"]) - formatted_traceback = info["traceback"] + formatted_traceback = info.get("traceback") if formatted_traceback: report_message += "\n\n{}".format(formatted_traceback) output.append(report_message) @@ -686,11 +696,10 @@ class CreatorsErrorMessageBox(ErrorMessageBox): item_id = item["id"] info = item["info"] message = info["message"] - formatted_traceback = info["traceback"] - creator_label = info["creator_label"] - creator_identifier = info["creator_identifier"] - if not creator_label: - creator_label = creator_identifier + formatted_traceback = info.get("traceback") + item_label = info.get("label") + if not item_label: + item_label = info["identifier"] msg_widget = QtWidgets.QWidget(stack_widget) msg_layout = QtWidgets.QVBoxLayout(msg_widget) @@ -710,7 +719,7 @@ class CreatorsErrorMessageBox(ErrorMessageBox): msg_layout.addStretch(1) - tabs_widget.add_tab(creator_label, item_id) + tabs_widget.add_tab(item_label, item_id) stack_layout.addWidget(msg_widget) if first: first = False From 3ab3582b0a260bb9008a4138e8c3edc1c8f67ac1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 19:26:48 +0200 Subject: [PATCH 036/160] prepare to handle convertor errors --- openpype/tools/publisher/window.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 58c73f4821..a3387043b8 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -280,6 +280,12 @@ class PublisherWindow(QtWidgets.QDialog): controller.event_system.add_callback( "instances.create.failed", self._on_creator_error ) + controller.event_system.add_callback( + "convertors.convert.failed", self._on_convertor_error + ) + controller.event_system.add_callback( + "convertors.find.failed", self._on_convertor_error + ) # Store extra header widget for TrayPublisher # - can be used to add additional widgets to header between context @@ -636,6 +642,16 @@ class PublisherWindow(QtWidgets.QDialog): new_failed_info.append(new_item) self.add_error_message_dialog(event["title"], new_failed_info, "Creator:") + def _on_convertor_error(self, event): + new_failed_info = [] + for item in event["failed_info"]: + new_item = copy.deepcopy(item) + new_item["identifier"] = new_item.pop("convertor_identifier") + new_failed_info.append(new_item) + self.add_error_message_dialog( + event["title"], new_failed_info, "Convertor:" + ) + class ErrorsMessageBox(ErrorMessageBox): def __init__(self, error_title, failed_info, message_start, parent): From f9a75ea240e1c1c9c5e9213dbbb32d4cbf354067 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 19:27:21 +0200 Subject: [PATCH 037/160] handle ConvertorsOperationFailed in controller --- openpype/tools/publisher/control.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 482227e708..7cfc89f59e 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -33,6 +33,7 @@ from openpype.pipeline.create import ( ) from openpype.pipeline.create.context import ( CreatorsOperationFailed, + ConvertorsOperationFailed, ) # Define constant for plugin orders offset @@ -1743,7 +1744,16 @@ class PublisherController(BasePublisherController): } ) - self._create_context.find_convertor_items() + try: + self._create_context.find_convertor_items() + except ConvertorsOperationFailed as exc: + self._emit_event( + "convertors.find.failed", + { + "title": "Collection of unsupported subset failed", + "failed_info": exc.failed_info + } + ) try: self._create_context.execute_autocreators() @@ -1881,8 +1891,19 @@ class PublisherController(BasePublisherController): ) def trigger_convertor_items(self, convertor_identifiers): - for convertor_identifier in convertor_identifiers: - self._create_context.run_convertor(convertor_identifier) + success = True + try: + self._create_context.run_convertors(convertor_identifiers) + + except ConvertorsOperationFailed as exc: + success = False + self._emit_event( + "convertors.convert.failed", + { + "title": "Conversion failed", + "failed_info": exc.failed_info + } + ) self._on_create_instance_change() self.emit_card_message("Conversion finished") From 22a1191ab1a4e8ce516aef216e18f0f5a0817c68 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 19:27:34 +0200 Subject: [PATCH 038/160] emit card message can accept message types --- openpype/tools/publisher/control.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 7cfc89f59e..d4dddb75d5 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1264,7 +1264,7 @@ class AbstractPublisherController(object): pass @abstractmethod - def emit_card_message(self, message): + def emit_card_message(self, message, message_type=None): """Emit a card message which can have a lifetime. This is for UI purposes. Method can be extended to more arguments @@ -1771,8 +1771,14 @@ class PublisherController(BasePublisherController): self._on_create_instance_change() - def emit_card_message(self, message): - self._emit_event("show.card.message", {"message": message}) + def emit_card_message(self, message, message_type=None): + self._emit_event( + "show.card.message", + { + "message": message, + "message_type": message_type + } + ) def get_creator_attribute_definitions(self, instances): """Collect creator attribute definitions for multuple instances. From 12a272a8eec1c63bc2aece3c5a9acbb56cee0867 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 24 Oct 2022 19:32:06 +0200 Subject: [PATCH 039/160] added different types of card messages --- openpype/tools/publisher/control.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index d4dddb75d5..18d1a5b083 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -40,6 +40,11 @@ from openpype.pipeline.create.context import ( PLUGIN_ORDER_OFFSET = 0.5 +class CardMessageTypes: + standard = None + error = "error" + + class MainThreadItem: """Callback with args and kwargs.""" @@ -1264,7 +1269,9 @@ class AbstractPublisherController(object): pass @abstractmethod - def emit_card_message(self, message, message_type=None): + def emit_card_message( + self, message, message_type=CardMessageTypes.standard + ): """Emit a card message which can have a lifetime. This is for UI purposes. Method can be extended to more arguments @@ -1771,7 +1778,9 @@ class PublisherController(BasePublisherController): self._on_create_instance_change() - def emit_card_message(self, message, message_type=None): + def emit_card_message( + self, message, message_type=CardMessageTypes.standard + ): self._emit_event( "show.card.message", { @@ -1910,8 +1919,12 @@ class PublisherController(BasePublisherController): "failed_info": exc.failed_info } ) + + if success: + self.emit_card_message("Conversion finished") + else: + self.emit_card_message("Conversion failed", CardMessageTypes.error) self._on_create_instance_change() - self.emit_card_message("Conversion finished") def create( self, creator_identifier, subset_name, instance_data, options From 754cebb06fbf0a01d63d33c8b1bda918c48b28b5 Mon Sep 17 00:00:00 2001 From: "Ryan J. Quinlan" Date: Mon, 24 Oct 2022 13:13:22 -0700 Subject: [PATCH 040/160] Update dev_requirements.md Small typo and grammar fixes. --- website/docs/dev_requirements.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/docs/dev_requirements.md b/website/docs/dev_requirements.md index eb4b132297..1c8958d1c0 100644 --- a/website/docs/dev_requirements.md +++ b/website/docs/dev_requirements.md @@ -39,13 +39,13 @@ Pype needs site-wide installation of **MongoDB**. It should be installed on reliable server, that all workstations (and possibly render nodes) can connect. This server holds **Avalon** database that is at the core of everything -Depending on project size and number of artists working connection speed and +Depending on project size and number of artists working, connection speed and latency influence performance experienced by artists. If remote working is required, this mongodb server must be accessible from Internet or cloud solution can be used. Reasonable backup plan or high availability options are recommended. *Replication* feature of MongoDB should be considered. This is beyond the scope of this documentation, please refer to [MongoDB Documentation](https://docs.mongodb.com/manual/replication/). -Pype can run it's own instance of mongodb, mostly for testing and development purposes. +Pype can run its own instance of mongodb, mostly for testing and development purposes. For that it uses locally installed MongoDB. Download it from [mognoDB website](https://www.mongodb.com/download-center/community), install it and @@ -69,7 +69,7 @@ the major DCCs, it most probably can run openPYPE. Installed, it takes around 400MB of space, depending on the platform -For well functioning ftrack event server, we recommend a linux virtual server with Ubuntu or CentOS. CPU and RAM allocation needs differ based on the studio size, but a 2GB of ram, with a dual core CPU and around 4GB of storage should suffice +For a well functioning ftrack event server, we recommend a linux virtual server with Ubuntu or CentOS. CPU and RAM allocation needs differ based on the studio size, but a 2GB of ram, with a dual core CPU and around 4GB of storage should suffice ## Deployment From 52bb4a0d40ba62f7be6c8e589bd36537571897e4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 10:33:10 +0200 Subject: [PATCH 041/160] fix publisher import in experimental tools --- openpype/tools/experimental_tools/tools_def.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index fa2971dc1d..d3a1caa60e 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -164,9 +164,9 @@ class ExperimentalTools: def _show_publisher(self): if self._publisher_tool is None: - from openpype.tools import publisher + from openpype.tools.publisher.window import PublisherWindow - self._publisher_tool = publisher.PublisherWindow( + self._publisher_tool = PublisherWindow( parent=self._parent_widget ) From c90e8fed53c2f6c50346684c90b728b990ff25b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 11:56:52 +0200 Subject: [PATCH 042/160] fix thumbnail publishing from standalone publisher --- .../standalonepublisher/plugins/publish/extract_thumbnail.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index 3ee2f70809..8d7ea07f42 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -118,6 +118,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): 'files': filename, "stagingDir": staging_dir, "tags": ["thumbnail", "delete"], + "thumbnail": True } if width and height: representation["width"] = width From 6f642ab34c09c617b0a0a10adc6d1821b901f337 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 16:56:28 +0200 Subject: [PATCH 043/160] trigger reset of controller when conversion finishes --- openpype/pipeline/create/context.py | 3 ++- openpype/tools/publisher/control.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index c87803c5c4..52a1729233 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1823,7 +1823,8 @@ class CreateContext: def run_convertors(self, convertor_identifiers): """Run convertor plugins by idenfitifiers. - Conversion is skipped if convertor is not available. + Conversion is skipped if convertor is not available. It is recommended + to trigger reset after conversion to reload instances. Args: convertor_identifiers (Iterator[str]): Identifiers of convertors diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 18d1a5b083..e05cffe20e 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1906,6 +1906,8 @@ class PublisherController(BasePublisherController): ) def trigger_convertor_items(self, convertor_identifiers): + self.save_changes() + success = True try: self._create_context.run_convertors(convertor_identifiers) @@ -1924,7 +1926,8 @@ class PublisherController(BasePublisherController): self.emit_card_message("Conversion finished") else: self.emit_card_message("Conversion failed", CardMessageTypes.error) - self._on_create_instance_change() + + self.reset() def create( self, creator_identifier, subset_name, instance_data, options @@ -1972,7 +1975,6 @@ class PublisherController(BasePublisherController): Args: instance_ids (List[str]): List of instance ids to remove. """ - # TODO expect instance ids instead of instances # QUESTION Expect that instances are really removed? In that case save # reset is not required and save changes too. self.save_changes() From 698fe8379ea78901418f4cc4f1d6f8fc941c40ae Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 25 Oct 2022 16:57:33 +0200 Subject: [PATCH 044/160] added logger to convertor --- openpype/pipeline/create/creator_plugins.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 584e082221..c69abb8861 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -61,9 +61,23 @@ class SubsetConvertorPlugin(object): create_context """ + _log = None + def __init__(self, create_context): self._create_context = create_context + @property + def log(self): + """Logger of the plugin. + + Returns: + logging.Logger: Logger with name of the plugin. + """ + + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + @abstractproperty def identifier(self): """Converted identifier. From 82ded66bd620eccaa2951d92e7de961aac121c4e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Oct 2022 11:37:53 +0200 Subject: [PATCH 045/160] add thumbnal if there are not reviewables --- .../ftrack/plugins/publish/integrate_ftrack_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index 53c6e69ac0..2d06e2ab02 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -151,7 +151,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): first_thumbnail_component = None first_thumbnail_component_repre = None - if has_movie_review: + if not review_representations or has_movie_review: for repre in thumbnail_representations: repre_path = self._get_repre_path(instance, repre, False) if not repre_path: From 3ce9bd26ffea646dd3373af039f1bba8fd0c18fa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 26 Oct 2022 11:53:32 +0200 Subject: [PATCH 046/160] adding synchronized to workflow --- .github/workflows/milestone_assign.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/milestone_assign.yml b/.github/workflows/milestone_assign.yml index b41886816b..c5a231e59e 100644 --- a/.github/workflows/milestone_assign.yml +++ b/.github/workflows/milestone_assign.yml @@ -2,7 +2,7 @@ name: Milestone - assign to PRs on: pull_request_target: - types: [opened, reopened, edited] + types: [opened, reopened, edited, synchronize] jobs: run_if_release: From 3ae02cfb2b570070d5b987969d4aa1667bbacbfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 26 Oct 2022 13:51:03 +0200 Subject: [PATCH 047/160] :bug: handle missing directory --- igniter/bootstrap_repos.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index ccc9d4ac52..addcbed24c 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -815,6 +815,13 @@ class BootstrapRepos: except Exception as e: self._print(str(e), LOG_ERROR, exc_info=True) return None + if not destination_dir.exists(): + destination_dir.mkdir(parents=True) + elif not destination_dir.is_dir(): + self._print( + "Destination exists but is not directory.", LOG_ERROR) + return None + try: shutil.move(zip_file.as_posix(), destination_dir.as_posix()) except shutil.Error as e: From b6a2be53d4f1e7d2caf827ad4d6b45e366e9b1b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Oct 2022 17:40:35 +0200 Subject: [PATCH 048/160] 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 910b2adfc7..c7d001e92e 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 049/160] 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 c7d001e92e..a8ca9af17d 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -10,6 +10,7 @@ from openpype.pipeline.create import ( from .widgets import ( IconValuePixmapLabel, CreateBtn, + ThumbnailWidget, ) from .assets_widget import CreateWidgetAssetsWidget from .tasks_widget import CreateWidgetTasksWidget @@ -23,11 +24,11 @@ from ..constants import ( 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 @@ class CreateWidget(QtWidgets.QWidget): # --- 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 @@ class CreateWidget(QtWidgets.QWidget): 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 @@ class CreateWidget(QtWidgets.QWidget): 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 @@ class CreateWidget(QtWidgets.QWidget): 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 @@ class CreateWidget(QtWidgets.QWidget): 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 @@ class CreateWidget(QtWidgets.QWidget): 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 050/160] modified thumbnail to paint the content on own --- openpype/tools/publisher/widgets/widgets.py | 169 +++++++++++++++++--- 1 file changed, 149 insertions(+), 20 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index d4c2623790..23ddeee2de 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -16,6 +16,7 @@ from openpype.tools.utils import ( 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 .assets_widget import AssetsDialog from .tasks_widget import TasksModel from .icons import ( get_pixmap, + get_image, get_icon_path ) @@ -1622,33 +1624,160 @@ class SubsetAttributesWidget(QtWidgets.QWidget): class ThumbnailWidget(QtWidgets.QWidget): - """Instance thumbnail widget. + """Instance thumbnail widget.""" + + width_ratio = 3.0 + height_ratio = 2.0 + border_width = 1 + offset_sep = 4 - 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) - # 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) + super(ThumbnailWidget, self).__init__(parent) - thumbnail_label = QtWidgets.QLabel(self) - thumbnail_label.setPixmap( - default_pix.scaled( - 200, 100, + # 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 - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(thumbnail_label, alignment=QtCore.Qt.AlignCenter) + 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) - self.thumbnail_label = thumbnail_label - self.default_pix = default_pix - self.current_pix = None + 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() From 89edb5cb9bfad4bc5fddfa04334e5e6444c0a1b1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Oct 2022 19:04:16 +0200 Subject: [PATCH 051/160] 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 23ddeee2de..58a023d5f4 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1570,7 +1570,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget): 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 052/160] 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 58a023d5f4..95ba321a63 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1650,11 +1650,25 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): )) 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 5d1fa90fccc1bd261c2af61eb639d677f372c363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 27 Oct 2022 12:40:44 +0200 Subject: [PATCH 053/160] :recycle: soft fail when applying preset --- .../maya/plugins/publish/extract_playblast.py | 2 +- openpype/vendor/python/common/capture.py | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 1b5b8d34e4..b19d24fad7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -133,7 +133,7 @@ class ExtractPlayblast(publish.Extractor): preset.update(panel_preset) cmds.setFocus(panel) - path = capture.capture(**preset) + path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) diff --git a/openpype/vendor/python/common/capture.py b/openpype/vendor/python/common/capture.py index 86c1c60e56..09a42d84d1 100644 --- a/openpype/vendor/python/common/capture.py +++ b/openpype/vendor/python/common/capture.py @@ -7,6 +7,7 @@ Playblasting with independent viewport, camera and display options import re import sys import contextlib +import logging from maya import cmds from maya import mel @@ -21,6 +22,7 @@ version_info = (2, 3, 0) __version__ = "%s.%s.%s" % version_info __license__ = "MIT" +logger = logging.getLogger("capture") def capture(camera=None, @@ -46,7 +48,8 @@ def capture(camera=None, display_options=None, viewport_options=None, viewport2_options=None, - complete_filename=None): + complete_filename=None, + log=None): """Playblast in an independent panel Arguments: @@ -91,6 +94,7 @@ def capture(camera=None, options, using `Viewport2Options` complete_filename (str, optional): Exact name of output file. Use this to override the output of `filename` so it excludes frame padding. + log (logger, optional): pass logger for logging messages. Example: >>> # Launch default capture @@ -109,7 +113,9 @@ def capture(camera=None, """ - + global logger + if log: + logger = log camera = camera or "persp" # Ensure camera exists @@ -736,7 +742,10 @@ def _applied_viewport_options(options, panel): plugin_options[plugin] = options.pop(plugin) # default options - cmds.modelEditor(panel, edit=True, **options) + try: + cmds.modelEditor(panel, edit=True, **options) + except TypeError as e: + logger.error("Cannot apply options {}".format(e)) # plugin display filter options for plugin, state in plugin_options.items(): From cc4d158c785d8a413076830aa556445d9292b234 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 15:12:51 +0200 Subject: [PATCH 054/160] 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 a8ca9af17d..7695101ad1 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -7,10 +7,10 @@ from openpype.pipeline.create import ( 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 0000000000..29bb6fb62f --- /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 95ba321a63..290f69f280 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -16,18 +16,17 @@ from openpype.tools.utils import ( 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 @@ class SubsetAttributesWidget(QtWidgets.QWidget): ) 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 055/160] 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 9919973b06..585adceb26 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 7695101ad1..d47c2a07e0 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -164,6 +164,7 @@ class CreateWidget(QtWidgets.QWidget): 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 056/160] 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 555041d389..f6dcce800d 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -14,7 +14,7 @@ from .pipeline import ( 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 c4fdf28d345170b2c972d922b4bde771f7de4e64 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 27 Oct 2022 15:24:57 +0200 Subject: [PATCH 057/160] nuke: fixing loader hash convertor --- openpype/hosts/nuke/plugins/load/load_clip.py | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 654ea367c8..aa5b1dfed1 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -1,3 +1,4 @@ +import os import nuke import qargparse @@ -84,6 +85,16 @@ class LoadClip(plugin.NukeLoader): + plugin.get_review_presets_config() ) + def _fix_path_for_knob(self, filepath, repre_cont): + basename = os.path.basename(filepath) + dirname = os.path.dirname(filepath) + frame = repre_cont.get("frame") + assert frame, "Representation is not sequence" + + padding = len(frame) + basename = basename.replace(frame, "#" * padding) + return os.path.join(dirname, basename).replace("\\", "/") + def load(self, context, name, namespace, options): repre = context["representation"] # reste container id so it is always unique for each instance @@ -91,7 +102,7 @@ class LoadClip(plugin.NukeLoader): is_sequence = len(repre["files"]) > 1 - file = self.fname.replace("\\", "/") + filepath = self.fname.replace("\\", "/") start_at_workfile = options.get( "start_at_workfile", self.options_defaults["start_at_workfile"]) @@ -121,18 +132,14 @@ class LoadClip(plugin.NukeLoader): duration = last - first first = 1 last = first + duration - elif "#" not in file: - frame = repre_cont.get("frame") - assert frame, "Representation is not sequence" - - padding = len(frame) - file = file.replace(frame, "#" * padding) + elif "#" not in filepath: + filepath = self._fix_path_for_knob(filepath, repre_cont) # Fallback to asset name when namespace is None if namespace is None: namespace = context['asset']['name'] - if not file: + if not filepath: self.log.warning( "Representation id `{}` is failing to load".format(repre_id)) return @@ -147,7 +154,7 @@ class LoadClip(plugin.NukeLoader): # to avoid multiple undo steps for rest of process # we will switch off undo-ing with viewer_update_and_undo_stop(): - read_node["file"].setValue(file) + read_node["file"].setValue(filepath) used_colorspace = self._set_colorspace( read_node, version_data, repre["data"]) @@ -218,7 +225,7 @@ class LoadClip(plugin.NukeLoader): is_sequence = len(representation["files"]) > 1 read_node = nuke.toNode(container['objectName']) - file = get_representation_path(representation).replace("\\", "/") + filepath = get_representation_path(representation).replace("\\", "/") start_at_workfile = "start at" in read_node['frame_mode'].value() @@ -251,14 +258,10 @@ class LoadClip(plugin.NukeLoader): duration = last - first first = 1 last = first + duration - elif "#" not in file: - frame = repre_cont.get("frame") - assert frame, "Representation is not sequence" + elif "#" not in filepath: + filepath = self._fix_path_for_knob(filepath, repre_cont) - padding = len(frame) - file = file.replace(frame, "#" * padding) - - if not file: + if not filepath: self.log.warning( "Representation id `{}` is failing to load".format(repre_id)) return @@ -266,14 +269,14 @@ class LoadClip(plugin.NukeLoader): read_name = self._get_node_name(representation) read_node["name"].setValue(read_name) - read_node["file"].setValue(file) + read_node["file"].setValue(filepath) # to avoid multiple undo steps for rest of process # we will switch off undo-ing with viewer_update_and_undo_stop(): used_colorspace = self._set_colorspace( read_node, version_data, representation["data"], - path=file) + path=filepath) self._set_range_to_node(read_node, first, last, start_at_workfile) From 733e3be8c469afc0b00782853bbfb17a6ccd324c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 27 Oct 2022 16:15:49 +0200 Subject: [PATCH 058/160] :recycle: optimize calls, fix representation dirname --- .../hosts/houdini/plugins/load/load_ass.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/houdini/plugins/load/load_ass.py b/openpype/hosts/houdini/plugins/load/load_ass.py index 57e2d34d7c..daabd1e405 100644 --- a/openpype/hosts/houdini/plugins/load/load_ass.py +++ b/openpype/hosts/houdini/plugins/load/load_ass.py @@ -1,11 +1,10 @@ import os - import re + from openpype.pipeline import ( load, get_representation_path, ) - from openpype.hosts.houdini.api import pipeline @@ -20,7 +19,6 @@ class AssLoader(load.LoaderPlugin): color = "orange" def load(self, context, name=None, namespace=None, data=None): - import hou # Get the root node @@ -35,8 +33,7 @@ class AssLoader(load.LoaderPlugin): procedural.setParms( { - "ar_filename": self.format_path( - self.fname, context["representation"]) + "ar_filename": self.format_path(context["representation"]) }) nodes = [procedural] @@ -52,10 +49,8 @@ class AssLoader(load.LoaderPlugin): ) def update(self, container, representation): - # Update the file path - file_path = get_representation_path(representation) - file_path = self.format_path(file_path, representation) + file_path = self.format_path(representation) procedural = container["node"] procedural.setParms({"ar_filename": file_path}) @@ -64,26 +59,31 @@ class AssLoader(load.LoaderPlugin): procedural.setParms({"representation": str(representation["_id"])}) def remove(self, container): - node = container["node"] node.destroy() @staticmethod - def format_path(path, representation): - """Format file path correctly for single bgeo or bgeo sequence.""" + def format_path(representation): + """Format file path correctly for single ass.* or ass.* sequence. + + Args: + representation (dict): representation to be loaded. + + Returns: + str: Formatted path to be used by the input node. + + """ + path = get_representation_path(representation) if not os.path.exists(path): - raise RuntimeError("Path does not exist: %s" % path) + raise RuntimeError("Path does not exist: {}".format(path)) is_sequence = bool(representation["context"].get("frame")) # The path is either a single file or sequence in a folder. - if not is_sequence: - filename = path - else: - filename = re.sub(r"(.*)\.(\d+)\.(ass.*)", "\\1.$F4.\\3", path) + if is_sequence: + dir_path, file_name = os.path.split(path) + path = os.path.join( + dir_path, + re.sub(r"(.*)\.(\d+)\.(ass.*)", "\\1.$F4.\\3", file_name) + ) - filename = os.path.join(path, filename) - - filename = os.path.normpath(filename) - filename = filename.replace("\\", "/") - - return filename + return os.path.normpath(path).replace("\\", "/") From 1b79f3162be3e7e827b0c6e95ac2412c0e26aa95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 27 Oct 2022 16:17:32 +0200 Subject: [PATCH 059/160] :recycle: unify the calls --- openpype/hosts/houdini/plugins/load/load_ass.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/load/load_ass.py b/openpype/hosts/houdini/plugins/load/load_ass.py index daabd1e405..710cd09c23 100644 --- a/openpype/hosts/houdini/plugins/load/load_ass.py +++ b/openpype/hosts/houdini/plugins/load/load_ass.py @@ -50,10 +50,8 @@ class AssLoader(load.LoaderPlugin): def update(self, container, representation): # Update the file path - file_path = self.format_path(representation) - procedural = container["node"] - procedural.setParms({"ar_filename": file_path}) + procedural.setParms({"ar_filename": self.format_path(representation)}) # Update attribute procedural.setParms({"representation": str(representation["_id"])}) From e13e59c6dc2bd9288d48f119aacb94893d46fa75 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 27 Oct 2022 17:10:01 +0200 Subject: [PATCH 060/160] hiero: fix effect collection --- .../hiero/plugins/publish/precollect_instances.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 84f2927fc7..1fc4b1f696 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -326,8 +326,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): return hiero_export.create_otio_time_range( frame_start, frame_duration, fps) - @staticmethod - def collect_sub_track_items(tracks): + def collect_sub_track_items(self, tracks): """ Returns dictionary with track index as key and list of subtracks """ @@ -336,8 +335,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin): for track in tracks: items = track.items() + effet_items = track.subTrackItems() + # skip if no clips on track > need track with effect only - if items: + if not effet_items: continue # skip all disabled tracks @@ -345,10 +346,11 @@ class PrecollectInstances(pyblish.api.ContextPlugin): continue track_index = track.trackIndex() - _sub_track_items = phiero.flatten(track.subTrackItems()) + _sub_track_items = phiero.flatten(effet_items) + _sub_track_items = list(_sub_track_items) # continue only if any subtrack items are collected - if not list(_sub_track_items): + if not _sub_track_items: continue enabled_sti = [] From 48c4c238f55bad7ae773abcf0c13d818180f0512 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 18:38:28 +0200 Subject: [PATCH 061/160] 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 2cf785d981..f5319c5a48 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -85,6 +85,7 @@ from .context_tools import ( 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 af0ee79f47..0ec19d50fe 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -5,6 +5,7 @@ import json import types import logging import platform +import uuid import pyblish.api from pyblish.lib import MessageHandler @@ -37,6 +38,7 @@ from . import ( _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 fbec44247a..579840c07d 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 062/160] 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 52a1729233..71338f96e0 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1077,6 +1077,8 @@ class CreateContext: # 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 @@ class CreateContext: 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 063/160] 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 4b91951a08..89b876e6de 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 @@ from .legacy_create import ( __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 3af9651947..375cfc4a12 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 c69abb8861..1e8423e48b 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -442,6 +442,13 @@ class BaseCreator: 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 064/160] 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 3d93e2c927..5f8b2878b7 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 @@ class CollectMovieBatch( 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 7035a61d7b..183195a515 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 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): 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 7781bb7b3e..96aefe0043 100644 --- a/openpype/hosts/traypublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/traypublisher/plugins/publish/extract_thumbnail.py @@ -42,7 +42,15 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): "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 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): 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 fc0f97b187..ddb6908a4c 100644 --- a/openpype/plugins/publish/collect_from_create_context.py +++ b/openpype/plugins/publish/collect_from_create_context.py @@ -19,14 +19,28 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): 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 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): 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 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): "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 065/160] 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 f6dcce800d..0b62492477 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 066/160] 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 0b62492477..40877968e9 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -4,7 +4,8 @@ from openpype.lib.attribute_definitions import FileDef from openpype.pipeline.create import ( Creator, HiddenCreator, - CreatedInstance + CreatedInstance, + PRE_CREATE_THUMBNAIL_KEY, ) from .pipeline import ( @@ -114,11 +115,14 @@ class TrayPublishCreator(Creator): 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 @@ class SettingsCreator(TrayPublishCreator): 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 067/160] 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 1e8423e48b..ef92b7ccc4 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 068/160] 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 e05cffe20e..7a2f2bbb82 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -4,6 +4,8 @@ import logging import traceback import collections import uuid +import tempfile +import shutil from abc import ABCMeta, abstractmethod, abstractproperty import six @@ -24,6 +26,7 @@ from openpype.pipeline import ( KnownPublishError, registered_host, legacy_io, + get_process_id, ) from openpype.pipeline.create import ( CreateContext, @@ -1283,6 +1286,22 @@ class AbstractPublisherController(object): 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 @@ class BasePublisherController(AbstractPublisherController): 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 a3387043b8..77d4339052 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -383,6 +383,7 @@ class PublisherWindow(QtWidgets.QDialog): 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 069/160] 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 7a2f2bbb82..eb6425b820 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1259,6 +1259,14 @@ class AbstractPublisherController(object): 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 @@ class PublisherController(BasePublisherController): 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 070/160] 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 eb6425b820..b113c9316a 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -1118,11 +1118,13 @@ class AbstractPublisherController(object): 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 071/160] 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 56132a4046..8b5856f234 100644 --- a/openpype/tools/publisher/control_qt.py +++ b/openpype/tools/publisher/control_qt.py @@ -115,6 +115,11 @@ class QtRemotePublishController(BasePublisherController): 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 @@ class QtRemotePublishController(BasePublisherController): 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 @@ class QtRemotePublishController(BasePublisherController): 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 072/160] 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 d47c2a07e0..fc35cd31cd 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -196,7 +196,7 @@ class CreateWidget(QtWidgets.QWidget): 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 29bb6fb62f..c93b555d5b 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.lib.transcoding import ( 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): ) 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 290f69f280..ae32e5f42d 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1472,7 +1472,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget): # 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 073/160] 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 fc35cd31cd..a610c405a4 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 @@ class CreateWidget(QtWidgets.QWidget): 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 @@ class CreateWidget(QtWidgets.QWidget): self._prereq_timer = prereq_timer self._first_show = True + self._last_thumbnail_path = None @property def current_asset_name(self): @@ -492,6 +496,14 @@ class CreateWidget(QtWidgets.QWidget): 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 @@ class CreateWidget(QtWidgets.QWidget): 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 @@ class CreateWidget(QtWidgets.QWidget): 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 074/160] 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 ae32e5f42d..1682e3e047 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -3,6 +3,8 @@ import os import re import copy import functools +import uuid +import shutil import collections from Qt import QtWidgets, QtCore, QtGui import qtawesome @@ -1064,6 +1066,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): def _on_submit(self): """Commit changes for selected instances.""" + variant_value = None asset_name = None task_name = None @@ -1132,6 +1135,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): 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 @@ class CreatorAttrsWidget(QtWidgets.QWidget): 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 @@ class CreatorAttrsWidget(QtWidgets.QWidget): 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 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): 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 @@ class SubsetAttributesWidget(QtWidgets.QWidget): 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 @@ class SubsetAttributesWidget(QtWidgets.QWidget): """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 @@ class SubsetAttributesWidget(QtWidgets.QWidget): ) 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 075/160] 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 b113c9316a..10734a69f4 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -828,6 +828,7 @@ class CreatorItem: default_variant, default_variants, create_allow_context_change, + create_allow_thumbnail, pre_create_attributes_defs ): self.identifier = identifier @@ -841,6 +842,7 @@ class CreatorItem: 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 @@ class CreatorItem: 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 @@ class CreatorItem: 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 @@ class CreatorItem: default_variant, default_variants, create_allow_context_change, + create_allow_thumbnail, pre_create_attr_defs ) @@ -917,6 +922,7 @@ class CreatorItem: "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 076/160] 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 c93b555d5b..8c43602147 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -44,20 +44,25 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 077/160] 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 8bea69c812..74337ea1ab 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -20,9 +20,10 @@ INSTANCE_ID_ROLE = QtCore.Qt.UserRole + 1 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 a610c405a4..f0db132d98 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -20,7 +20,8 @@ from .precreate_widget import PreCreateWidget from ..constants import ( VARIANT_TOOLTIP, CREATOR_IDENTIFIER_ROLE, - FAMILY_ROLE + FAMILY_ROLE, + CREATOR_THUMBNAIL_ENABLED_ROLE, ) SEPARATORS = ("---separator---", "---") @@ -457,6 +458,10 @@ class CreateWidget(QtWidgets.QWidget): 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 @@ class CreateWidget(QtWidgets.QWidget): 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 @@ class CreateWidget(QtWidgets.QWidget): 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 33416b4658a4df85dc1dd44cb2199bfed5615dbc Mon Sep 17 00:00:00 2001 From: "Ryan J. Quinlan" Date: Thu, 27 Oct 2022 13:59:35 -0700 Subject: [PATCH 078/160] Formatting and wording changes to admin_settings --- website/docs/admin_settings.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/website/docs/admin_settings.md b/website/docs/admin_settings.md index 9b00e6c612..d27ffe8d4c 100644 --- a/website/docs/admin_settings.md +++ b/website/docs/admin_settings.md @@ -11,7 +11,7 @@ OpenPype stores all of it's settings and configuration in the mongo database. To **Settings** GUI can be started from the tray menu Admin -> Studio Settings. -Please keep in mind that these settings are set-up for the full studio and not per-individual. If you're looking for individual artist settings, you can head to +Please keep in mind that these settings are set-up for the full studio and not per-individual. If you're looking for individual artist settings, you can head to [Local Settings](admin_settings_local.md) section in the artist documentation. ## Categories @@ -31,32 +31,32 @@ You'll find that settings are split into categories: System sections contains all settings that can be configured on a studio level, but cannot be changed on a per-project basis. These include mostly high level options like path to mongo database, toggling major modules on and off and configuring studio wide application -availability. +availability. ### Project -Project tab contains most of OpenPype settings and all of them can be configured and overridden on a per-project basis if need be. This includes most of the workflow behaviors -like what formats to export, naming conventions, publishing validations, automatic assets loaders and a lot more. +Project tab contains most of OpenPype settings and all of them can be configured and overridden on a per-project basis if need be. This includes most of the workflow behaviors +like what formats to export, naming conventions, publishing validations, automatic assets loaders and a lot more. We recommend to try to keep as many configurations as possible on a studio level and only override selectively, because micromanaging all of the project settings might become cumbersome down the line. Most of the settings can be safely adjusted and locked on a project after the production started. ## Understanding Overrides -Most of the individual settings can be set and overridden on multiple levels. +Most of the individual settings can be set and overridden on multiple levels. ### OpenPype defaults -When you first open settings all of the values and categories will be marked with a -light **grey labels** or a **grey vertical bar** on the left edge of the expandable category. +When you first open settings, all of the values and categories will be marked with either +light **grey labels** or a **grey vertical bar** on the left edge of the expandable category. -That means, the value has been left at OpenPype Default. If the default changes in future +The grey colouring signifies the value has been left at OpenPype Default. If the default changes in future OpenPype versions, these values will be reflect the change after you deploy the new version. ### Studio defaults Any values that you change and then press save in the bottom right corner, will be saved -as studio defaults. This means they will stay at those values even if you update your pype. -To make it clear which settings are set by you specifically, they are marked with a **green +as studio defaults. This means they will stay at those values even if you update your pype. +To make it clear which settings are set by you specifically, they are marked with a **green edge** and **green labels**, once set. To set studio default, just change the value in the system tab and press save. If you want @@ -76,10 +76,13 @@ You can also reset any settings to OpenPype default by doing `right click` and ` Many settings are useful to be adjusted on a per-project basis. To identify project overrides, they are marked with **orange edge** and **orange labels** in the settings GUI. -To set project overrides proceed the same way as with the Studio defaults, but first select -a particular project you want to be configuring on the left hand side of the Project Settings tab. +The process of settting project overrides is similar to setting the Studio defaults. The key difference is to select a particular project you want to be configure. Those projects can be found on the left hand side of the Project Settings tab. + +In the image below you can see all three overrides at the same time. +1. Deadline has **no changes to the OpenPype defaults** at all — **grey** colour of left bar. +2. Maya has **studio-wide defaults configured**, which are inherited in the particular project - **green** colour of left bar. +3. Nuke contains **project specific overrides** - **orange** colour of left bar. -Here you can see all three overrides at the same time. Deadline has not studio changes at all, Maya has some studio defaults configures and Nuke also contains project specific overrides. ![colours_01](assets/settings/colours_02.png) Override colours work as breadcrumbs to allow quick identification of what was changed and where. As you can see on this image, Orange colour is propagated up the hierarchy even though only a single value (sync render version with workfile toggle), was changed. From 2c0e59da6af61567487a49326aa9afda65b175dd Mon Sep 17 00:00:00 2001 From: "Ryan J. Quinlan" Date: Thu, 27 Oct 2022 14:02:06 -0700 Subject: [PATCH 079/160] Italicized menu path --- website/docs/admin_settings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_settings.md b/website/docs/admin_settings.md index d27ffe8d4c..8626ef16ba 100644 --- a/website/docs/admin_settings.md +++ b/website/docs/admin_settings.md @@ -9,7 +9,7 @@ import TabItem from '@theme/TabItem'; OpenPype stores all of it's settings and configuration in the mongo database. To make the configuration as easy as possible we provide a robust GUI where you can access and change everything that is configurable -**Settings** GUI can be started from the tray menu Admin -> Studio Settings. +**Settings** GUI can be started from the tray menu *Admin -> Studio Settings*. Please keep in mind that these settings are set-up for the full studio and not per-individual. If you're looking for individual artist settings, you can head to [Local Settings](admin_settings_local.md) section in the artist documentation. From 5a3856b5654d75553b8abfe6e989995e8a17dd6c Mon Sep 17 00:00:00 2001 From: "Ryan J. Quinlan" Date: Thu, 27 Oct 2022 14:06:34 -0700 Subject: [PATCH 080/160] Fixed spelling and wording --- website/docs/admin_settings_system.md | 42 +++++++++++++-------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index 66715e7288..92f522104a 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -19,11 +19,11 @@ Settings applicable to the full studio. **`Admin Password`** - After setting admin password, normal user won't have access to OpenPype settings and Project Manager GUI. Please keep in mind that this is a studio wide password and it is meant purely -as a naive barier to prevent artists from accidental setting changes. +as a simple barrier to prevent artists from accidental setting changes. **`Environment`** - Globally applied environment variables that will be appended to any OpenPype process in the studio. -**`Disk mapping`** - Platform dependent configuration for mapping of virtual disk(s) on an artist's OpenPype machines before OP starts up. +**`Disk mapping`** - Platform dependent configuration for mapping of virtual disk(s) on an artist's OpenPype machines before OP starts up. Uses `subst` command, if configured volume character in `Destination` field already exists, no re-mapping is done for that character(volume). ### FFmpeg and OpenImageIO tools @@ -58,10 +58,10 @@ their own attributes that need to be set, before they become fully functional. ### Avalon -**`Avalon Mongo Timeout`** - You might need to change this if your mongo connection is a bit slow. Making the +**`Avalon Mongo Timeout`** - You might need to change this if your mongo connection is a bit slow. Making the timeout longer will give Avalon better chance to connect. -**`Thumbnail Storage Location`** - simple disk storage path, where all thumbnails will be stored. +**`Thumbnail Storage Location`** - simple disk storage path, where all thumbnails will be stored. ### Ftrack @@ -89,15 +89,15 @@ Disable/Enable Standalone Publisher option ### Deadline -**`Deadline Rest URL`** - URL to deadline webservice that. This URL must be reachable from every +**`Deadline Rest URL`** - URL to deadline webservice that. This URL must be reachable from every workstation that should be submitting render jobs to deadline via OpenPype. ### Muster -**`Muster Rest URL`** - URL to Muster webservice that. This URL must be reachable from every +**`Muster Rest URL`** - URL to Muster webservice that. This URL must be reachable from every workstation that should be submitting render jobs to muster via OpenPype. -**`templates mapping`** - you can customize Muster templates to match your existing setup here. +**`templates mapping`** - you can customize Muster templates to match your existing setup here. ### Clockify @@ -107,36 +107,36 @@ workstation that should be submitting render jobs to muster via OpenPype. **`Max Idle Time`** - Duration (minutes) of inactivity, after which currently running timer will be stopped. -**`Dialog popup time`** - Time in minutes, before the end of Max Idle ti, when a notification will alert +**`Dialog popup time`** - Time in minutes, before the end of Max Idle ti, when a notification will alert the user that their timer is about to be stopped. ### Idle Manager Service monitoring the activity, which triggers the Timers Manager timeouts. -### Logging +### Logging Module that allows storing all logging into the database for easier retrieval and support. ## Applications -In this section you can manage what Applications are available to your studio, locations of their -executables and their additional environments. In OpenPype context each application that is integrated is +In this section you can manage what Applications are available to your studio, locations of their +executables and their additional environments. In OpenPype context each application that is integrated is also called a `Host` and these two terms might be used interchangeably in the documentation. -Each Host is made of two levels. +Each Host is made of two levels. 1. **Application group** - This is the main name of the application and you can define extra environments that are applicable to all versions of the given application. For example any extra Maya scripts that are not version dependent, can be added to `Maya` environment here. -2. **Application versions** - Here you can define executables (per platform) for each supported version of -the DCC and any default arguments (`--nukex` for instance). You can also further extend it's environment. +2. **Application versions** - Here you can define executables (per platform) for each supported version of +the DCC and any default arguments (`--nukex` for instance). You can also further extend it's environment. ![settings_applications](assets/settings/applications_01.png) ### Environments -Please keep in mind that the environments are not additive by default, so if you are extending variables like -`PYTHONPATH`, or `PATH` make sure that you add themselves to the end of the list. +Please keep in mind that the environments are not additive by default, so if you are extending variables like +`PYTHONPATH`, or `PATH` make sure that you add themselves to the end of the list. For instance: @@ -151,7 +151,7 @@ For instance: ### Adding versions -It is possible to add new version for any supported application. There are two ways of doing it. +It is possible to add new version for any supported application. There are two ways of doing it. 1. **Add new executable** to an existing application version. This is a good way if you have multiple fully compatible versions of your DCC across the studio. Nuke is a typical example where multiple artists might have different `v#` releases of the same minor Nuke release. For example `12.2v3` and `12.3v6`. When you add both to `12.2` Nuke executables they will be treated the same in OpenPype and the system will automatically pick the first that it finds on an artist machine when launching. Their order is also the order of their priority when choosing which version to run if multiple are present. ![settings_applications](assets/settings/settings_addapplication.gif) @@ -161,16 +161,16 @@ It is possible to add new version for any supported application. There are two w ## Tools -A tool in openPype is anything that needs to be selectively added to your DCC applications. Most often these are plugins, modules, extensions or similar depending on what your package happens to call it. +A tool in openPype is anything that needs to be selectively added to your DCC applications. Most often these are plugins, modules, extensions or similar depending on what your package happens to call it. OpenPype comes with some major CG renderers pre-configured as an example, but these and any others will need to be changed to match your particular environment. -Their environment settings are split to two levels just like applications to allow more flexibility when setting them up. +Their environment settings are split to two levels just like applications to allow more flexibility when setting them up. -In the image before you can see that we set most of the environment variables in the general MTOA level, and only specify the version variable in the individual versions below. Because all environments within pype setting will resolve any cross references, this is enough to get a fully dynamic plugin loading as far as your folder structure where you store the plugins is nicely organized. +In the image before you can see that we set most of the environment variables in the general MTOA level, and only specify the version variable in the individual versions below. Because all environments within pype setting will resolve any cross references, this is enough to get a fully dynamic plugin loading as far as your folder structure where you store the plugins is nicely organized. -In this example MTOA will automatically will the `MAYA_VERSION`(which is set by Maya Application environment) and `MTOA_VERSION` into the `MTOA` variable. We then use the `MTOA` to set all the other variables needed for it to function within Maya. +In this example MTOA will automatically will the `MAYA_VERSION`(which is set by Maya Application environment) and `MTOA_VERSION` into the `MTOA` variable. We then use the `MTOA` to set all the other variables needed for it to function within Maya. ![tools](assets/settings/tools_01.png) All of the tools defined in here can then be assigned to projects. You can also change the tools versions on any project level all the way down to individual asset or shot overrides. So if you just need to upgrade you render plugin for a single shot, while not risking the incompatibilities on the rest of the project, it is possible. \ No newline at end of file From 3a08324a879471afa5d5916a60227c948f9e3df2 Mon Sep 17 00:00:00 2001 From: "Ryan J. Quinlan" Date: Thu, 27 Oct 2022 14:27:13 -0700 Subject: [PATCH 081/160] Fixed some wording and grammar. --- website/docs/admin_settings_system.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index 92f522104a..f03a4e8c7b 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -27,7 +27,8 @@ as a simple barrier to prevent artists from accidental setting changes. Uses `subst` command, if configured volume character in `Destination` field already exists, no re-mapping is done for that character(volume). ### FFmpeg and OpenImageIO tools -We bundle FFmpeg tools for all platforms and OpenImageIO tools for Windows and Linux. By default are used bundled tools but it is possible to set environment variables `OPENPYPE_FFMPEG_PATHS` and `OPENPYPE_OIIO_PATHS` in system settings environments to look for them in different directory e.g. for different linux distributions or to add oiio support for MacOs. Values of both environment variables should lead to directory where tool executables are located (multiple paths are supported). +We bundle FFmpeg tools for all platforms and OpenImageIO tools for Windows and Linux. By default, bundled tools are used, but it is possible to set environment variables `OPENPYPE_FFMPEG_PATHS` and `OPENPYPE_OIIO_PATHS` in system settings environments to look for them in different directory. +For example—when using different Linux distributions in a facility that do not have a consistent install location or to add OIIO support for MacOS. Values of both environment variables should lead to directory where tool executables are located instead of an explicit path to the binary executable. Using multiple paths are supported, separated by colons, is supported—e.g. */usr/local/bin:$HOME/.local/bin* ### OpenPype deployment control **`Versions Repository`** - Location where automatic update mechanism searches for zip files with @@ -41,11 +42,11 @@ For more information about Production and Staging go to [Distribute](admin_distr **Production version** and **Staging version** fields will define which version will be used in studio. Filling explicit version will force new OpenPype processes to use it. That gives more control over studio deployment especially when some workstations don't have access to version repository (e.g. remote users). It can be also used to downgrade studio version when newer version have production breaking bug. -When fields are not filled the latest version in versions repository is used as studio version. That makes updating easier as it is not needed to modify settings but workstations without access to versions repository can't find out which OpenPype version should be used. +When fields are not filled, the latest version in the versions repository is used as studio version. That makes updating easier as it is not needed to modify settings, though workstations without access to versions repository can't find out which OpenPype version should be used. -If version repository is not set or is not accessible for workstation the latest available version on workstation is used or version inside build. +If **`Version Repository`** is not set or is not accessible for workstation, the latest available version on workstation is used or the version inside build. -**`Version check interval`** - OpenPype tray application check if currently used OpenPype version is up to date with production/staging version. It is possible to modify how often the validation is triggered in minutes. It is possible to set the interval to `0`. That will turn off version validations but it is not recommend. +**`Version check interval`** - The OpenPype tray application has the ability to check if its version currently in use is up to date with the Studio's production/staging version. It is possible to modify how often the validation is triggered in minutes. The interval can also be set to `0`, which will turn off version validations, but it is not recommend. A dialog asking for restart is shown when OpenPype tray application detect that different version should be used. ![general_settings](assets/settings/settings_system_version_update.png) From fae3f14af49f59e3ddc945dfe25a2450f8e45c9f Mon Sep 17 00:00:00 2001 From: "Ryan J. Quinlan" Date: Thu, 27 Oct 2022 14:31:15 -0700 Subject: [PATCH 082/160] Modified working --- website/docs/admin_settings_system.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index f03a4e8c7b..00936d724c 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -54,15 +54,13 @@ A dialog asking for restart is shown when OpenPype tray application detect that ## Modules -Configuration of OpenPype modules. Some can only be turned on and off, others have -their own attributes that need to be set, before they become fully functional. +Configuration of OpenPype's various modules. Some can only be toggled on or off, while others have their own attributes that need to be set before they become fully functional. ### Avalon -**`Avalon Mongo Timeout`** - You might need to change this if your mongo connection is a bit slow. Making the -timeout longer will give Avalon better chance to connect. +**`Avalon Mongo Timeout`** - This might need to be changed if your mongo connection is a bit slow. Making the timeout longer will give Avalon better chance to connect. -**`Thumbnail Storage Location`** - simple disk storage path, where all thumbnails will be stored. +**`Thumbnail Storage Location`** - simple disk storage path where all thumbnails will be stored. ### Ftrack From 4f07ddae9d0cfe5a64d212af6f98180c60e42c12 Mon Sep 17 00:00:00 2001 From: "Ryan J. Quinlan" Date: Thu, 27 Oct 2022 14:34:27 -0700 Subject: [PATCH 083/160] Modified wording for clarity. --- website/docs/admin_settings_system.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index 00936d724c..c1d6c0a9ef 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -75,8 +75,7 @@ Additional Action paths **`Intent`** - Special ftrack attribute that mark the intention of individual publishes. This setting will be reflected in publisher as well as ftrack custom attributes -**`Custom Attributes`** - Write and Read permissions for all OpenPype required ftrack custom attributes. The values should be -ftrack roles names. +**`Custom Attributes`** - Write and Read permissions for all OpenPype required ftrack custom attributes. Each values needs to be name of an ftrack role. ### Sync Server From c81b42d6a6d86d62e5c829b32d3093a1e4d502a7 Mon Sep 17 00:00:00 2001 From: "Ryan J. Quinlan" Date: Thu, 27 Oct 2022 14:37:25 -0700 Subject: [PATCH 084/160] Added punctuation for clarity. --- website/docs/admin_settings_system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_settings_system.md b/website/docs/admin_settings_system.md index c1d6c0a9ef..8aeb281109 100644 --- a/website/docs/admin_settings_system.md +++ b/website/docs/admin_settings_system.md @@ -119,7 +119,7 @@ Module that allows storing all logging into the database for easier retrieval an ## Applications In this section you can manage what Applications are available to your studio, locations of their -executables and their additional environments. In OpenPype context each application that is integrated is +executables, and their additional environments. In OpenPype context, each application that is integrated is also called a `Host` and these two terms might be used interchangeably in the documentation. Each Host is made of two levels. From f8cf9ce3a13ae012bcc30d3d0b2c83070ceb5052 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 28 Oct 2022 10:15:20 +0200 Subject: [PATCH 085/160] store loaded reports locally on machine to specific directory and allow label changes --- .../publisher/publish_report_viewer/window.py | 341 +++++++++++++----- 1 file changed, 245 insertions(+), 96 deletions(-) diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py index 678884677c..2c249d058c 100644 --- a/openpype/tools/publisher/publish_report_viewer/window.py +++ b/openpype/tools/publisher/publish_report_viewer/window.py @@ -1,11 +1,12 @@ import os import json import six +import uuid + import appdirs from Qt import QtWidgets, QtCore, QtGui from openpype import style -from openpype.lib import JSONSettingRegistry from openpype.resources import get_openpype_icon_filepath from openpype.tools import resources from openpype.tools.utils import ( @@ -23,38 +24,198 @@ else: from report_items import PublishReport -FILEPATH_ROLE = QtCore.Qt.UserRole + 1 -MODIFIED_ROLE = QtCore.Qt.UserRole + 2 +ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 -class PublisherReportRegistry(JSONSettingRegistry): - """Class handling storing publish report tool. - - Attributes: - vendor (str): Name used for path construction. - product (str): Additional name used for path construction. +def get_reports_dir(): + """Root directory where publish reports are stored for next session. + Returns: + str: Path to directory where reports are stored. """ + report_dir = os.path.join( + appdirs.user_data_dir("openpype", "pypeclub"), + "publish_report_viewer" + ) + if not os.path.exists(report_dir): + os.makedirs(report_dir) + return report_dir + + +class PublishReportItem: + """Report item representing one file in report directory.""" + + def __init__(self, content): + item_id = content.get("id") + changed = False + if not item_id: + item_id = str(uuid.uuid4()) + changed = True + content["id"] = item_id + + if not content.get("report_version"): + changed = True + content["report_version"] = "0.0.1" + + report_path = os.path.join(get_reports_dir(), item_id) + file_modified = None + if os.path.exists(report_path): + file_modified = os.path.getmtime(report_path) + self.content = content + self.report_path = report_path + self.file_modified = file_modified + self._loaded_label = content.get("label") + self._changed = changed + self.publish_report = PublishReport(content) + + @property + def version(self): + return self.content["report_version"] + + @property + def id(self): + return self.content["id"] + + def get_label(self): + return self.content.get("label") or "Unfilled label" + + def set_label(self, label): + if not label: + self.content.pop("label", None) + self.content["label"] = label + + label = property(get_label, set_label) + + def save(self): + save = False + if ( + self._changed + or self._loaded_label != self.label + or not os.path.exists(self.report_path) + or self.file_modified != os.path.getmtime(self.report_path) + ): + save = True + + if not save: + return + + with open(self.report_path, "w") as stream: + json.dump(self.content, stream) + + self._loaded_label = self.content.get("label") + self._changed = False + self.file_modified = os.path.getmtime(self.report_path) + + @classmethod + def from_filepath(cls, filepath): + if not os.path.exists(filepath): + return None + + try: + with open(filepath, "r") as stream: + content = json.load(stream) + + return cls(content) + except Exception: + return None + + def remove_file(self): + if os.path.exists(self.report_path): + os.remove(self.report_path) + + def update_file_content(self): + if not os.path.exists(self.report_path): + return + + file_modified = os.path.getmtime(self.report_path) + if file_modified == self.file_modified: + return + + with open(self.report_path, "r") as stream: + content = json.load(self.content, stream) + + item_id = content.get("id") + version = content.get("report_version") + if not item_id: + item_id = str(uuid.uuid4()) + content["id"] = item_id + + if not version: + version = "0.0.1" + content["report_version"] = version + + self.content = content + self.file_modified = file_modified + + +class PublisherReportHandler: + """Class handling storing publish report tool.""" + def __init__(self): - self.vendor = "pypeclub" - self.product = "openpype" - name = "publish_report_viewer" - path = appdirs.user_data_dir(self.product, self.vendor) - super(PublisherReportRegistry, self).__init__(name, path) + self._reports = None + self._reports_by_id = {} + + def reset(self): + self._reports = None + self._reports_by_id = {} + + def list_reports(self): + if self._reports is not None: + return self._reports + + reports = [] + reports_by_id = {} + report_dir = get_reports_dir() + for filename in os.listdir(report_dir): + ext = os.path.splitext(filename)[-1] + if ext == ".json": + continue + filepath = os.path.join(report_dir, filename) + item = PublishReportItem.from_filepath(filepath) + reports.append(item) + reports_by_id[item.id] = item + + self._reports = reports + self._reports_by_id = reports_by_id + return reports + + def remove_report_items(self, item_id): + item = self._reports_by_id.get(item_id) + if item: + try: + item.remove_file() + self._reports_by_id.get(item_id) + except Exception: + pass -class LoadedFilesMopdel(QtGui.QStandardItemModel): +class LoadedFilesModel(QtGui.QStandardItemModel): def __init__(self, *args, **kwargs): - super(LoadedFilesMopdel, self).__init__(*args, **kwargs) - self.setColumnCount(2) - self._items_by_filepath = {} - self._reports_by_filepath = {} + super(LoadedFilesModel, self).__init__(*args, **kwargs) - self._registry = PublisherReportRegistry() + self._items_by_id = {} + self._report_items_by_id = {} + + self._handler = PublisherReportHandler() self._loading_registry = False - self._load_registry() + + def refresh(self): + self._handler.reset() + self._items_by_id = {} + self._report_items_by_id = {} + + new_items = [] + for report_item in self._handler.list_reports(): + item = self._create_item(report_item) + self._report_items_by_id[report_item.id] = report_item + self._items_by_id[report_item.id] = item + new_items.append(item) + + if new_items: + root_item = self.invisibleRootItem() + root_item.appendRows(new_items) def headerData(self, section, orientation, role): if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): @@ -63,22 +224,7 @@ class LoadedFilesMopdel(QtGui.QStandardItemModel): if section == 1: return "Modified" return "" - super(LoadedFilesMopdel, self).headerData(section, orientation, role) - - def _load_registry(self): - self._loading_registry = True - try: - filepaths = self._registry.get_item("filepaths") - self.add_filepaths(filepaths) - except ValueError: - pass - self._loading_registry = False - - def _store_registry(self): - if self._loading_registry: - return - filepaths = list(self._items_by_filepath.keys()) - self._registry.set_item("filepaths", filepaths) + super(LoadedFilesModel, self).headerData(section, orientation, role) def data(self, index, role=None): if role is None: @@ -88,17 +234,28 @@ class LoadedFilesMopdel(QtGui.QStandardItemModel): if col != 0: index = self.index(index.row(), 0, index.parent()) - if role == QtCore.Qt.ToolTipRole: - if col == 0: - role = FILEPATH_ROLE - elif col == 1: - return "File modified" + return super(LoadedFilesModel, self).data(index, role) + + def setData(self, index, value, role): + if role == QtCore.Qt.EditRole: + item_id = index.data(ITEM_ID_ROLE) + report_item = self._report_items_by_id.get(item_id) + if report_item is not None: + report_item.label = value + report_item.save() + value = report_item.label + + return super(LoadedFilesModel, self).setData(index, value, role) + + def _create_item(self, report_item): + if report_item.id in self._items_by_id: return None - elif role == QtCore.Qt.DisplayRole: - if col == 1: - role = MODIFIED_ROLE - return super(LoadedFilesMopdel, self).data(index, role) + item = QtGui.QStandardItem(report_item.label) + item.setColumnCount(self.columnCount()) + item.setData(report_item.id, ITEM_ID_ROLE) + + return item def add_filepaths(self, filepaths): if not filepaths: @@ -110,9 +267,6 @@ class LoadedFilesMopdel(QtGui.QStandardItemModel): filtered_paths = [] for filepath in filepaths: normalized_path = os.path.normpath(filepath) - if normalized_path in self._items_by_filepath: - continue - if ( os.path.exists(normalized_path) and normalized_path not in filtered_paths @@ -127,54 +281,46 @@ class LoadedFilesMopdel(QtGui.QStandardItemModel): try: with open(normalized_path, "r") as stream: data = json.load(stream) - report = PublishReport(data) + report_item = PublishReportItem(data) except Exception: # TODO handle errors continue - modified = os.path.getmtime(normalized_path) - item = QtGui.QStandardItem(os.path.basename(normalized_path)) - item.setColumnCount(self.columnCount()) - item.setData(normalized_path, FILEPATH_ROLE) - item.setData(modified, MODIFIED_ROLE) + label = data.get("label") + if not label: + report_item.label = ( + os.path.splitext(os.path.basename(filepath))[0] + ) + + item = self._create_item(report_item) + if item is None: + continue + new_items.append(item) - self._items_by_filepath[normalized_path] = item - self._reports_by_filepath[normalized_path] = report + report_item.save() + self._items_by_id[report_item.id] = item + self._report_items_by_id[report_item.id] = report_item - if not new_items: + if new_items: + root_item = self.invisibleRootItem() + root_item.appendRows(new_items) + + def remove_item_by_id(self, item_id): + report_item = self._report_items_by_id.get(item_id) + if not report_item: return + self._handler.remove_report_items(item_id) + item = self._items_by_id.get(item_id) + parent = self.invisibleRootItem() - parent.appendRows(new_items) + parent.removeRow(item.row()) - self._store_registry() - - def remove_filepaths(self, filepaths): - if not filepaths: - return - - if isinstance(filepaths, six.string_types): - filepaths = [filepaths] - - filtered_paths = [] - for filepath in filepaths: - normalized_path = os.path.normpath(filepath) - if normalized_path in self._items_by_filepath: - filtered_paths.append(normalized_path) - - if not filtered_paths: - return - - parent = self.invisibleRootItem() - for filepath in filtered_paths: - self._reports_by_filepath.pop(normalized_path) - item = self._items_by_filepath.pop(filepath) - parent.removeRow(item.row()) - - self._store_registry() - - def get_report_by_filepath(self, filepath): - return self._reports_by_filepath.get(filepath) + def get_report_by_id(self, item_id): + report_item = self._report_items_by_id.get(item_id) + if report_item: + return report_item.publish_report + return None class LoadedFilesView(QtWidgets.QTreeView): @@ -182,11 +328,13 @@ class LoadedFilesView(QtWidgets.QTreeView): def __init__(self, *args, **kwargs): super(LoadedFilesView, self).__init__(*args, **kwargs) - self.setEditTriggers(self.NoEditTriggers) + self.setEditTriggers( + self.EditKeyPressed | self.SelectedClicked | self.DoubleClicked + ) self.setIndentation(0) self.setAlternatingRowColors(True) - model = LoadedFilesMopdel() + model = LoadedFilesModel() self.setModel(model) time_delegate = PrettyTimeDelegate() @@ -226,9 +374,10 @@ class LoadedFilesView(QtWidgets.QTreeView): def showEvent(self, event): super(LoadedFilesView, self).showEvent(event) - self._update_remove_btn() + self._model.refresh() header = self.header() header.resizeSections(header.ResizeToContents) + self._update_remove_btn() def _on_selection_change(self): self.selection_changed.emit() @@ -237,14 +386,14 @@ class LoadedFilesView(QtWidgets.QTreeView): self._model.add_filepaths(filepaths) self._fill_selection() - def remove_filepaths(self, filepaths): - self._model.remove_filepaths(filepaths) + def remove_item_by_id(self, item_id): + self._model.remove_item_by_id(item_id) self._fill_selection() def _on_remove_clicked(self): index = self.currentIndex() - filepath = index.data(FILEPATH_ROLE) - self.remove_filepaths(filepath) + item_id = index.data(ITEM_ID_ROLE) + self.remove_item_by_id(item_id) def _fill_selection(self): index = self.currentIndex() @@ -257,8 +406,8 @@ class LoadedFilesView(QtWidgets.QTreeView): def get_current_report(self): index = self.currentIndex() - filepath = index.data(FILEPATH_ROLE) - return self._model.get_report_by_filepath(filepath) + item_id = index.data(ITEM_ID_ROLE) + return self._model.get_report_by_id(item_id) class LoadedFilesWidget(QtWidgets.QWidget): From f18e5c5896b8dc1fdcc9c1b08044d1a31a240d89 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 28 Oct 2022 10:31:30 +0200 Subject: [PATCH 086/160] 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 087/160] 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 96aefe0043..eaf48df5cb 100644 --- a/openpype/plugins/publish/extract_thumbnail_from_source.py +++ b/openpype/plugins/publish/extract_thumbnail_from_source.py @@ -41,12 +41,6 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): 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 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): 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 088/160] 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 eaf48df5cb..1d75b6c381 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 c5790fa896f646401e765e2d77527f71e03c7d0a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 28 Oct 2022 10:50:56 +0200 Subject: [PATCH 089/160] :art: add switch method --- openpype/hosts/houdini/plugins/load/load_ass.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/houdini/plugins/load/load_ass.py b/openpype/hosts/houdini/plugins/load/load_ass.py index 710cd09c23..557d601677 100644 --- a/openpype/hosts/houdini/plugins/load/load_ass.py +++ b/openpype/hosts/houdini/plugins/load/load_ass.py @@ -85,3 +85,6 @@ class AssLoader(load.LoaderPlugin): ) return os.path.normpath(path).replace("\\", "/") + + def switch(self, container, representation): + self.update(container, representation) From a45be7d4b57c1108059b3d7887a21fb0afd8dd7f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 28 Oct 2022 11:23:25 +0200 Subject: [PATCH 090/160] nuke: add 13.2 variant --- .../system_settings/applications.json | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 42eeb06191..03499a8567 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -192,6 +192,24 @@ ] }, "variants": { + "13-2": { + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.2v1\\Nuke13.2.exe" + ], + "darwin": [], + "linux": [ + "/usr/local/Nuke13.2v1/Nuke13.2" + ] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": {} + }, "13-0": { "use_python_2": false, "executables": { @@ -281,6 +299,7 @@ "environment": {} }, "__dynamic_keys_labels__": { + "13-2": "13.2", "13-0": "13.0", "12-2": "12.2", "12-0": "12.0", @@ -301,6 +320,30 @@ ] }, "variants": { + "13-2": { + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.2v1\\Nuke13.2.exe" + ], + "darwin": [], + "linux": [ + "/usr/local/Nuke13.2v1/Nuke13.2" + ] + }, + "arguments": { + "windows": [ + "--nukex" + ], + "darwin": [ + "--nukex" + ], + "linux": [ + "--nukex" + ] + }, + "environment": {} + }, "13-0": { "use_python_2": false, "executables": { @@ -420,6 +463,7 @@ "environment": {} }, "__dynamic_keys_labels__": { + "13-2": "13.2", "13-0": "13.0", "12-2": "12.2", "12-0": "12.0", @@ -438,6 +482,30 @@ "TAG_ASSETBUILD_STARTUP": "0" }, "variants": { + "13-2": { + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.2v1\\Nuke13.2.exe" + ], + "darwin": [], + "linux": [ + "/usr/local/Nuke13.2v1/Nuke13.2" + ] + }, + "arguments": { + "windows": [ + "--studio" + ], + "darwin": [ + "--studio" + ], + "linux": [ + "--studio" + ] + }, + "environment": {} + }, "13-0": { "use_python_2": false, "executables": { @@ -555,6 +623,7 @@ "environment": {} }, "__dynamic_keys_labels__": { + "13-2": "13.2", "13-0": "13.0", "12-2": "12.2", "12-0": "12.0", @@ -573,6 +642,30 @@ "TAG_ASSETBUILD_STARTUP": "0" }, "variants": { + "13-2": { + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.2v1\\Nuke13.2.exe" + ], + "darwin": [], + "linux": [ + "/usr/local/Nuke13.2v1/Nuke13.2" + ] + }, + "arguments": { + "windows": [ + "--hiero" + ], + "darwin": [ + "--hiero" + ], + "linux": [ + "--hiero" + ] + }, + "environment": {} + }, "13-0": { "use_python_2": false, "executables": { @@ -692,6 +785,7 @@ "environment": {} }, "__dynamic_keys_labels__": { + "13-2": "13.2", "13-0": "13.0", "12-2": "12.2", "12-0": "12.0", From fec7df18f12334a11ff0cb61177993d5dc89f626 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 28 Oct 2022 12:07:20 +0200 Subject: [PATCH 091/160] 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 8c43602147..dcb18d9cb7 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -2,6 +2,7 @@ import os 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 092/160] 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 dcb18d9cb7..48f40f7b5b 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): ) 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 093/160] 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 1682e3e047..96addb70a3 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1673,6 +1673,11 @@ class SubsetAttributesWidget(QtWidgets.QWidget): 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 094/160] 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 96addb70a3..fb9bd761f4 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1687,4 +1687,5 @@ class SubsetAttributesWidget(QtWidgets.QWidget): 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 095/160] 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 1d75b6c381..df877cec29 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 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): "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 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): ) # Skip representation and try next one if wasn't created - if not thumbnail_created: - self.log.warning("Thumbanil has not been created.") - return + if thumbnail_created: + return full_output_path - new_repre = { - "name": "thumbnail", - "ext": "jpg", - "files": dst_filename, - "stagingDir": dst_staging, - "thumbnail": True, - "tags": ["thumbnail"] - } + self.log.warning("Thumbanil has not been created.") - # adding representation - self.log.debug( - "Adding thumbnail representation: {}".format(new_repre) - ) - instance.data["representations"].append(new_repre) - - 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 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): 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 096/160] integrate thumbnail can use context thumbnail (if is available) --- .../plugins/publish/integrate_thumbnail.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index e7046ba2ea..d8a3a00041 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 @@ class IntegrateThumbnails(pyblish.api.InstancePlugin): ).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\"" ) - return + 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"]) - version = get_version_by_id(project_name, thumb_repre["parent"]) - if not version: - raise AssertionError( - "There does not exist version with id {}".format( - str(thumb_repre["parent"]) - ) - ) - - # 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, version_id) + if not version: + raise AssertionError( + "There does not exist version with id {}".format( + str(version_id) + ) + ) + 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 097/160] 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 f0db132d98..e3c171912f 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -502,10 +502,6 @@ class CreateWidget(QtWidgets.QWidget): 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 098/160] 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 df877cec29..8da1213807 100644 --- a/openpype/plugins/publish/extract_thumbnail_from_source.py +++ b/openpype/plugins/publish/extract_thumbnail_from_source.py @@ -190,5 +190,5 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): 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 099/160] 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 e3c171912f..a57a8791a8 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 9e0993a53dc0e2aba4afd208bb977e8647e6d2ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 28 Oct 2022 14:44:14 +0200 Subject: [PATCH 100/160] Update openpype/hosts/nuke/plugins/load/load_clip.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/plugins/load/load_clip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index aa5b1dfed1..666312167f 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -91,7 +91,7 @@ class LoadClip(plugin.NukeLoader): frame = repre_cont.get("frame") assert frame, "Representation is not sequence" - padding = len(frame) + padding = len(str(frame)) basename = basename.replace(frame, "#" * padding) return os.path.join(dirname, basename).replace("\\", "/") From 302af6bc3a738351e12992fd260a8092cf2c4d47 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 28 Oct 2022 15:07:43 +0200 Subject: [PATCH 101/160] update changelog --- CHANGELOG.md | 39 +++++++++++++++- HISTORY.md | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5464c390ce..707b61676f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,43 @@ # Changelog -## [3.14.5](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.14.6](https://github.com/pypeclub/OpenPype/tree/HEAD) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.4...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.5...HEAD) + +### 📖 Documentation + +- Documentation: Minor updates to dev\_requirements.md [\#4025](https://github.com/pypeclub/OpenPype/pull/4025) + +**🆕 New features** + +- Nuke: add 13.2 variant [\#4041](https://github.com/pypeclub/OpenPype/pull/4041) + +**🚀 Enhancements** + +- Publish Report Viewer: Store reports locally on machine [\#4040](https://github.com/pypeclub/OpenPype/pull/4040) +- General: More specific error in burnins script [\#4026](https://github.com/pypeclub/OpenPype/pull/4026) +- General: Extract review does not crash with old settings overrides [\#4023](https://github.com/pypeclub/OpenPype/pull/4023) +- Publisher: Convertors for legacy instances [\#4020](https://github.com/pypeclub/OpenPype/pull/4020) +- workflows: adding milestone creator and assigner [\#4018](https://github.com/pypeclub/OpenPype/pull/4018) +- Publisher: Catch creator errors [\#4015](https://github.com/pypeclub/OpenPype/pull/4015) + +**🐛 Bug fixes** + +- Hiero - effect collection fixes [\#4038](https://github.com/pypeclub/OpenPype/pull/4038) +- Nuke - loader clip correct hash conversion in path [\#4037](https://github.com/pypeclub/OpenPype/pull/4037) +- Maya: Soft fail when applying capture preset [\#4034](https://github.com/pypeclub/OpenPype/pull/4034) +- Igniter: handle missing directory [\#4032](https://github.com/pypeclub/OpenPype/pull/4032) +- StandalonePublisher: Fix thumbnail publishing [\#4029](https://github.com/pypeclub/OpenPype/pull/4029) +- Experimental Tools: Fix publisher import [\#4027](https://github.com/pypeclub/OpenPype/pull/4027) +- Houdini: fix wrong path in ASS loader [\#4016](https://github.com/pypeclub/OpenPype/pull/4016) + +**🔀 Refactored code** + +- General: Import lib functions from lib [\#4017](https://github.com/pypeclub/OpenPype/pull/4017) + +## [3.14.5](https://github.com/pypeclub/OpenPype/tree/3.14.5) (2022-10-24) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.4...3.14.5) **🚀 Enhancements** diff --git a/HISTORY.md b/HISTORY.md index ca54c60273..f6cc74e114 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,128 @@ # Changelog +## [3.14.5](https://github.com/pypeclub/OpenPype/tree/3.14.5) (2022-10-24) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.4...3.14.5) + +**🚀 Enhancements** + +- Maya: add OBJ extractor to model family [\#4021](https://github.com/pypeclub/OpenPype/pull/4021) +- Publish report viewer tool [\#4010](https://github.com/pypeclub/OpenPype/pull/4010) +- Nuke | Global: adding custom tags representation filtering [\#4009](https://github.com/pypeclub/OpenPype/pull/4009) +- Publisher: Create context has shared data for collection phase [\#3995](https://github.com/pypeclub/OpenPype/pull/3995) +- Resolve: updating to v18 compatibility [\#3986](https://github.com/pypeclub/OpenPype/pull/3986) + +**🐛 Bug fixes** + +- TrayPublisher: Fix missing argument [\#4019](https://github.com/pypeclub/OpenPype/pull/4019) +- General: Fix python 2 compatibility of ffmpeg and oiio tools discovery [\#4011](https://github.com/pypeclub/OpenPype/pull/4011) + +**🔀 Refactored code** + +- Maya: Removed unused imports [\#4008](https://github.com/pypeclub/OpenPype/pull/4008) +- Unreal: Fix import of moved function [\#4007](https://github.com/pypeclub/OpenPype/pull/4007) +- Houdini: Change import of RepairAction [\#4005](https://github.com/pypeclub/OpenPype/pull/4005) +- Nuke/Hiero: Refactor openpype.api imports [\#4000](https://github.com/pypeclub/OpenPype/pull/4000) +- TVPaint: Defined with HostBase [\#3994](https://github.com/pypeclub/OpenPype/pull/3994) + +**Merged pull requests:** + +- Unreal: Remove redundant Creator stub [\#4012](https://github.com/pypeclub/OpenPype/pull/4012) +- Unreal: add `uproject` extension to Unreal project template [\#4004](https://github.com/pypeclub/OpenPype/pull/4004) +- Unreal: fix order of includes [\#4002](https://github.com/pypeclub/OpenPype/pull/4002) +- Fusion: Implement backwards compatibility \(+/- Fusion 17.2\) [\#3958](https://github.com/pypeclub/OpenPype/pull/3958) + +## [3.14.4](https://github.com/pypeclub/OpenPype/tree/3.14.4) (2022-10-19) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.3...3.14.4) + +**🆕 New features** + +- Webpublisher: use max next published version number for all items in batch [\#3961](https://github.com/pypeclub/OpenPype/pull/3961) +- General: Control Thumbnail integration via explicit configuration profiles [\#3951](https://github.com/pypeclub/OpenPype/pull/3951) + +**🚀 Enhancements** + +- Publisher: Multiselection in card view [\#3993](https://github.com/pypeclub/OpenPype/pull/3993) +- TrayPublisher: Original Basename cause crash too early [\#3990](https://github.com/pypeclub/OpenPype/pull/3990) +- Tray Publisher: add `originalBasename` data to simple creators [\#3988](https://github.com/pypeclub/OpenPype/pull/3988) +- General: Custom paths to ffmpeg and OpenImageIO tools [\#3982](https://github.com/pypeclub/OpenPype/pull/3982) +- Integrate: Preserve existing subset group if instance does not set it for new version [\#3976](https://github.com/pypeclub/OpenPype/pull/3976) +- Publisher: Prepare publisher controller for remote publishing [\#3972](https://github.com/pypeclub/OpenPype/pull/3972) +- Maya: new style dataclasses in maya deadline submitter plugin [\#3968](https://github.com/pypeclub/OpenPype/pull/3968) +- Maya: Define preffered Qt bindings for Qt.py and qtpy [\#3963](https://github.com/pypeclub/OpenPype/pull/3963) +- Settings: Move imageio from project anatomy to project settings \[pypeclub\] [\#3959](https://github.com/pypeclub/OpenPype/pull/3959) +- TrayPublisher: Extract thumbnail for other families [\#3952](https://github.com/pypeclub/OpenPype/pull/3952) +- Publisher: Pass instance to subset name method on update [\#3949](https://github.com/pypeclub/OpenPype/pull/3949) +- General: Set root environments before DCC launch [\#3947](https://github.com/pypeclub/OpenPype/pull/3947) +- Refactor: changed legacy way to update database for Hero version integrate [\#3941](https://github.com/pypeclub/OpenPype/pull/3941) +- Maya: Moved plugin from global to maya [\#3939](https://github.com/pypeclub/OpenPype/pull/3939) +- Publisher: Create dialog is part of main window [\#3936](https://github.com/pypeclub/OpenPype/pull/3936) +- Fusion: Implement Alembic and FBX mesh loader [\#3927](https://github.com/pypeclub/OpenPype/pull/3927) + +**🐛 Bug fixes** + +- TrayPublisher: Disable sequences in batch mov creator [\#3996](https://github.com/pypeclub/OpenPype/pull/3996) +- Fix - tags might be missing on representation [\#3985](https://github.com/pypeclub/OpenPype/pull/3985) +- Resolve: Fix usage of functions from lib [\#3983](https://github.com/pypeclub/OpenPype/pull/3983) +- Maya: remove invalid prefix token for non-multipart outputs [\#3981](https://github.com/pypeclub/OpenPype/pull/3981) +- Ftrack: Fix schema cache for Python 2 [\#3980](https://github.com/pypeclub/OpenPype/pull/3980) +- Maya: add object to attr.s declaration [\#3973](https://github.com/pypeclub/OpenPype/pull/3973) +- Maya: Deadline OutputFilePath hack regression for Renderman [\#3950](https://github.com/pypeclub/OpenPype/pull/3950) +- Houdini: Fix validate workfile paths for non-parm file references [\#3948](https://github.com/pypeclub/OpenPype/pull/3948) +- Photoshop: missed sync published version of workfile with workfile [\#3946](https://github.com/pypeclub/OpenPype/pull/3946) +- Maya: Set default value for RenderSetupIncludeLights option [\#3944](https://github.com/pypeclub/OpenPype/pull/3944) +- Maya: fix regression of Renderman Deadline hack [\#3943](https://github.com/pypeclub/OpenPype/pull/3943) +- Kitsu: 2 fixes, nb\_frames and Shot type error [\#3940](https://github.com/pypeclub/OpenPype/pull/3940) +- Tray: Change order of attribute changes [\#3938](https://github.com/pypeclub/OpenPype/pull/3938) +- AttributeDefs: Fix crashing multivalue of files widget [\#3937](https://github.com/pypeclub/OpenPype/pull/3937) +- General: Fix links query on hero version [\#3900](https://github.com/pypeclub/OpenPype/pull/3900) +- Publisher: Files Drag n Drop cleanup [\#3888](https://github.com/pypeclub/OpenPype/pull/3888) + +**🔀 Refactored code** + +- Flame: Import lib functions from lib [\#3992](https://github.com/pypeclub/OpenPype/pull/3992) +- General: Fix deprecated warning in legacy creator [\#3978](https://github.com/pypeclub/OpenPype/pull/3978) +- Blender: Remove openpype api imports [\#3977](https://github.com/pypeclub/OpenPype/pull/3977) +- General: Use direct import of resources [\#3964](https://github.com/pypeclub/OpenPype/pull/3964) +- General: Direct settings imports [\#3934](https://github.com/pypeclub/OpenPype/pull/3934) +- General: import 'Logger' from 'openpype.lib' [\#3926](https://github.com/pypeclub/OpenPype/pull/3926) +- General: Remove deprecated functions from lib [\#3907](https://github.com/pypeclub/OpenPype/pull/3907) + +**Merged pull requests:** + +- Maya + Yeti: Load Yeti Cache fix frame number recognition [\#3942](https://github.com/pypeclub/OpenPype/pull/3942) +- Fusion: Implement callbacks to Fusion's event system thread [\#3928](https://github.com/pypeclub/OpenPype/pull/3928) +- Photoshop: create single frame image in Ftrack as review [\#3908](https://github.com/pypeclub/OpenPype/pull/3908) + +## [3.14.3](https://github.com/pypeclub/OpenPype/tree/3.14.3) (2022-10-03) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.2...3.14.3) + +**🚀 Enhancements** + +- Publisher: Enhancement proposals [\#3897](https://github.com/pypeclub/OpenPype/pull/3897) + +**🐛 Bug fixes** + +- Maya: Fix Render single camera validator [\#3929](https://github.com/pypeclub/OpenPype/pull/3929) +- Flame: loading multilayer exr to batch/reel is working [\#3901](https://github.com/pypeclub/OpenPype/pull/3901) +- Hiero: Fix inventory check on launch [\#3895](https://github.com/pypeclub/OpenPype/pull/3895) +- WebPublisher: Fix import after refactor [\#3891](https://github.com/pypeclub/OpenPype/pull/3891) + +**🔀 Refactored code** + +- Maya: Remove unused 'openpype.api' imports in plugins [\#3925](https://github.com/pypeclub/OpenPype/pull/3925) +- Resolve: Use new Extractor location [\#3918](https://github.com/pypeclub/OpenPype/pull/3918) +- Unreal: Use new Extractor location [\#3917](https://github.com/pypeclub/OpenPype/pull/3917) +- Flame: Use new Extractor location [\#3916](https://github.com/pypeclub/OpenPype/pull/3916) +- Houdini: Use new Extractor location [\#3894](https://github.com/pypeclub/OpenPype/pull/3894) +- Harmony: Use new Extractor location [\#3893](https://github.com/pypeclub/OpenPype/pull/3893) + +**Merged pull requests:** + +- Maya: Fix Scene Inventory possibly starting off-screen due to maya preferences [\#3923](https://github.com/pypeclub/OpenPype/pull/3923) + ## [3.14.2](https://github.com/pypeclub/OpenPype/tree/3.14.2) (2022-09-12) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.1...3.14.2) From 4651627041c1b634814724f8db13348dd21f93cd Mon Sep 17 00:00:00 2001 From: OpenPype Date: Fri, 28 Oct 2022 13:17:15 +0000 Subject: [PATCH 102/160] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index bf36fc4b10..838f935069 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.6-nightly.1" +__version__ = "3.14.6-nightly.2" From 77d84f42fae88adb2a17d0a1baae12e2f85e7997 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Fri, 28 Oct 2022 13:21:14 +0000 Subject: [PATCH 103/160] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 838f935069..cc78495ea2 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.6-nightly.2" +__version__ = "3.14.6-nightly.3" From d30f5e61cabf31e8a9075a55b3edb0b8b003f92a Mon Sep 17 00:00:00 2001 From: OpenPype Date: Fri, 28 Oct 2022 13:25:35 +0000 Subject: [PATCH 104/160] [Automated] Release --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index cc78495ea2..e464d6787d 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.6-nightly.3" +__version__ = "3.14.6" From 6ecbf122e46d08e0f04879b74be6c1ef7e909a66 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 29 Oct 2022 03:47:38 +0000 Subject: [PATCH 105/160] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index e464d6787d..442c5f033b 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.6" +__version__ = "3.14.7-nightly.1" From 7647176173d3e4d28290650761370efeede53505 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 13:37:07 +0100 Subject: [PATCH 106/160] 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 a57a8791a8..4540e70eb8 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -558,7 +558,7 @@ class CreateWidget(QtWidgets.QWidget): 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 48f40f7b5b..53152f488f 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -55,8 +55,6 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 fb9bd761f4..9af9595a97 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1674,7 +1674,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget): 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 @@ class SubsetAttributesWidget(QtWidgets.QWidget): 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 107/160] 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 53152f488f..808210a673 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -155,11 +155,6 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 108/160] 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 808210a673..e119d640b4 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 109/160] 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 9af9595a97..c7b6965991 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1653,7 +1653,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget): 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 110/160] 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 | 288 +++++++++++------- openpype/tools/publisher/widgets/widgets.py | 18 ++ 3 files changed, 198 insertions(+), 112 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index 4540e70eb8..7bdac46273 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -272,6 +272,7 @@ class CreateWidget(QtWidgets.QWidget): ) 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 @@ class CreateWidget(QtWidgets.QWidget): 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 e119d640b4..b45d61623e 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 openpype.tools.publisher.control import CardMessageTypes 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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._cached_pix = None self._current_pixes = None + self._has_pixes = False + + @property + def has_pixes(self): + return self._has_pixes + + def clear_cache(self): 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 - - 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 - - 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 + self.repaint() def set_current_thumbnails(self, thumbnail_paths=None): pixes = [] @@ -153,8 +64,17 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 c7b6965991..744c51ce07 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1569,6 +1569,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget): ) 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 @@ class SubsetAttributesWidget(QtWidgets.QWidget): 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 8935e221e63c0e34d882dff1fdad0024b4c345b8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 31 Oct 2022 23:36:31 +0800 Subject: [PATCH 111/160] remove underscore from subset name --- .../deadline/plugins/publish/submit_publish_job.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index aba505b3c6..35f2532c16 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -457,9 +457,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): cam = [c for c in cameras if c in col.head] if cam: - subset_name = '{}_{}_{}'.format(group_name, cam, aov) + if aov: + subset_name = '{}_{}_{}'.format(group_name, cam, aov) + else: + subset_name = '{}_{}'.format(group_name, cam) else: - subset_name = '{}_{}'.format(group_name, aov) + if aov: + subset_name = '{}_{}'.format(group_name, aov) + else: + subset_name = '{}'.format(group_name) if isinstance(col, (list, tuple)): staging = os.path.dirname(col[0]) From 1f2ad7c304f3d8e623bfac657d01d2ebe22c790f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 16:45:54 +0100 Subject: [PATCH 112/160] 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 146af84663..404ca6944c 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 113/160] 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 d8dd80046a..5302946c28 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 114/160] 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 b45d61623e..69161a7bd7 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -114,11 +114,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): 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 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): | 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 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): 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 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): | 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 115/160] 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 69161a7bd7..3f159d5812 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 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): 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 116/160] 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 585adceb26..15abb6130b 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 019ea16391..31c8232f47 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -7,6 +7,7 @@ from .widgets import ( ExpandBtn, PixmapLabel, IconButton, + PixmapButton, SeparatorWidget, ) from .views import DeselectableTreeView @@ -38,6 +39,7 @@ __all__ = ( "ExpandBtn", "PixmapLabel", "IconButton", + "PixmapButton", "SeparatorWidget", "DeselectableTreeView", diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index ca65182124..13225081ed 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -252,6 +252,90 @@ class PixmapLabel(QtWidgets.QLabel): 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 @@ class SeparatorWidget(QtWidgets.QFrame): 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 @@ class SeparatorWidget(QtWidgets.QFrame): 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 117/160] 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 3f159d5812..d0ac83d6eb 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -77,84 +77,70 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): 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 + checker_painter.end() + return checker_pix - 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) + 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 ) - empty_painter.end() - return empty_pix - - 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 + pos_x = int( + (pix_width - scaled_pix.width()) / 2 ) - 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 - - 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 - - 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 + pos_y = int( + (pix_height - scaled_pix.height()) / 2 ) - pix_width = expected_width - width_offset - pix_height = expected_height - height_offset + 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.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 pixes_to_draw: + for src_pix in thumbnails: scaled_pix = src_pix.scaled( pix_width - full_border_width, pix_height - full_border_width, @@ -177,9 +163,63 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): | 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() + 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 + + if self._current_pixes is None: + used_default_pix = True + pixes_to_draw = None + pixes_len = 1 + else: + 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) + + 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 + + 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 + ) if pixes_len == 1: width_offset_part = 0 @@ -207,13 +247,13 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): 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 118/160] 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 d0ac83d6eb..53e0891623 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -35,7 +35,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): 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 119/160] 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 zcmZX5by$;O)b}=0Qba&$Nona0B?l6Y5F`}^L3*^r5K$1RQPL^{K~j;DngXIo$N*`W z)T9|5V|>r>eZRN<;DU?$Jm;MI-1l>y6TcH@W};6|!%hPNf#{(IIxr9j0{jU9QBwlH zc7lhFKp=3Go3^$Y9HxJR7pkkRAg!#RAR!|u4FX+F2}o%)(!PHFd7FTtK>q8PSf|bR z7WbaMPSDjUe9bET8K=gayN zmx^v2v3A@?=QbL~ulBy=j$SYrAyP1FTZAbLoKeB%`j3uvzxh}tfv}JROZPx#| zgVv<{Ep*ql{H6AT@{U?mb#w0eeZI1b8_umLNo00G>Zr(cVj>TZL+(xKzDwMcms7^H zHN7bB>&ArL)`-mbG%qSsHvh|7P%oD*{H;q-sLnl$o!FP;$~C8v$M5x@PsS_*n*nwt^MMAa+G8;XNuQ@XtNwJ3Pa$S$m8W zd>_ftc)v4iqwZD2T1)PfH}wA?BmccnR(aQ(Cv+ifu3IWK#`S1{M#~L2lXE@>HgFI~ zlArVg4ldFR1c7)#P@U@*!MSTwA)g*thHRX8%U={s`1F-t*DYP>79VfPXa|!He1qj8 zfAZrGU6a(WN}wHVJ_7vidiThq=f)LF6q(*C-ng)l!1aw%SL@>LFsKR>YjK6z;ELzK z#o^}2o*LzMpTh%XM?x2~SB@{wDfWSHew*yKd#t`)-m+)M3q^*yUf%%;B-|IN-oLq* z{bSmeFnM{@SIDUDps^yd{{G_Wz6$n@Ct-5B&6ai&Ts}67!|uzYqv?mVV0w}uly!X6 z0-DBYp{Jh(UG*UZJ|lA9Keef#myN0(ROaH>-qT+|wxX>k{DT59R84^<_9*O2M6NT6 zzqov-fgWw#BS~%5dwB9J>$Fb9fb_xfJ7%quW!Wk)OX&AmrLu&~TS659|+=E~5Jb z!-2HVWuK*+R=NxZqIt9+H+X!M_AAeWe()YGSBlUstqU7jCPedV4nvWC1ITPz8+%bwT571L@_>quCU|F1;fh;lm$Dk`Q@5q&ZGaUOQE6jUw2HJv! zXoXiZ`pn-XiY-0f#bm65T5sH;BdBiv9Q|X^>j!?#E&<8!Ty;ErKKr2(?*Sn?Vt=A2 zz62Q@vl@r6KTOw^8aeC=)pNwdBM(CH7#EC8-nr zP6mB-JZMu88-}Gtc_tP0`GQ|p+R!v75A7rsKYl!4Ciy5z3W7^vrO+UU=KX0isG9Hk|3q}zbEwJL2L}qfuetCc8F@~@%I3;)B;1sZFE#T(y zJVim@#+1-|`qPxzO@Kc*H-H&?9{fSKrNwEge@gM;E$#4gSjs#&J2h!$eS|% z62RW&HrWQauha9nydWDW`x^|qi(W#}hY8fqDPZl<`aV~9OLdJ9ijX#5BBn$-yssC{ zVXy&(y7%0}3Sjbn3`LVLUl)7CG%vJjv5@5f5%76kl8o5C3}#CQ<~mI&-g z;ktR$x^WkfJQR{iCOm8WWEfAQWuF0wq|YT2XXK7YsfhDSfHEMNv(w<=O&TrreA+O& zgWkVU2Je5%qY|FBvZ$Xo?ymv|&h(-6qr9EAGl$BBAu|Hu4}5T=Zyw%p0ROFPN0^ee zF{z)2w!pbIX)xQ`L)U@b8Pi6!@rK{tm;U0MGGzJ~d>ZX2i_yphgC4) zOV0+|9Q_mBEgG$?J%R9+jqiV>N|g4&LCN{!bu9&L5N>jA$Zz_?Mz6(F95QQFX&wi| z_%%TAo&&}NRzVi)em`;UYq-3 z+wkfwR1BI1Q|U+5KmP+&M)!V%45d$3z@dI#P!iN#O&%<@*)>3R%PI#k+@o+L<{IJf zoY7(o!-q-Cpz8l|9ToFrdFw9L7~MWVwxr4i!e@X_A!=fP)bS})Q@&K*4@Sh}8~Q`x zNtF4NYOX^4l!1Wv^=qIz@J36$rNX#Mg6^raDKU;J0*`zbb;&sqM4wW5yyP@v1U&Bj z!0Vfw&4#}$>%m=fsT9N7-a__}iTf(498on;;fj;Gd>F-8lAN$>>f+TSCFw%kU2Iy# zJB#;&7cj=x2E5f|Be@~147rA%p~h32_c6KQd2-JOx>B+NpuS;FQ zFmT5*u|eaHs%hOMH4U>T{?7gM(40Y6^jVgsqfKKYQ?n}I{F^2i*PrhvqEL-{g->L* zotxijr-x83Jhvg7VBLNK$Oy8@Pd)j70j2>+mGGDAXhSvS2tas!3BqPB&^} zIx=P_8JYLYI6L%#Fh&~Bf|0%uo}2C8rxrMhf)+iN%{Mdro{nA$#7TKjyY7KD+VoJEui#2>$NE# zB~w9jK9_U4xu6y+1U$7@E2G!u*_}>PT=d*WnGG0Y;v&q4@@r8!M;QrnVzA?k6fbr= zBf@df-XusZ;*(oSwlBy>7VrHm8?NYvtLu8HBn~P_wTygpJkcOaOW*houu(isOg+boOi^2RFu_bYnw4BmgHw~DB!CvG@)e&$c z_3E{6Y6kXoWSe&|A9zlPWg`2jeFLz-{`SQJ*%3$;T0&#kG2UP!>QP*z=5ORN-Vh=4 znAKsg|B&*;$mxf*Ebs6meGGk-hP<5Vdgo(E+K(&2?T(dU+=`nib*_S&6tgq$hg}`dfkQHG8m`HJ#9n za(-g?{XO6bIgU_e|qoBSsisi_Cc&MZHEO4U9e#Y%Q@KAx&1Nf(-JcD@8yR5y^=BNW?E~t zTSbDk52QDP4pPu1Hb%=7%PHw-&kmk2`|s@7nACI|STY-Ox@iEHBss4TrU&%~Jn++l z=t|l~@}OVH&+d_!$4W9yP(eRHlWnvki8D653m^w$4EFRf56c7*Hc%XUVl_uM) zb9Y&wB%aDWMxAhLlURei`re>?h5Dy#Ja8_G#T!gWjch;z+O zv&)9dZ~5{3QFj|zM!jd3-%}g;IKRBxNX_V1T9pBw;UlV9xVO@;Uh@rLS=a91(J5M_ zYo3?do#MkZ@*ag*xN9hdxUnqBckrkQx~ThXT?|OKC>Z%u_jlswIrAY)FarfQFA*&w z!3gOxxpf=5tCHxS7^`8Lo^c$dU0hOj=aaAx8J^16tucPblNvgn&yQ$|({xPFC`bv7 zr>Q%CAcLnea%-dr?gkhS@jOrNyF7ra!1#enyGqQw`H8$D5+i@2B?S_2cobZJ=dkQk zvuF0(_sac3Q&`q;L|PgT==i3&1+=A>af$OnIJcO{3baL!Vk~{(6@jS$;$npNp(@Z$F+A44wVskEVopy?7mk*XM! zvG3b{Z^4Bkb4v^J$KpQ1c&ZyBAuIk1X{a~ThJ=vHw8?Yg1(mtkLpLxEdjnM0&)G2M z%GoOmx(Nu+(T_xjF;_J_JD{BvZhNYRXgY?vk9=r&gyoIYcpxEP+>Qw7Fb(rx|2B0Z z&>1Yb&fX9=6rWF8dYcEl`UN|G)eyaouyX!QL$nZ<9p(*d*SJBrukWhvu$O7^Y;F_! zB*&lyO0U8>YCzD@@8Tc|q|e`f(}HxoW$G~CiVq8( z`In32mGDJD==kJ4jIzv;+LTMB!0y-S5M_L)x9lT>f&eJ8&QtlMi3j_@Ms(y#JmOKT zRG96U!O=@2q}*iu-;op0?ZF!Fr!CJ8-qvTe{WMpAF2~fwYI@gaF>j?+gvCd##7PC) zHeS@+&qVo(Q??ot4x1_nY56;Gsj*UlwvEv{Ss4OuYCoa4Uw^lQZIz`>$GU6Dt&PLp zOnleF{#ARBigHlNCQdR|XLs{l+@YL}pS03QDRn^(xrUF65_hxQt#XUdy&Y@~3FY9u zZj$`kmZg4uuwrWH1F`W*1U6}@AHH##>yI`>7S}xUH(855L1@0<*6}imvW;`JuTA-h zpa|&}g7W2=w`mg&TM{M><8Onw54z_FGvo+{=&hqiozPaB2dv-?Dy-rd|q z3M6<5AK^W+_UA>dZ3C&+1zUZLaj)zs%**;9CJEe6U-*hHy)xV9Gz#N+sIs5jdh+YC z_m$R%#<=aV_eBbMovWOmsehIBa5G9mJ}=RB>_->FL+jk-J*0h@@f43__Q}NHEH^8% zCJ6TA552a7n10!dL2>hcTYug6;@kZ_yZZ;(*rblmi47;NpnH*6iT^SYf9@GLAVFcihJXdLz%Xfp@b|hm5m)uT@P1T21A$hEWc-xrMP( zCWA3ES8QvCP;UR2NgJOrQ(Og28ichyl+kx7v(hUNY`tc@m~d!VM7i*g7~Meqx0UmZ z>C~oh$9}xfcGaO_dRObviPJM%L=j-r;(3oVvdY{uvISe3NyD(2hoUQp@Q;>a(c?ix zRSCz^p=6;mp`YKyoo#e0&jcWn>0G62K==oR`CrfO=0ZyX?1bdT2vojIzqpJQPOsaM zr+Knr8dX0>%B4TrPIp)7=Wo4ayy!{N?$ZE?&a748jBoc|;Owz}sxqH#QLzsLKIWll zP-?zMiA?PoH?vBG9FqOIYpIr?p#$Nl(l8m@BB4+GrBFS#x23r$RTkaiZ_jL|#`JF+ zDyat=7>jkjVB4ci4`;L5`x2Y4!ir#eDgTyZ&o+H^(AfaD^>?nYl;F?`OV}8kxNJcm zV){)*q~OAtTC?fD&a^DvzYysmmu~u?e9$@rvY&VjExCBz$)*)1l8XFCm^O4qnEwC` zgc}^j^W0DNN|e2xd9rPKxOMOBtd0%;$?YHO;9)af7=0PQJ{H2vP`9H-SKebS zY+N;eX5GEC$un*l=_vT)9p3>Rjnoe&{A1@Dq}tI&J<69?P)h-C&bAw3e|E@Me>QgT zFdNTnz)Zb*@$}8H7d|ot!@jAwso1-dPWPcn=07w0g7971=7$@XSLCW9-Tvz8;AV4m zF&3XkN%E2%+5VZ?_Z&dQe2B}{Wp`o25?LM0bWFMd^(ud!S?|!l@k}_KFmt51T*`4& zp6qY8CI{}#+41jWXfHs$A46w$yVk<~&pK-+DkJ(lw8rxfF0XvD$5FL36vG|*_}uv9 z{XW@Yi&zJvFw&q+Ym#j!C8DQpkQQ@`DICYcR=;C^Pd*Z?_vmlpFOOR2u%qB2GE zcOgHqY9;49x%})iz4dOx&#au=NeXYtjOXhCy~H-IVks1u_D_-qpPvai+&wcLuSrEe z)j>|K?l@+jHaN<7{uf|~??-3HSREdIX0}+o#VoEhE=HwgKK0^1V^_aa&U)iCbgv9`n@7|`NF%St07M%lPtjaGd1weQb5J;~ z7{uu!dLvhW10A^?SFkY5^#)*E@eL}Jx;0?_oA#T9G_ZN0qZ zKqx7DTBt{u3HT9?b}ghD;IG$ZeC^~qP^F759i4xMZ^7jEBSWuye%xE#xX||YN1o}T zc(UHCV|e@Eb6rFiqD#xI?@Gip9H-j&qJ|@2`YN7aM}e{S0JWRL$$Zw2Rrdvt?axe* zy!0nfpLO$|pR0T|ufDGw6UUd$p;7AGZDMc8ZE#rkm#H<-5O0tJy!~?7O>|_ zQy6tj^&2`?2{$zjI=)fjkQ_JPv3r_~MgW4xdzhQp&3h%3XalVdrVtD#aZ6a-nC#;}BB6o9XMB z`BN%^8^ONog&|Ij*VVi#zm=!59a-k7n&7`$>QK9zm=gNfvx!&TJC~-khVUpL_?r#6 z_0Oa^ej}XdiF^(wxq^-A>6dWU)FEI_}e|F?8xVVi8_zc;Z{0#~z z(7J@v>qfu!Fe&Td4^i~})s!x4LRERopI#iyN#0(+vU{+0#{ta?Uz~Z?Vr}_TPeEs?v`ZyyG;9el9iM5Rb#B zuUS%uORR6}FlybU40`xj__!{wHgBd@d1G&*emwL$4R-d6rbi>ldTiM&`}V}Vo0=r# zS&d}-7{St(ajd+{yO9z7V(;5P0~KQBE!7*Jb>`R5Q38dJ#HP~@ugo3keV#fy(2zd` zwhR62@AVRDNr)UnP8+2{awt?H6i)HF&PXvkF&>ogb+Jlfc*w}A=j3@KEuyn$nUN=+O$Y z9yl_~t`!XhiDtxudLL|$Tq}D^l|1(1ixiv0|GX60CbG7BjY^! z&R((H2$fD8ZCkfhPxMW~% zrOT+5H_!LlOCi$Ooy0H{HS(B3Ydkj|p)y90PXO3hwGeN0XueZ&&A;XuGxYe~g-FgE zHDwkzl{OQCZ?3EnuC?KW_L5oOYQw@Rlo?R&WR#HJ4x3F{$hD!Y;QoM{bw%iQ_n+@VlgMb6K1i~z1g~$afbMG=UU;|qZ zqWcZwmIpNyTE+mcPlo2ocI5uRMvO~K>n)C%w2aH-(t%Bn0dz3qmQFDch8&30ek@iM z4-eTkLwJq9>uT8mi`pb6Vjxv227&r)D>u>1c!bO-VK^f@^k?6-CZO3gmrMG0PCUX| zA+ol#c={uMk0dCA93XcDjMOsA$NQ|;(X*_yUOY;r3Z!;P9kiPmj|dziu)HarhOM!H z>cLjzY~Z!(Loq|+mZEVn&ikZ{0N^n?hQp$_l6WIXEE_{Jczx1Os@^(cUh}I2A9VO6My)$ZUKyj9l+M? zBrEr}*-Jt${YyFa5QoC4wIpPy7&uOs+o0c+z_GOe z=69$*%>1H@7>AjA+tAj~B2*z%NL$jC`LXE8$_6IgiUo#O_Mq}}oCaR<6&MTdoX9L- zb8f)qhnQ$3|+iR6rjGaPl{&?F~xcG2nnnHw_59r)K%Ij7v)EJpw6S7Xx>+ zOs=!$Feo)1bas==aT|;t@2|>xj+H0@FOGQ55{eT03I4_g1 z05rf$q;~h`Sn9X?+f~AVbE=Cqc*mPOhyj2p-G+wDxtDfHP9~hiM;EXTRN?e9;Ga-dI*h!9`k^hr z33V*a0C}fe9PoxKRDmWhTuUw@QMWx>pDs} z{-KNb_JJ=Hbh(0^09%1zHB(y0uL3@CLy-;4-f4nvCAq3$Y$o6~IRKYb2*}gYtvRFx zI7Wz(eBVD0XEuWp0eYN^fUb8*D%Ds(oWmy(@P3 zm_M`<)yoE2iORXdV*+PL>P#&1Y1Qr~_l6Q?Nz#B1&5P)$?Lw+I z=M_%>n2=4Y;|eN<>*$xa*590p4Zp7gY3pRtIt2$_j&C9DGd{M{n~DWNbM<7;@Dtz@Guw?zrS&`} z1Vcer$xH8UEOVaT;mUVoPp|c?tP`EdjMMz)w-}@sGVu7~wO_f7@t%%LX8U8Wp9$+n zfBe=vV!dEPJqd&j>9S2pN3)R*XeW4a-G*QHH#tW?1))b%4i%Qq-kp!MTj)vnU!jyX zDU?F~uTYBE1EI{7EnvrWmOWH>`^W7glg)9%c$s~H5HYwrOZHm`zDnE)tqgBhl5*($ z2&OmXp#QEMTRx&M)n|9L?Lh~Rb|W0C zL@^4sz72$oiw8i+_!bBmh2Q>B5?<(y+fvFum=kv8zPCbk+Pm_cf=V#M{kb&c2nOWpA^u1Hpgh@so;k*eJApLkTw|HxIN%T^Hy*4+Ov1@ z^zF(O-X@QN6!2ZT>uR3aPsAzL#ef6^NKk0ru6kSq5){3E3Cib`17|+hb>}Dah&qc2 zW2T%-a9bVuZUI zD|Ds_^k0-WW^#i%Z!9+v+Rg=EPn=z)^tg)cobTI)U(|Q5VWKnbO+YtpY)odZ2E`Q| zPmXCVPuYakBiPbZ3wkU$6M87Gsf9lTag%YA6)8VI22)$$g=sw59i?Y?xj4& zC;#C2J5%9^B#NXM*y_z?iUD}OsyEL^L@AdSfSf0@dwIM65+&0{bFESTzRSJd*AT% z1Lgj_iOVNR$0;Wp5C`jZ576FE8-d=Rx@eChphRLv-t%p7rt-m1Nc8bLIIA1>DL}-m ztPW(t7ubMLW}_YXD1pEEzq0OdL;{eUPd3T`6dyIDARa{9m2Dth=J}5V9vK?I-|C2_ z_27HHD2~+o-9<{bkB=Its}H+^Me*<%oz~161KUu2uvB$ivoOFVLqh`Kvb{1$;(}}G z=#OMRJS!@|X-Gu{D;YI;DDz4?DZBryAr6rGvXa#^MbgQo%=B5ms}LtuJ9PiE+Cfr9 zvo)Kfx;5aV03yefQ2#>UwgLr_ZNN&*rM^r~+XZ$B@t~YYwa23u9k_R9v-`;IhXp?o zvLflA*^}u!cNInFmT)2hMNe zCQU;tuZt0jV%RRG!~^9G^QA3-Re@tBNfST28wC6>9ilQ&1c8TXOYy02M3Kq-h7JNB zX7u7ruB0TuXh8{aF!!dEu>Ue3d&C4CPAWn9nE(Z-Q2RT;8RU!<0hVkN$6biL2^5T| zgFl^wT&8rm$4zPFQl45o5<^;5x3>mJe{Kb18r6>)9`Y6Zuzr`qrTE9GDLDzC+PWG7 z?A_b(2TA>qJ|SYmL(wjm^1<&wLC)mbsO0>0Od+3T#@WFu37#osL#d`i_%NUYWLvZ6 z3%Kth>=XK_u9`zh9Ga0NJIM@x-Z&M)ngjK)x=asOGx`6l6AF^dvvBn;AW~{j0|gJ+ z!4FtONI8~%tLOI_QJ}Om>id3M^Z{!la5XZ8{YYdZSsnW+#eQtn0*<9Z@c$@c)+JhG z@sT9?>7H>MkKVZ);?LtITF_%vDx#Kja6>MuenBjq{nsL3O~+ORlF2`|>f9uwE9erq*Rb8 z5K-ks9-0WZddp=E0@c^hW5`OZl%aWJ40_{^`+Z}QK2yUTXJU)c6&?Et#UB%Q)}Fl$ z%Q*Ndm!-G}Xq5&BByF_jl2|Dc&f!nq{vbSRg)P7@;$z%o6d|hj%QCXlNa|GSCz1}4 zBak<*tngSjRawTiR^|?MxlpT;@nS8h&}>`-s5Ro6O(Oe`(zYc~+D5hUwQ3nJsyN-M zyjSX*O44dK`#wo*qAuDrZiCr)ac%7VFro{>5i12t4biQ0FQuP5Bja!-k!9SzFvFAG zL+KPkD$N^pu)Y6bXEYA6nUd}%T~MCFvyFb_Uz+5c0_fOkEX%HUU2rTZ-p&Nns-v?7 zZr!o#D2Z0xTd-A1F3kibH)un@nnvowq^#+jJnr{^OsAGPd4_UU=O4_<=l!3yO6q^M zVi0x|sRF$#pydeA6z?g(fhpf43_S5Y=3k3OF;;V|vWrw9MWPYYQn=J&KIA*3~d z@N`JE4Cq2p09~jkZJA-9*|TDHgzx=mjD`YTucxG#VNr>85X2o*&`u&NyeGZWt{ zQ293L=CM*9wx#a7LeMWqek>u(o5eTf#kyfwm)ZCUz2Bh63nDoJyW`Vnu8x9{iSNc1 z2P3Cw;Lv?+4|Raf6|5lq$-#DrZAf@=HjCT2lhLLS=IRu~)^As!*Bx%EWa<_4PkeWW zC6$i@u;0%WlVI_FQr|7T3sYcpnuB28POI=ZYy1KIJ^w{{Bcxu#Zli9?JakCd zK|AcrM9<8b4qt6L3hF0%&V_XUrjP&IzmCW;D7f2l&1gZO)Jy8EX2G#H-|kmkQtMGl zxOLOhg}yqH)ZNU>NbO4>z^y23e-uw&c@=}U>|kr_wY!m|flEP2iwjN3vV`Y*$==4y zOdLw6ynlqi$7k$o#sp>ybObK<|;G*&1 zjgT4dSwny1>`X-^P81Ns*W17aoX4>mp1VrlJ9jqwOrhD%(o54Nf46-n4ugTN@I6N? zH>q3OEUS+fC@t*cC8|vZu2b&)xYeAUYeMMwAAR5bM!*w(;_NKzy?_af9Y`Jg?;Yd) z8~-}SJJ=}ARF!83=U#l*4c0`UBCN^7;t#gHbR3XxA}o(p@TnJ5WJ#T5nvpI*bFkyj zTg~HKmmxG{+!kIYUfQo)&Lcwi!4(0jk$sQZqf5{J#$1}5~HzD5L1XgK+sF#{H z25`eA&7>~-U}juFbIBy6Jh$~J^5PG+j+2Rx}y5YSstnUpq!DXwX& zJl8GIFZAQtY$z=og#$Q%l$1ko^~#WC8ftIVP$fvWK$CyFxU}plD`ZA6LWX}^eo`HH zQ2b5XaUJ>>Z1gCpD;1(a#jH9#3OBB=7Q#jfkP~b+hSPk z!S;RsY3Tr#+H|fcpfo@Ew>e<^h9Qh>T90rjt#UgxG@ac?=_D*{K0`T#w4fJ0H5=rm z0BMu;ydrPsSbr`*XBYipkQIiMQA#9w;Z+rRvg(qv0kmgrhiRbyq>MG}6aU{0);2ht zzErW4!nDhq!h_GV>Tpuzkq9(Z=}Ck)UiG^Q%oG++d~y&cb%k-Z3yd5O;3O_+elIY! zF*0_#;=_bbb-eL=E+Gmzj#nY{L?(OB62RH$Ac;So<5Y9k}$Qt)p%T}ycL^#?0< zROPwFCCJeK51I)s4EVwdVLKn0oR(n=+ynrrbK*rx-0P-rjWGIZ*TERC! zFknmuqs)5*Adn;lch3imMgSg>=VsAjAohD)nOJFjqCm@jVZUb4q7`hzjDkVf6MH_~ zEIm8nAO$f*6%WlM0O+r25qZYc&}X84G(NY36&KD2u^vdES$H-@x$2U{H{TG6M@_+&6T+9VPCH6Ym53jL##l4pd(*bj z^lAgyw~MjbBd_Eo@~++cohn(jrVuzRm~c-K)0Msw7ugmheF|Qc4jdj9JBEy$kE{-q zuLo6eHZ3Ecg|IBOu$&Or-76{gE;lVN7o9|L#A-fl%B77W`zFzh>g7aCWSZu1_~@G_ zES&>D9z4zVeJYqj{hsv5DdEGALAGwKgUk*R321I>Zd6 zF@VRKrnZtFB~3C=rwpJ_?mM*_dmb;nP0bJADnI{PrK z4^uLL)U2WiyyVP{lAHi5F#({Ujkat*%_U9=`9*4=&Ky@xIZZ9j6(WT4vY{cI^Wnz> zEd)QQrAYt@Y7^}2WZp=jL_hjqek{4Wj@C!TM+|?46pdJ{%HVq9$mAgtlb`gavA2WM*uz1-TCdBS8qK#F5+)IcrP^}Tjcrn}~E_Q)lLiN=_EJ4o)$P>0V$s3~aI zxL^m*e)!KID9*anT9vFonAnTyQUm2Oa?<}d`0rd`%dLOdUvH*OFbN{u=ZVUpEKupU zgHCMAVT{9p_e5=9d4r0zPxdy<*95F^{*-e0aV2E>$qer=LPknrdReTd*Ks~QI_H^b zrbV4I{?YZK`(&Gz*x_L1TQz|~T+EK06pux0XgOs)FsRC4N(Vi0iG2tT)E1w~cKq;) z+j{>O*T;Zoi)yda41n#IlLPN<&QFg#U>by;yJ2d~T!KNMx0Z2UL{^j1%ENmLI99R) zO1!sB%}KeCr<^~XMsf*d6LN9O12d_| zBpDPQc*TvOFo|C_vhPYBN0Eb4%%>dt9ghB&`5aK&R_>&kt5<)>U~lG=eftnL{$aPuJ6j~t+V2$|xrj4?R zSN3$1)twuf!s_tW`$o`ycSq$^7F)vliR6htnGfe?;oqo-8MFwQwR2(YN;09mqJ4L; z3{Jht1#Nux4}u>Gt<-HFJ4t_-%M~B%+c1K5Eli^wv?D%ueysBcZyI9zfXCs$j6m)A z10$>_s$XBQ)JOpoN#9J?#$_(R23mNmu;h*ayzBCd0&o@I)LPYz!}Dod_LOQA-nl75 z^f=`DgO7)7ePW=gEg39-gJT_PYm$P%m4+YBOiBhz=LZzqMk9m!yzn2t~4$Z+># zT7nb*4JUDfJeQuM;Zbv&@av^3m3%FkN$5o>H#*v=Oz`d0e2~k_B|Xx}(+R&R#)37) z>(b}^1LhoQm*Yc`rIzIrd*sJEH$$&}cb_%&|MqIET&P8njUP&X2ht2~$U0yyI2*TG zfs)3pdSRHsuU>*gorgCI{KS?jiA4s?TaOUkcSV-KeBPA#7ypfNoiZQjW4~ZO7-0BW zK~h5DIHe;g0cZDa#i&Ajk^+=#(;q8~4xKg42d2ar$xd}brJ!St*%pJ``bXtkI!>9u zllDGRo9D3MsHWp0JzxgVHjvDLiXc)g9xM)v^P-$)%%z{?Qe)jk(1L0`rI$3Kn4|c8 z-^uJw=wW}NyP~9$HDprn0CRRJ8Dx=4sa?Qiko#Z)N_hK3-NzglR1@jBff+_blAsma z>C(pcxt1>NcW1r;+Dhpzj#znA5_JSWAc6R1u7$@_|Yig(=!F1|^<@{~G8U5vNFmI2ev!_QZ|; zDZ%|lu1{++Hh}7Jmd}CRWv-Do$9}pLd}Xr)SQX|5wHh25oyMb2KD4I@%|WzkN3%Y) zp+BG_!F#b>iK1{}z`dV>3WQG_5J$p_x`B=7%QdU^Z^D-D}BMd3pJ{?QZx)574gCWQa&uUipNcSRD#1_f#KD1 z3e;n2D~MPf9v+DarwaeX-Egh}Y%QP4u}Emn+6fPX())mUJMY}QLtgF_c&i$6AdiWl zc-&7|9am)j4GBtawT-0mp|w>!%-Jkf6M0DsD&@%dc=*I=eFM&RP8I28#lChSQV(2Rlpr+yDRo literal 0 HcmV?d00001 diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index 53e0891623..aef5035603 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -17,6 +17,7 @@ from openpype.lib.transcoding import ( from openpype.tools.utils import ( paint_image_with_color, + PixmapButton, ) from openpype.tools.publisher.control import CardMessageTypes @@ -292,7 +293,13 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 744c51ce07..f0c1a6df80 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 120/160] 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 15abb6130b..0a703d1170 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 121/160] 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 0a703d1170..887c044dae 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 aef5035603..035ec4b04b 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -299,7 +299,7 @@ class ThumbnailWidget(QtWidgets.QWidget): 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 645971c07e3df0d4a5dc1cdc564f5d147f592f56 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 19:04:27 +0100 Subject: [PATCH 122/160] better deffer reset of publisher --- openpype/tools/publisher/window.py | 40 ++++++++++++++++++------------ 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index a3387043b8..d8a69bbeb0 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -225,6 +225,12 @@ class PublisherWindow(QtWidgets.QDialog): # Floating publish frame publish_frame = PublishFrame(controller, self.footer_border, self) + # Timer started on show -> connected to timer counter + # - helps to deffer on show logic by 3 event loops + show_timer = QtCore.QTimer() + show_timer.setInterval(1) + show_timer.timeout.connect(self._on_show_timer) + errors_dialog_message_timer = QtCore.QTimer() errors_dialog_message_timer.setInterval(100) errors_dialog_message_timer.timeout.connect( @@ -329,7 +335,6 @@ class PublisherWindow(QtWidgets.QDialog): # forin init self._reset_on_first_show = reset_on_show self._reset_on_show = True - self._restart_timer = None self._publish_frame_visible = None self._error_messages_to_show = collections.deque() @@ -337,6 +342,9 @@ class PublisherWindow(QtWidgets.QDialog): self._set_publish_visibility(False) + self._show_timer = show_timer + self._show_counter = 0 + @property def controller(self): return self._controller @@ -347,17 +355,7 @@ class PublisherWindow(QtWidgets.QDialog): self._first_show = False self._on_first_show() - if not self._reset_on_show: - return - - self._reset_on_show = False - # Detach showing - give OS chance to draw the window - timer = QtCore.QTimer() - timer.setSingleShot(True) - timer.setInterval(1) - timer.timeout.connect(self._on_show_restart_timer) - self._restart_timer = timer - timer.start() + self._show_timer.start() def resizeEvent(self, event): super(PublisherWindow, self).resizeEvent(event) @@ -374,11 +372,21 @@ class PublisherWindow(QtWidgets.QDialog): self.setStyleSheet(style.load_stylesheet()) self._reset_on_show = self._reset_on_first_show - def _on_show_restart_timer(self): - """Callback for '_restart_timer' timer.""" + def _on_show_timer(self): + # Add 1 to counter until hits 2 + if self._show_counter < 3: + self._show_counter += 1 + return - self._restart_timer = None - self.reset() + # Stop the timer + self._show_timer.stop() + # Reset counter when done for next show event + self._show_counter = 0 + + # Reset if requested + if self._reset_on_show: + self._reset_on_show = False + self.reset() def closeEvent(self, event): self.save_changes() From bebb9031c18534d06a3e7236f950c16c6cc88c02 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 10:12:26 +0100 Subject: [PATCH 123/160] 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 e736ba8ef0..0bfccd3443 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -42,7 +42,7 @@ XML_CHAR_REF_REGEX_HEX = re.compile(r"&#x?[0-9a-fA-F]+;") # 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 @@ IMAGE_EXTENSIONS = [ ".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 124/160] 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 d8a3a00041..f74c3d9609 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -13,174 +13,279 @@ import sys 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"] + # 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 + ) - if repre["name"].lower() == "thumbnail": - thumb_repre = repre - thumb_repre_anatomy_data = repre_info["anatomy_data"] + 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"]) + return None - if not os.path.exists(src_full_path): - self.log.warning("Thumbnail file was not found. Path: {}".format( - src_full_path + 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 + + 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 + + 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 )) - return + 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) - 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) - ) + 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)) ) - 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"] + # 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 + )) - # 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: - 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)) - ) - - # 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"]) - )) - - 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 638f8238250cdd4ce7935c9c637dd0c3e80746ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 12:07:03 +0100 Subject: [PATCH 125/160] run openpype subprocess using 'Popen' instead of 'check_output' function --- .../repository/custom/plugins/GlobalJobPreLoad.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 61b95cf06d..b8d90cdf69 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -193,10 +193,17 @@ def inject_openpype_environment(deadlinePlugin): env["AVALON_TIMEOUT"] = "5000" print(">>> Executing: {}".format(" ".join(args))) - std_output = subprocess.check_output(args, - cwd=os.path.dirname(exe), - env=env) - print(">>> Process result {}".format(std_output)) + proc = subprocess.Popen( + args, + cwd=os.path.dirname(exe), + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + std_output, std_err = proc.communicate() + print(">>> Process result {}\n".format(std_output, std_err)) + if proc.returncode != 0: + raise RuntimeError("OpenPype process failed.") print(">>> Loading file ...") with open(export_url) as fp: From 298fbcae701f2bcc2833e8b5c4310c40c05fe380 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 12:07:30 +0100 Subject: [PATCH 126/160] removed unnecessary headless environment (handled by '--headless' arg) --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index b8d90cdf69..a25a1b7e93 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -189,7 +189,6 @@ def inject_openpype_environment(deadlinePlugin): print(">>> Missing OPENPYPE_MONGO env var, process won't work") env = os.environ - env["OPENPYPE_HEADLESS_MODE"] = "1" env["AVALON_TIMEOUT"] = "5000" print(">>> Executing: {}".format(" ".join(args))) From c178d6e2a1f9251ba9cfb09d1ad62cf62a30e411 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 12:07:37 +0100 Subject: [PATCH 127/160] formatting changes --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index a25a1b7e93..80f91607bc 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -164,7 +164,7 @@ def inject_openpype_environment(deadlinePlugin): args = [ exe, "--headless", - 'extractenvironments', + "extractenvironments", export_url ] From 4161b3b48db137a7bdd1b998612ea6450cc3ea54 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 12:56:53 +0100 Subject: [PATCH 128/160] use Deadline 'ProcessUtils' to run openpype process --- .../custom/plugins/GlobalJobPreLoad.py | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 80f91607bc..61d0c8eb86 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -7,7 +7,12 @@ import json import platform import uuid import re -from Deadline.Scripting import RepositoryUtils, FileUtils, DirectoryUtils +from Deadline.Scripting import ( + RepositoryUtils, + FileUtils, + DirectoryUtils, + ProcessUtils, +) def get_openpype_version_from_path(path, build=True): @@ -162,7 +167,6 @@ def inject_openpype_environment(deadlinePlugin): print(">>> Temporary path: {}".format(export_url)) args = [ - exe, "--headless", "extractenvironments", export_url @@ -188,21 +192,16 @@ def inject_openpype_environment(deadlinePlugin): if not os.environ.get("OPENPYPE_MONGO"): print(">>> Missing OPENPYPE_MONGO env var, process won't work") - env = os.environ - env["AVALON_TIMEOUT"] = "5000" + os.environ["AVALON_TIMEOUT"] = "5000" - print(">>> Executing: {}".format(" ".join(args))) - proc = subprocess.Popen( - args, - cwd=os.path.dirname(exe), - env=env, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - std_output, std_err = proc.communicate() - print(">>> Process result {}\n".format(std_output, std_err)) - if proc.returncode != 0: - raise RuntimeError("OpenPype process failed.") + args_str = subprocess.list2cmdline(args) + print(">>> Executing: {} {}".format(exe, args_str)) + process = ProcessUtils.SpawnProcess(exe, args_str, os.path.dirname(exe)) + ProcessUtils.WaitForExit(process, -1) + if process.ExitCode != 0: + raise RuntimeError( + "Failed to run OpenPype process to extract environments." + ) print(">>> Loading file ...") with open(export_url) as fp: From f1e198ea94b0c13d90a7419e2b5d3a4dc1769b60 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 12:58:22 +0100 Subject: [PATCH 129/160] fix too long line --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 61d0c8eb86..9b35c9502d 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -196,7 +196,9 @@ def inject_openpype_environment(deadlinePlugin): args_str = subprocess.list2cmdline(args) print(">>> Executing: {} {}".format(exe, args_str)) - process = ProcessUtils.SpawnProcess(exe, args_str, os.path.dirname(exe)) + process = ProcessUtils.SpawnProcess( + exe, args_str, os.path.dirname(exe) + ) ProcessUtils.WaitForExit(process, -1) if process.ExitCode != 0: raise RuntimeError( From f2a2cc1c38304d33eb1ebf410961bc9bf4f23da6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 14:13:15 +0100 Subject: [PATCH 130/160] change import of 'AttributeDefinitionsWidget' --- openpype/tools/workfile_template_build/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/workfile_template_build/window.py b/openpype/tools/workfile_template_build/window.py index ea4e2fec5a..22e26be451 100644 --- a/openpype/tools/workfile_template_build/window.py +++ b/openpype/tools/workfile_template_build/window.py @@ -3,7 +3,7 @@ from Qt import QtWidgets from openpype import style from openpype.lib import Logger from openpype.pipeline import legacy_io -from openpype.widgets.attribute_defs import AttributeDefinitionsWidget +from openpype.tools.attribute_defs import AttributeDefinitionsWidget class WorkfileBuildPlaceholderDialog(QtWidgets.QDialog): From 990647ef4f7c585b3a5e4c4fec6c5cef66e8b7f7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 14:36:19 +0100 Subject: [PATCH 131/160] 'AbtractAttrDef' inherit from 'object' --- openpype/lib/attribute_definitions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index bb0b07948f..589a4ef9ab 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -91,7 +91,7 @@ class AbstractAttrDefMeta(ABCMeta): @six.add_metaclass(AbstractAttrDefMeta) -class AbtractAttrDef: +class AbtractAttrDef(object): """Abstraction of attribute definiton. Each attribute definition must have implemented validation and From 563447e1a4b61cc404fbda6e92a0eec5acbe10c3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 14:36:31 +0100 Subject: [PATCH 132/160] add stretch to dialog --- openpype/tools/attribute_defs/dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/attribute_defs/dialog.py b/openpype/tools/attribute_defs/dialog.py index e6c11516c8..69923d54e5 100644 --- a/openpype/tools/attribute_defs/dialog.py +++ b/openpype/tools/attribute_defs/dialog.py @@ -20,7 +20,8 @@ class AttributeDefinitionsDialog(QtWidgets.QDialog): btns_layout.addWidget(cancel_btn, 0) main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addWidget(attrs_widget, 1) + main_layout.addWidget(attrs_widget, 0) + main_layout.addStretch(1) main_layout.addWidget(btns_widget, 0) ok_btn.clicked.connect(self.accept) From cec37f0101037840ba29a8f2daf7caf8a961a0c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 14:36:50 +0100 Subject: [PATCH 133/160] loader can handle both qargparse and attribute definitions --- openpype/tools/loader/lib.py | 25 ++++++++++++++++++------- openpype/tools/utils/widgets.py | 24 ++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/openpype/tools/loader/lib.py b/openpype/tools/loader/lib.py index 28e94237ec..78a25d8d85 100644 --- a/openpype/tools/loader/lib.py +++ b/openpype/tools/loader/lib.py @@ -2,6 +2,8 @@ import inspect from Qt import QtGui import qtawesome +from openpype.lib.attribute_definitions import AbtractAttrDef +from openpype.tools.attribute_defs import AttributeDefinitionsDialog from openpype.tools.utils.widgets import ( OptionalAction, OptionDialog @@ -34,21 +36,30 @@ def get_options(action, loader, parent, repre_contexts): None when dialog was closed or cancelled, in all other cases {} if no options """ + # Pop option dialog options = {} loader_options = loader.get_options(repre_contexts) - if getattr(action, "optioned", False) and loader_options: + if not getattr(action, "optioned", False) or not loader_options: + return options + + if isinstance(loader_options[0], AbtractAttrDef): + qargparse_options = False + dialog = AttributeDefinitionsDialog(loader_options, parent) + else: + qargparse_options = True dialog = OptionDialog(parent) - dialog.setWindowTitle(action.label + " Options") dialog.create(loader_options) - if not dialog.exec_(): - return None + dialog.setWindowTitle(action.label + " Options") - # Get option - options = dialog.parse() + if not dialog.exec_(): + return None - return options + # Get option + if qargparse_options: + return dialog.parse() + return dialog.get_values() def add_representation_loaders_to_menu(loaders, menu, repre_contexts): diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index ca65182124..05513bc96e 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -3,10 +3,12 @@ import logging from Qt import QtWidgets, QtCore, QtGui import qargparse import qtawesome + from openpype.style import ( get_objected_colors, get_style_image_path ) +from openpype.lib.attribute_definitions import AbtractAttrDef log = logging.getLogger(__name__) @@ -317,8 +319,26 @@ class OptionalAction(QtWidgets.QWidgetAction): def set_option_tip(self, options): sep = "\n\n" - mak = (lambda opt: opt["name"] + " :\n " + opt["help"]) - self.option_tip = sep.join(mak(opt) for opt in options) + if not options or not isinstance(options[0], AbtractAttrDef): + mak = (lambda opt: opt["name"] + " :\n " + opt["help"]) + self.option_tip = sep.join(mak(opt) for opt in options) + return + + option_items = [] + for option in options: + option_lines = [] + if option.label: + option_lines.append( + "{} ({}) :".format(option.label, option.key) + ) + else: + option_lines.append("{} :".format(option.key)) + + if option.tooltip: + option_lines.append(" - {}".format(option.tooltip)) + option_items.append("\n".join(option_lines)) + + self.option_tip = sep.join(option_items) def on_option(self): self.optioned = True From 1738a177d2adaaaa8fa3cc95806c327673d0a866 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 14:43:34 +0100 Subject: [PATCH 134/160] tvpaint loaders are using attribute definitions instead of qargparse --- .../hosts/tvpaint/plugins/load/load_image.py | 44 +++++++++--------- .../plugins/load/load_reference_image.py | 45 ++++++++++--------- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/load/load_image.py b/openpype/hosts/tvpaint/plugins/load/load_image.py index 151db94135..5283d04355 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_image.py +++ b/openpype/hosts/tvpaint/plugins/load/load_image.py @@ -1,4 +1,4 @@ -import qargparse +from openpype.lib.attribute_definitions import BoolDef from openpype.hosts.tvpaint.api import plugin from openpype.hosts.tvpaint.api.lib import execute_george_through_file @@ -27,26 +27,28 @@ class ImportImage(plugin.Loader): "preload": True } - options = [ - qargparse.Boolean( - "stretch", - label="Stretch to project size", - default=True, - help="Stretch loaded image/s to project resolution?" - ), - qargparse.Boolean( - "timestretch", - label="Stretch to timeline length", - default=True, - help="Clip loaded image/s to timeline length?" - ), - qargparse.Boolean( - "preload", - label="Preload loaded image/s", - default=True, - help="Preload image/s?" - ) - ] + @classmethod + def get_options(cls, contexts): + return [ + BoolDef( + "stretch", + label="Stretch to project size", + default=cls.defaults["stretch"], + tooltip="Stretch loaded image/s to project resolution?" + ), + BoolDef( + "timestretch", + label="Stretch to timeline length", + default=cls.defaults["timestretch"], + tooltip="Clip loaded image/s to timeline length?" + ), + BoolDef( + "preload", + label="Preload loaded image/s", + default=cls.defaults["preload"], + tooltip="Preload image/s?" + ) + ] def load(self, context, name, namespace, options): stretch = options.get("stretch", self.defaults["stretch"]) diff --git a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py index 393236fba6..7f7a68cc41 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py +++ b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py @@ -1,7 +1,6 @@ import collections -import qargparse - +from openpype.lib.attribute_definitions import BoolDef from openpype.pipeline import ( get_representation_context, register_host, @@ -42,26 +41,28 @@ class LoadImage(plugin.Loader): "preload": True } - options = [ - qargparse.Boolean( - "stretch", - label="Stretch to project size", - default=True, - help="Stretch loaded image/s to project resolution?" - ), - qargparse.Boolean( - "timestretch", - label="Stretch to timeline length", - default=True, - help="Clip loaded image/s to timeline length?" - ), - qargparse.Boolean( - "preload", - label="Preload loaded image/s", - default=True, - help="Preload image/s?" - ) - ] + @classmethod + def get_options(cls, contexts): + return [ + BoolDef( + "stretch", + label="Stretch to project size", + default=cls.defaults["stretch"], + tooltip="Stretch loaded image/s to project resolution?" + ), + BoolDef( + "timestretch", + label="Stretch to timeline length", + default=cls.defaults["timestretch"], + tooltip="Clip loaded image/s to timeline length?" + ), + BoolDef( + "preload", + label="Preload loaded image/s", + default=cls.defaults["preload"], + tooltip="Preload image/s?" + ) + ] def load(self, context, name, namespace, options): stretch = options.get("stretch", self.defaults["stretch"]) From 761d624b2e40901be713068157ad7d7aeaeb3bb4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 16:07:50 +0100 Subject: [PATCH 135/160] fix comparison of repre name --- openpype/tools/loader/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index d37ce500e0..826c7110da 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -515,7 +515,7 @@ class SubsetWidget(QtWidgets.QWidget): if not one_item_selected: # Filter loaders from first subset by intersected combinations for repre, loader in first_loaders: - if (repre["name"], loader) not in found_combinations: + if (repre["name"].lower(), loader) not in found_combinations: continue loaders.append((repre, loader)) From 7df622df0cfd2af6092faf5918e75e97e1658473 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 18:29:21 +0100 Subject: [PATCH 136/160] 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 71338f96e0..4fd460ffea 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1159,9 +1159,7 @@ class CreateContext: 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 @@ class CreateContext: # 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. From bae7a9960a4f28e23aab10f959789eaa00c20c87 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 2 Nov 2022 04:02:57 +0000 Subject: [PATCH 137/160] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 442c5f033b..46bb4b1cd0 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.7-nightly.1" +__version__ = "3.14.7-nightly.2" From ea5a1f8e525a92af8a8c071bce7e55c92a99f451 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 2 Nov 2022 19:43:37 +0100 Subject: [PATCH 138/160] added new settings for filtering of single frame output for review --- .../settings/defaults/project_settings/global.json | 3 ++- .../schemas/schema_global_publish.json | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index b128564bc2..8c129eeff5 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -79,7 +79,8 @@ "ftrack" ], "subsets": [], - "custom_tags": [] + "custom_tags": [], + "single_frame_filter": "everytime" }, "overscan_crop": "", "overscan_color": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 51fc8dedf3..a39ae9eff4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -304,6 +304,20 @@ "label": "Custom Tags", "type": "list", "object_type": "text" + }, + { + "type": "label", + "label": "Use output always / only if input is 1 frame image / only if has 2+ frames or is video" + }, + { + "type": "enum", + "key": "single_frame_filter", + "default": "everytime", + "enum_items": [ + {"everytime": "Always"}, + {"single_frame": "On 1 frame input"}, + {"multi_frame": "On 2+ frame input"} + ] } ] }, From 2c0ea81df64ea1958bbf377923ae4f7b3ed51eee Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 2 Nov 2022 19:44:04 +0100 Subject: [PATCH 139/160] added single frame filtering logic into extract review --- openpype/plugins/publish/extract_review.py | 56 ++++++++++++++++------ 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index d457bdc988..0c902cb568 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -3,26 +3,26 @@ import re import copy import json import shutil - from abc import ABCMeta, abstractmethod + import six - import clique - +import speedcopy import pyblish.api from openpype.lib import ( get_ffmpeg_tool_path, - get_ffprobe_streams, path_to_subprocess_arg, run_subprocess, - +) +from openpype.lib.transcoding import ( + IMAGE_EXTENSIONS, + get_ffprobe_streams, should_convert_for_ffmpeg, convert_input_paths_for_ffmpeg, - get_transcode_temp_directory + get_transcode_temp_directory, ) -import speedcopy class ExtractReview(pyblish.api.InstancePlugin): @@ -175,6 +175,26 @@ class ExtractReview(pyblish.api.InstancePlugin): outputs_per_representations.append((repre, outputs)) return outputs_per_representations + def _single_frame_filter(self, input_filepaths, output_defs): + single_frame_image = False + if len(input_filepaths) == 1: + ext = os.path.splitext(input_filepaths[0])[-1] + single_frame_image = ext in IMAGE_EXTENSIONS + + filtered_defs = [] + for output_def in output_defs: + output_filters = output_def.get("filter") or {} + frame_filter = output_filters.get("single_frame_filter") + if ( + (not single_frame_image and frame_filter == "single_frame") + or (single_frame_image and frame_filter == "multi_frame") + ): + continue + + filtered_defs.append(output_def) + + return filtered_defs + @staticmethod def get_instance_label(instance): return ( @@ -195,7 +215,7 @@ class ExtractReview(pyblish.api.InstancePlugin): outputs_per_repres = self._get_outputs_per_representations( instance, profile_outputs ) - for repre, outpu_defs in outputs_per_repres: + for repre, output_defs in outputs_per_repres: # Check if input should be preconverted before processing # Store original staging dir (it's value may change) src_repre_staging_dir = repre["stagingDir"] @@ -216,6 +236,16 @@ class ExtractReview(pyblish.api.InstancePlugin): if first_input_path is None: first_input_path = filepath + filtered_output_defs = self._single_frame_filter( + input_filepaths, output_defs + ) + if not filtered_output_defs: + self.log.debug(( + "Repre: {} - All output definitions were filtered" + " out by single frame filter. Skipping" + ).format(repre["name"])) + continue + # Skip if file is not set if first_input_path is None: self.log.warning(( @@ -249,7 +279,7 @@ class ExtractReview(pyblish.api.InstancePlugin): try: self._render_output_definitions( - instance, repre, src_repre_staging_dir, outpu_defs + instance, repre, src_repre_staging_dir, filtered_output_defs ) finally: @@ -263,10 +293,10 @@ class ExtractReview(pyblish.api.InstancePlugin): shutil.rmtree(new_staging_dir) def _render_output_definitions( - self, instance, repre, src_repre_staging_dir, outpu_defs + self, instance, repre, src_repre_staging_dir, output_defs ): fill_data = copy.deepcopy(instance.data["anatomyData"]) - for _output_def in outpu_defs: + for _output_def in output_defs: output_def = copy.deepcopy(_output_def) # Make sure output definition has "tags" key if "tags" not in output_def: @@ -1659,9 +1689,7 @@ class ExtractReview(pyblish.api.InstancePlugin): return True return False - def filter_output_defs( - self, profile, subset_name, families - ): + def filter_output_defs(self, profile, subset_name, families): """Return outputs matching input instance families. Output definitions without families filter are marked as valid. From e5205f5c81b677209e1626866c100ffd169ac414 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 2 Nov 2022 19:53:03 +0100 Subject: [PATCH 140/160] prepared common function to cache instances during collection phase --- openpype/pipeline/create/__init__.py | 2 ++ openpype/pipeline/create/creator_plugins.py | 32 +++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index 4b91951a08..9e858151fd 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -24,6 +24,8 @@ from .creator_plugins import ( deregister_creator_plugin, register_creator_plugin_path, deregister_creator_plugin_path, + + cache_and_get_instances, ) from .context import ( diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index c69abb8861..0f9c346966 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -1,5 +1,6 @@ import os import copy +import collections from abc import ( ABCMeta, @@ -660,3 +661,34 @@ def deregister_creator_plugin_path(path): deregister_plugin_path(BaseCreator, path) deregister_plugin_path(LegacyCreator, path) deregister_plugin_path(SubsetConvertorPlugin, path) + + +def cache_and_get_instances(creator, shared_key, list_instances_func): + """Common approach to cache instances in shared data. + + This is helper function which does not handle cases when a 'shared_key' is + used for different list instances functions. The same approach of caching + instances into 'collection_shared_data' is not required but is so common + we've decided to unify it to some degree. + + Function 'list_instances_func' is called only if 'shared_key' is not + available in 'collection_shared_data' on creator. + + Args: + creator (Creator): Plugin which would like to get instance data. + shared_key (str): Key under which output of function will be stored. + list_instances_func (Function): Function that will return instance data + if data were not yet stored under 'shared_key'. + + Returns: + Dict[str, Dict[str, Any]]: Cached instances by creator identifier from + result of passed function. + """ + + if shared_key not in creator.collection_shared_data: + value = collections.defaultdict(list) + for instance in list_instances_func(): + identifier = instance.get("creator_identifier") + value[identifier].append(instance) + creator.collection_shared_data[shared_key] = value + return creator.collection_shared_data[shared_key] From 53467f97f941eaeb60651f177c5639d4b45f314d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 2 Nov 2022 19:54:01 +0100 Subject: [PATCH 141/160] use new function in tray publisher --- openpype/hosts/traypublisher/api/plugin.py | 45 ++++++++-------------- 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 555041d389..24d7004e84 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -2,7 +2,8 @@ from openpype.lib.attribute_definitions import FileDef from openpype.pipeline.create import ( Creator, HiddenCreator, - CreatedInstance + CreatedInstance, + cache_and_get_instances, ) from .pipeline import ( @@ -16,34 +17,19 @@ from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS REVIEW_EXTENSIONS = IMAGE_EXTENSIONS + VIDEO_EXTENSIONS - -def _cache_and_get_instances(creator): - """Cache instances in shared data. - - Args: - creator (Creator): Plugin which would like to get instances from host. - - Returns: - List[Dict[str, Any]]: Cached instances list from host implementation. - """ - - shared_key = "openpype.traypublisher.instances" - if shared_key not in creator.collection_shared_data: - creator.collection_shared_data[shared_key] = list_instances() - return creator.collection_shared_data[shared_key] +SHARED_DATA_KEY = "openpype.traypublisher.instances" 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) + instances_by_identifier = cache_and_get_instances( + self, SHARED_DATA_KEY, list_instances + ) + for instance_data in instances_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 +60,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) + instances_by_identifier = cache_and_get_instances( + self, SHARED_DATA_KEY, list_instances + ) + for instance_data in instances_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 eaa097d513ba5ec55e9b80267f634ca77d39fe9b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 2 Nov 2022 19:54:13 +0100 Subject: [PATCH 142/160] change 'REVIEW_EXTENSIONS' to set instead of list --- openpype/hosts/traypublisher/api/plugin.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 24d7004e84..6b95379cf2 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -1,22 +1,19 @@ from openpype.lib.attribute_definitions import FileDef +from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS from openpype.pipeline.create import ( Creator, HiddenCreator, CreatedInstance, cache_and_get_instances, ) - from .pipeline import ( list_instances, update_instances, remove_instances, HostContext, ) -from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS - - -REVIEW_EXTENSIONS = IMAGE_EXTENSIONS + VIDEO_EXTENSIONS +REVIEW_EXTENSIONS = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS) SHARED_DATA_KEY = "openpype.traypublisher.instances" From e3c339b1537a3afdde9b694c825a8ab71ad5abe2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Nov 2022 11:27:53 +0100 Subject: [PATCH 143/160] added information about additional filtering --- website/docs/project_settings/settings_project_global.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 24ea09b6fb..9666c6568a 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -135,6 +135,12 @@ Profile may generate multiple outputs from a single input. Each output must defi - set alpha to `0` to not use this option at all (in most of cases background stays black) - other than `0` alpha will draw color as background +- **`Additional filtering`** + - Profile filtering defines which group of output definitions is used but output definitions may require more specific filters on their own. + - They may filter by subset name (regex can be used) or publish families. Publish families are more complex as are based on knowing code base. + - Filtering by custom tags -> this is used for targeting to output definitions from other extractors using settings (at this moment only Nuke bake extractor can target using custom tags). + - Nuke extractor settings path: `project_settings/nuke/publish/ExtractReviewDataMov/outputs/baking/add_custom_tags` + - Filtering by input length. Input may be video, sequence or single image. It is possible that `.mp4` should be created only when input is video or sequence and to create review `.png` when input is single frame. In some cases the output should be created even if it's single frame or multi frame input. ### IntegrateAssetNew From f694b433a581e45a371c99b960d905dbe7925efe Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Nov 2022 11:32:53 +0100 Subject: [PATCH 144/160] fix too long line --- openpype/plugins/publish/extract_review.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 0c902cb568..1f9b30fba3 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -279,7 +279,10 @@ class ExtractReview(pyblish.api.InstancePlugin): try: self._render_output_definitions( - instance, repre, src_repre_staging_dir, filtered_output_defs + instance, + repre, + src_repre_staging_dir, + filtered_output_defs ) finally: From 3c4476c2335ce760d2a49a80531f09705af79c68 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Nov 2022 12:00:44 +0100 Subject: [PATCH 145/160] added default png output definition --- .../defaults/project_settings/global.json | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 8c129eeff5..9c3f2f1e1b 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -53,6 +53,62 @@ "families": [], "hosts": [], "outputs": { + "png": { + "ext": "png", + "tags": [ + "ftrackreview" + ], + "burnins": [], + "ffmpeg_args": { + "video_filters": [], + "audio_filters": [], + "input": [], + "output": [] + }, + "filter": { + "families": [ + "render", + "review", + "ftrack" + ], + "subsets": [], + "custom_tags": [], + "single_frame_filter": "single_frame" + }, + "overscan_crop": "", + "overscan_color": [ + 0, + 0, + 0, + 255 + ], + "width": 1920, + "height": 1080, + "scale_pixel_aspect": true, + "bg_color": [ + 0, + 0, + 0, + 0 + ], + "letter_box": { + "enabled": false, + "ratio": 0.0, + "fill_color": [ + 0, + 0, + 0, + 255 + ], + "line_thickness": 0, + "line_color": [ + 255, + 0, + 0, + 255 + ] + } + }, "h264": { "ext": "mp4", "tags": [ @@ -80,7 +136,7 @@ ], "subsets": [], "custom_tags": [], - "single_frame_filter": "everytime" + "single_frame_filter": "multi_frame" }, "overscan_crop": "", "overscan_color": [ From 0b201e4f48175a72431460d80b1e9f337267b10d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Nov 2022 13:23:17 +0100 Subject: [PATCH 146/160] change labels in schema --- .../projects_schema/schemas/schema_global_publish.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index a39ae9eff4..742437fbde 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -315,8 +315,8 @@ "default": "everytime", "enum_items": [ {"everytime": "Always"}, - {"single_frame": "On 1 frame input"}, - {"multi_frame": "On 2+ frame input"} + {"single_frame": "Only if input has 1 image frame"}, + {"multi_frame": "Only if input is video or sequence of frames"} ] } ] From 646a289860cfce3d352a7f8b06d1a264086cbc1c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Nov 2022 16:15:35 +0100 Subject: [PATCH 147/160] interfaces are in init of modules --- openpype/modules/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py index 02e7dc13ab..1f345feea9 100644 --- a/openpype/modules/__init__.py +++ b/openpype/modules/__init__.py @@ -1,4 +1,14 @@ # -*- coding: utf-8 -*- +from .interfaces import ( + ILaunchHookPaths, + IPluginPaths, + ITrayModule, + ITrayAction, + ITrayService, + ISettingsChangeListener, + IHostAddon, +) + from .base import ( OpenPypeModule, OpenPypeAddOn, @@ -17,6 +27,14 @@ from .base import ( __all__ = ( + "ILaunchHookPaths", + "IPluginPaths", + "ITrayModule", + "ITrayAction", + "ITrayService", + "ISettingsChangeListener", + "IHostAddon", + "OpenPypeModule", "OpenPypeAddOn", From 0478d3ea59db98e732c13891d42e93676a11c0d8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Nov 2022 16:21:58 +0100 Subject: [PATCH 148/160] use new import of interfaces --- openpype/modules/avalon_apps/avalon_app.py | 3 +-- openpype/modules/clockify/clockify_module.py | 11 ++++++----- openpype/modules/deadline/deadline_module.py | 3 +-- .../modules/example_addons/example_addon/addon.py | 5 +---- openpype/modules/kitsu/kitsu_module.py | 7 +++++-- openpype/modules/launcher_action.py | 6 ++++-- openpype/modules/log_viewer/log_view_module.py | 3 +-- openpype/modules/muster/muster.py | 3 +-- openpype/modules/project_manager_action.py | 3 +-- .../modules/python_console_interpreter/module.py | 3 +-- openpype/modules/royalrender/royal_render_module.py | 3 +-- openpype/modules/settings_action.py | 3 +-- openpype/modules/shotgrid/shotgrid_module.py | 5 ++--- openpype/modules/sync_server/sync_server_module.py | 12 ++++++------ openpype/modules/timers_manager/timers_manager.py | 4 ++-- openpype/modules/webserver/host_console_listener.py | 2 +- openpype/modules/webserver/webserver_module.py | 3 +-- openpype/settings/lib.py | 9 +++------ openpype/tools/tray/pype_tray.py | 2 +- 19 files changed, 40 insertions(+), 50 deletions(-) diff --git a/openpype/modules/avalon_apps/avalon_app.py b/openpype/modules/avalon_apps/avalon_app.py index 1d21de129b..f9085522b0 100644 --- a/openpype/modules/avalon_apps/avalon_app.py +++ b/openpype/modules/avalon_apps/avalon_app.py @@ -1,7 +1,6 @@ import os -from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayModule +from openpype.modules import OpenPypeModule, ITrayModule class AvalonModule(OpenPypeModule, ITrayModule): diff --git a/openpype/modules/clockify/clockify_module.py b/openpype/modules/clockify/clockify_module.py index 932ce87c36..14fcb01f67 100644 --- a/openpype/modules/clockify/clockify_module.py +++ b/openpype/modules/clockify/clockify_module.py @@ -2,16 +2,17 @@ import os import threading import time +from openpype.modules import ( + OpenPypeModule, + ITrayModule, + IPluginPaths +) + from .clockify_api import ClockifyAPI from .constants import ( CLOCKIFY_FTRACK_USER_PATH, CLOCKIFY_FTRACK_SERVER_PATH ) -from openpype.modules import OpenPypeModule -from openpype_interfaces import ( - ITrayModule, - IPluginPaths -) class ClockifyModule( diff --git a/openpype/modules/deadline/deadline_module.py b/openpype/modules/deadline/deadline_module.py index bbd0f74e8a..9855f8c1b1 100644 --- a/openpype/modules/deadline/deadline_module.py +++ b/openpype/modules/deadline/deadline_module.py @@ -4,8 +4,7 @@ import six import sys from openpype.lib import requests_get, Logger -from openpype.modules import OpenPypeModule -from openpype_interfaces import IPluginPaths +from openpype.modules import OpenPypeModule, IPluginPaths class DeadlineWebserviceError(Exception): diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py index 50554b1e43..ead647b41d 100644 --- a/openpype/modules/example_addons/example_addon/addon.py +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -13,10 +13,7 @@ import click from openpype.modules import ( JsonFilesSettingsDef, OpenPypeAddOn, - ModulesManager -) -# Import interface defined by this addon to be able find other addons using it -from openpype_interfaces import ( + ModulesManager, IPluginPaths, ITrayAction ) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 23c032715b..b91373af20 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -3,8 +3,11 @@ import click import os -from openpype.modules import OpenPypeModule -from openpype_interfaces import IPluginPaths, ITrayAction +from openpype.modules import ( + OpenPypeModule, + IPluginPaths, + ITrayAction, +) class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): diff --git a/openpype/modules/launcher_action.py b/openpype/modules/launcher_action.py index e3252e3842..c4331b6094 100644 --- a/openpype/modules/launcher_action.py +++ b/openpype/modules/launcher_action.py @@ -1,5 +1,7 @@ -from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayAction +from openpype.modules import ( + OpenPypeModule, + ITrayAction, +) class LauncherAction(OpenPypeModule, ITrayAction): diff --git a/openpype/modules/log_viewer/log_view_module.py b/openpype/modules/log_viewer/log_view_module.py index da1628b71f..31e954fadd 100644 --- a/openpype/modules/log_viewer/log_view_module.py +++ b/openpype/modules/log_viewer/log_view_module.py @@ -1,5 +1,4 @@ -from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayModule +from openpype.modules import OpenPypeModule, ITrayModule class LogViewModule(OpenPypeModule, ITrayModule): diff --git a/openpype/modules/muster/muster.py b/openpype/modules/muster/muster.py index 6e26ad2d7b..8d395d16e8 100644 --- a/openpype/modules/muster/muster.py +++ b/openpype/modules/muster/muster.py @@ -2,8 +2,7 @@ import os import json import appdirs import requests -from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayModule +from openpype.modules import OpenPypeModule, ITrayModule class MusterModule(OpenPypeModule, ITrayModule): diff --git a/openpype/modules/project_manager_action.py b/openpype/modules/project_manager_action.py index 251964a059..5f74dd9ee5 100644 --- a/openpype/modules/project_manager_action.py +++ b/openpype/modules/project_manager_action.py @@ -1,5 +1,4 @@ -from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayAction +from openpype.modules import OpenPypeModule, ITrayAction class ProjectManagerAction(OpenPypeModule, ITrayAction): diff --git a/openpype/modules/python_console_interpreter/module.py b/openpype/modules/python_console_interpreter/module.py index 8c4a2fba73..cb99c05e37 100644 --- a/openpype/modules/python_console_interpreter/module.py +++ b/openpype/modules/python_console_interpreter/module.py @@ -1,5 +1,4 @@ -from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayAction +from openpype.modules import OpenPypeModule, ITrayAction class PythonInterpreterAction(OpenPypeModule, ITrayAction): diff --git a/openpype/modules/royalrender/royal_render_module.py b/openpype/modules/royalrender/royal_render_module.py index 4f72860ad6..10d74d01d1 100644 --- a/openpype/modules/royalrender/royal_render_module.py +++ b/openpype/modules/royalrender/royal_render_module.py @@ -2,8 +2,7 @@ """Module providing support for Royal Render.""" import os import openpype.modules -from openpype.modules import OpenPypeModule -from openpype_interfaces import IPluginPaths +from openpype.modules import OpenPypeModule, IPluginPaths class RoyalRenderModule(OpenPypeModule, IPluginPaths): diff --git a/openpype/modules/settings_action.py b/openpype/modules/settings_action.py index 1e7eca4dec..1902caff1d 100644 --- a/openpype/modules/settings_action.py +++ b/openpype/modules/settings_action.py @@ -1,5 +1,4 @@ -from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayAction +from openpype.modules import OpenPypeModule, ITrayAction class SettingsAction(OpenPypeModule, ITrayAction): diff --git a/openpype/modules/shotgrid/shotgrid_module.py b/openpype/modules/shotgrid/shotgrid_module.py index 281c6fdcad..d26647d06a 100644 --- a/openpype/modules/shotgrid/shotgrid_module.py +++ b/openpype/modules/shotgrid/shotgrid_module.py @@ -1,12 +1,11 @@ import os -from openpype_interfaces import ( +from openpype.modules import ( + OpenPypeModule, ITrayModule, IPluginPaths, ) -from openpype.modules import OpenPypeModule - SHOTGRID_MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index a478faa9ef..e84c333a58 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -11,9 +11,12 @@ from collections import deque, defaultdict import click from bson.objectid import ObjectId -from openpype.client import get_projects -from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayModule +from openpype.client import ( + get_projects, + get_representations, + get_representation_by_id, +) +from openpype.modules import OpenPypeModule, ITrayModule from openpype.settings import ( get_project_settings, get_system_settings, @@ -30,9 +33,6 @@ from .providers import lib from .utils import time_function, SyncStatus, SiteAlreadyPresentError -from openpype.client import get_representations, get_representation_by_id - - log = Logger.get_logger("SyncServer") diff --git a/openpype/modules/timers_manager/timers_manager.py b/openpype/modules/timers_manager/timers_manager.py index c168e9534d..27f9a4f68b 100644 --- a/openpype/modules/timers_manager/timers_manager.py +++ b/openpype/modules/timers_manager/timers_manager.py @@ -3,8 +3,8 @@ import platform from openpype.client import get_asset_by_name -from openpype.modules import OpenPypeModule -from openpype_interfaces import ( +from openpype.modules.interfaces import ( + OpenPypeModule, ITrayService, IPluginPaths ) diff --git a/openpype/modules/webserver/host_console_listener.py b/openpype/modules/webserver/host_console_listener.py index 6138f9f097..fdfe1ba688 100644 --- a/openpype/modules/webserver/host_console_listener.py +++ b/openpype/modules/webserver/host_console_listener.py @@ -5,7 +5,7 @@ import logging from concurrent.futures import CancelledError from Qt import QtWidgets -from openpype_interfaces import ITrayService +from openpype.modules import ITrayService log = logging.getLogger(__name__) diff --git a/openpype/modules/webserver/webserver_module.py b/openpype/modules/webserver/webserver_module.py index 16861abd29..354ab1e4f9 100644 --- a/openpype/modules/webserver/webserver_module.py +++ b/openpype/modules/webserver/webserver_module.py @@ -24,8 +24,7 @@ import os import socket from openpype import resources -from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayService +from openpype.modules import OpenPypeModule, ITrayService class WebServerModule(OpenPypeModule, ITrayService): diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 5eaddf6e6e..288c587d03 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -138,8 +138,7 @@ def save_studio_settings(data): SaveWarningExc: If any module raises the exception. """ # Notify Pype modules - from openpype.modules import ModulesManager - from openpype_interfaces import ISettingsChangeListener + from openpype.modules import ModulesManager, ISettingsChangeListener old_data = get_system_settings() default_values = get_default_settings()[SYSTEM_SETTINGS_KEY] @@ -186,8 +185,7 @@ def save_project_settings(project_name, overrides): SaveWarningExc: If any module raises the exception. """ # Notify Pype modules - from openpype.modules import ModulesManager - from openpype_interfaces import ISettingsChangeListener + from openpype.modules import ModulesManager, ISettingsChangeListener default_values = get_default_settings()[PROJECT_SETTINGS_KEY] if project_name: @@ -248,8 +246,7 @@ def save_project_anatomy(project_name, anatomy_data): SaveWarningExc: If any module raises the exception. """ # Notify Pype modules - from openpype.modules import ModulesManager - from openpype_interfaces import ISettingsChangeListener + from openpype.modules import ModulesManager, ISettingsChangeListener default_values = get_default_settings()[PROJECT_ANATOMY_KEY] if project_name: diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 3842a4e216..d4189af4d8 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -401,7 +401,7 @@ class TrayManager: def initialize_modules(self): """Add modules to tray.""" - from openpype_interfaces import ( + from openpype.modules import ( ITrayAction, ITrayService ) From 19c8fae79912424f9a4897f3d29181078e9c33b7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Nov 2022 16:26:18 +0100 Subject: [PATCH 149/160] fix timers manager --- openpype/modules/timers_manager/timers_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/timers_manager/timers_manager.py b/openpype/modules/timers_manager/timers_manager.py index 27f9a4f68b..0ba68285a4 100644 --- a/openpype/modules/timers_manager/timers_manager.py +++ b/openpype/modules/timers_manager/timers_manager.py @@ -3,7 +3,7 @@ import platform from openpype.client import get_asset_by_name -from openpype.modules.interfaces import ( +from openpype.modules import ( OpenPypeModule, ITrayService, IPluginPaths From 81017f90cf9844c70bf8feb3dd45804f2aa954d7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Nov 2022 16:26:26 +0100 Subject: [PATCH 150/160] added forgotten modules --- openpype/modules/ftrack/ftrack_module.py | 4 ++-- openpype/modules/slack/slack_module.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index 678af0e577..6f14f8428d 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -5,8 +5,8 @@ import platform import click -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import ( +from openpype.modules import ( + OpenPypeModule, ITrayModule, IPluginPaths, ISettingsChangeListener diff --git a/openpype/modules/slack/slack_module.py b/openpype/modules/slack/slack_module.py index 499c1c19ce..797ae19f4a 100644 --- a/openpype/modules/slack/slack_module.py +++ b/openpype/modules/slack/slack_module.py @@ -1,6 +1,5 @@ import os -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IPluginPaths +from openpype.modules import OpenPypeModule, IPluginPaths SLACK_MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) From 12c28fe4668985e0e990921537a06e85e239003e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Nov 2022 16:27:38 +0100 Subject: [PATCH 151/160] change imports in hosts --- openpype/hosts/aftereffects/addon.py | 3 +-- openpype/hosts/blender/addon.py | 3 +-- openpype/hosts/flame/addon.py | 3 +-- openpype/hosts/fusion/addon.py | 3 +-- openpype/hosts/harmony/addon.py | 3 +-- openpype/hosts/hiero/addon.py | 3 +-- openpype/hosts/houdini/addon.py | 3 +-- openpype/hosts/maya/addon.py | 3 +-- openpype/hosts/nuke/addon.py | 3 +-- openpype/hosts/photoshop/addon.py | 3 +-- openpype/hosts/resolve/addon.py | 3 +-- openpype/hosts/standalonepublisher/addon.py | 3 +-- openpype/hosts/traypublisher/addon.py | 3 +-- openpype/hosts/tvpaint/addon.py | 3 +-- openpype/hosts/unreal/addon.py | 3 +-- openpype/hosts/webpublisher/addon.py | 3 +-- 16 files changed, 16 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/aftereffects/addon.py b/openpype/hosts/aftereffects/addon.py index 94843e7dc5..79df550312 100644 --- a/openpype/hosts/aftereffects/addon.py +++ b/openpype/hosts/aftereffects/addon.py @@ -1,5 +1,4 @@ -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostAddon +from openpype.modules import OpenPypeModule, IHostAddon class AfterEffectsAddon(OpenPypeModule, IHostAddon): diff --git a/openpype/hosts/blender/addon.py b/openpype/hosts/blender/addon.py index 3ee638a5bb..f1da9b808c 100644 --- a/openpype/hosts/blender/addon.py +++ b/openpype/hosts/blender/addon.py @@ -1,6 +1,5 @@ import os -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostAddon +from openpype.modules import OpenPypeModule, IHostAddon BLENDER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/hosts/flame/addon.py b/openpype/hosts/flame/addon.py index 5a34413bb0..d9359fc5bf 100644 --- a/openpype/hosts/flame/addon.py +++ b/openpype/hosts/flame/addon.py @@ -1,6 +1,5 @@ import os -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostAddon +from openpype.modules import OpenPypeModule, IHostAddon HOST_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/hosts/fusion/addon.py b/openpype/hosts/fusion/addon.py index 1913cc2e30..d1bd1566b7 100644 --- a/openpype/hosts/fusion/addon.py +++ b/openpype/hosts/fusion/addon.py @@ -1,6 +1,5 @@ import os -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostAddon +from openpype.modules import OpenPypeModule, IHostAddon FUSION_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/hosts/harmony/addon.py b/openpype/hosts/harmony/addon.py index 872a7490b5..efef40ab92 100644 --- a/openpype/hosts/harmony/addon.py +++ b/openpype/hosts/harmony/addon.py @@ -1,6 +1,5 @@ import os -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostAddon +from openpype.modules import OpenPypeModule, IHostAddon HARMONY_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/hosts/hiero/addon.py b/openpype/hosts/hiero/addon.py index 3523e9aed7..f5bb94dbaa 100644 --- a/openpype/hosts/hiero/addon.py +++ b/openpype/hosts/hiero/addon.py @@ -1,7 +1,6 @@ import os import platform -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostAddon +from openpype.modules import OpenPypeModule, IHostAddon HIERO_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/hosts/houdini/addon.py b/openpype/hosts/houdini/addon.py index 8d88e83c56..80856b0624 100644 --- a/openpype/hosts/houdini/addon.py +++ b/openpype/hosts/houdini/addon.py @@ -1,6 +1,5 @@ import os -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostAddon +from openpype.modules import OpenPypeModule, IHostAddon HOUDINI_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/hosts/maya/addon.py b/openpype/hosts/maya/addon.py index cdd2bc1667..b9ecb8279f 100644 --- a/openpype/hosts/maya/addon.py +++ b/openpype/hosts/maya/addon.py @@ -1,6 +1,5 @@ import os -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostAddon +from openpype.modules import OpenPypeModule, IHostAddon MAYA_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/hosts/nuke/addon.py b/openpype/hosts/nuke/addon.py index 54e4da5195..1c5d5c4005 100644 --- a/openpype/hosts/nuke/addon.py +++ b/openpype/hosts/nuke/addon.py @@ -1,7 +1,6 @@ import os import platform -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostAddon +from openpype.modules import OpenPypeModule, IHostAddon NUKE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/hosts/photoshop/addon.py b/openpype/hosts/photoshop/addon.py index a41d91554b..965a545ac5 100644 --- a/openpype/hosts/photoshop/addon.py +++ b/openpype/hosts/photoshop/addon.py @@ -1,6 +1,5 @@ import os -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostAddon +from openpype.modules import OpenPypeModule, IHostAddon PHOTOSHOP_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/hosts/resolve/addon.py b/openpype/hosts/resolve/addon.py index a31da52a6d..02c1d7957f 100644 --- a/openpype/hosts/resolve/addon.py +++ b/openpype/hosts/resolve/addon.py @@ -1,7 +1,6 @@ import os -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostAddon +from openpype.modules import OpenPypeModule, IHostAddon from .utils import RESOLVE_ROOT_DIR diff --git a/openpype/hosts/standalonepublisher/addon.py b/openpype/hosts/standalonepublisher/addon.py index 98ec44d4e2..65a4226664 100644 --- a/openpype/hosts/standalonepublisher/addon.py +++ b/openpype/hosts/standalonepublisher/addon.py @@ -4,8 +4,7 @@ import click from openpype.lib import get_openpype_execute_args from openpype.lib.execute import run_detached_process -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import ITrayAction, IHostAddon +from openpype.modules import OpenPypeModule, ITrayAction, IHostAddon STANDALONEPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/hosts/traypublisher/addon.py b/openpype/hosts/traypublisher/addon.py index c86c835ed9..c157799898 100644 --- a/openpype/hosts/traypublisher/addon.py +++ b/openpype/hosts/traypublisher/addon.py @@ -4,8 +4,7 @@ import click from openpype.lib import get_openpype_execute_args from openpype.lib.execute import run_detached_process -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import ITrayAction, IHostAddon +from openpype.modules import OpenPypeModule, ITrayAction, IHostAddon TRAYPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/hosts/tvpaint/addon.py b/openpype/hosts/tvpaint/addon.py index d710e63f93..b695bf8ecc 100644 --- a/openpype/hosts/tvpaint/addon.py +++ b/openpype/hosts/tvpaint/addon.py @@ -1,6 +1,5 @@ import os -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostAddon +from openpype.modules import OpenPypeModule, IHostAddon TVPAINT_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 16736214c5..e2c8484651 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -1,6 +1,5 @@ import os -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostAddon +from openpype.modules import OpenPypeModule, IHostAddon UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/openpype/hosts/webpublisher/addon.py b/openpype/hosts/webpublisher/addon.py index a64d74e62b..eb7fced2e6 100644 --- a/openpype/hosts/webpublisher/addon.py +++ b/openpype/hosts/webpublisher/addon.py @@ -2,8 +2,7 @@ import os import click -from openpype.modules import OpenPypeModule -from openpype.modules.interfaces import IHostAddon +from openpype.modules import OpenPypeModule, IHostAddon WEBPUBLISHER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) From b716eb18cfa46bf9d527a76bf66feb7de14132fc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Nov 2022 16:36:59 +0100 Subject: [PATCH 152/160] added deprecation warning --- openpype/modules/base.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 09aea50424..4761462df0 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -9,6 +9,7 @@ import logging import platform import threading import collections +import traceback from uuid import uuid4 from abc import ABCMeta, abstractmethod import six @@ -139,6 +140,15 @@ class _InterfacesClass(_ModuleClass): "cannot import name '{}' from 'openpype_interfaces'" ).format(attr_name)) + if _LoadCache.interfaces_loaded and attr_name != "log": + stack = list(traceback.extract_stack()) + stack.pop(-1) + self.log.warning(( + "Using deprecated import of \"{}\" from 'openpype_interfaces'." + " Please switch to use import" + " from 'openpype.modules.interfaces'" + " (will be removed after 3.16.x).{}" + ).format(attr_name, "".join(traceback.format_list(stack)))) return self.__attributes__[attr_name] From 406a7e0e2e0e39811f9c8d6b28e2aea28896da40 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Nov 2022 16:42:35 +0100 Subject: [PATCH 153/160] project list does not trigger unsaved changes dialog if is not in edit node --- openpype/tools/settings/settings/categories.py | 4 ++++ openpype/tools/settings/settings/widgets.py | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index f4b2c13a12..e1b3943317 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -892,6 +892,10 @@ class ProjectWidget(SettingsCategoryWidget): def __init__(self, *args, **kwargs): super(ProjectWidget, self).__init__(*args, **kwargs) + def set_edit_mode(self, enabled): + super(ProjectWidget, self).set_edit_mode(enabled) + self.project_list_widget.set_edit_mode(enabled) + def _check_last_saved_info(self): if self.is_modifying_defaults: return True diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 722717df89..183b5b7b40 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -1009,6 +1009,7 @@ class ProjectListWidget(QtWidgets.QWidget): self._entity = None self.current_project = None + self._edit_mode = True super(ProjectListWidget, self).__init__(parent) self.setObjectName("ProjectListWidget") @@ -1061,6 +1062,10 @@ class ProjectListWidget(QtWidgets.QWidget): self.project_model = project_model self.inactive_chk = inactive_chk + def set_edit_mode(self, enabled): + if self._edit_mode is not enabled: + self._edit_mode = enabled + def set_entity(self, entity): self._entity = entity @@ -1112,7 +1117,7 @@ class ProjectListWidget(QtWidgets.QWidget): save_changes = False change_project = False - if self.validate_context_change(): + if not self._edit_mode or self.validate_context_change(): change_project = True else: From 040ba43d97f64dc0c8da4959539f8fc0f1137990 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Nov 2022 16:42:45 +0100 Subject: [PATCH 154/160] added title to unsaved changes dialog --- openpype/tools/settings/settings/widgets.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 183b5b7b40..b8ad21e7e4 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -646,6 +646,9 @@ class UnsavedChangesDialog(QtWidgets.QDialog): def __init__(self, parent=None): super(UnsavedChangesDialog, self).__init__(parent) + + self.setWindowTitle("Unsaved changes") + message_label = QtWidgets.QLabel(self.message) btns_widget = QtWidgets.QWidget(self) From 4874f5f79bb0b209302a48e8998ca6aa4caef376 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Nov 2022 17:58:10 +0100 Subject: [PATCH 155/160] OP-4371 - remove families from PreIntegrateThumbnail It should handle all instance which has thumbnail representations, not only specifically for image and render. There might be many more. --- .../plugins/publish/preintegrate_thumbnail_representation.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/plugins/publish/preintegrate_thumbnail_representation.py b/openpype/plugins/publish/preintegrate_thumbnail_representation.py index f9e23223e6..b88ccee9dc 100644 --- a/openpype/plugins/publish/preintegrate_thumbnail_representation.py +++ b/openpype/plugins/publish/preintegrate_thumbnail_representation.py @@ -21,9 +21,8 @@ class PreIntegrateThumbnails(pyblish.api.InstancePlugin): label = "Override Integrate Thumbnail Representations" order = pyblish.api.IntegratorOrder - 0.1 - families = ["review"] - integrate_profiles = {} + integrate_profiles = [] def process(self, instance): repres = instance.data.get("representations") From fa6a80bcef9cb225d2993e979e8e353ef2adffa9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Nov 2022 18:04:29 +0100 Subject: [PATCH 156/160] OP-4371 - remove obsolete ExtractThumbnail Functionality will be replaced by ExtractThumbnailFromSource which handles resolution. Reviewable instance must contain thumbnailSource field pointing to source file for Thumbnail. --- .../publish/collect_published_files.py | 5 +- .../plugins/publish/extract_thumbnail.py | 137 ------------------ 2 files changed, 4 insertions(+), 138 deletions(-) delete mode 100644 openpype/hosts/webpublisher/plugins/publish/extract_thumbnail.py diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index dd4646f356..3aa941af42 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -149,10 +149,13 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): self.log.warning("Unable to count frames " "duration {}".format(no_of_frames)) - # raise ValueError("STOP") instance.data["handleStart"] = asset_doc["data"]["handleStart"] instance.data["handleEnd"] = asset_doc["data"]["handleEnd"] + if "review" in tags: + instance.data["thumbnailSource"] = \ + instance.data["representations"][0] + instances.append(instance) self.log.info("instance.data:: {}".format(instance.data)) diff --git a/openpype/hosts/webpublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/webpublisher/plugins/publish/extract_thumbnail.py deleted file mode 100644 index a56521891b..0000000000 --- a/openpype/hosts/webpublisher/plugins/publish/extract_thumbnail.py +++ /dev/null @@ -1,137 +0,0 @@ -import os -import shutil - -import pyblish.api -from openpype.lib import ( - get_ffmpeg_tool_path, - - run_subprocess, - - get_transcode_temp_directory, - convert_input_paths_for_ffmpeg, - should_convert_for_ffmpeg -) - - -class ExtractThumbnail(pyblish.api.InstancePlugin): - """Create jpg thumbnail from input using ffmpeg.""" - - label = "Extract Thumbnail" - order = pyblish.api.ExtractorOrder - families = [ - "render", - "image" - ] - hosts = ["webpublisher"] - targets = ["filespublish"] - - def process(self, instance): - self.log.info("subset {}".format(instance.data['subset'])) - - filtered_repres = self._get_filtered_repres(instance) - for repre in filtered_repres: - repre_files = repre["files"] - if not isinstance(repre_files, (list, tuple)): - input_file = repre_files - else: - file_index = int(float(len(repre_files)) * 0.5) - input_file = repre_files[file_index] - - stagingdir = os.path.normpath(repre["stagingDir"]) - - full_input_path = os.path.join(stagingdir, input_file) - self.log.info("Input filepath: {}".format(full_input_path)) - - do_convert = should_convert_for_ffmpeg(full_input_path) - # If result is None the requirement of conversion can't be - # determined - if do_convert is None: - self.log.info(( - "Can't determine if representation requires conversion." - " Skipped." - )) - continue - - # Do conversion if needed - # - change staging dir of source representation - # - must be set back after output definitions processing - convert_dir = None - if do_convert: - convert_dir = get_transcode_temp_directory() - filename = os.path.basename(full_input_path) - convert_input_paths_for_ffmpeg( - [full_input_path], - convert_dir, - self.log - ) - full_input_path = os.path.join(convert_dir, filename) - - filename = os.path.splitext(input_file)[0] - while filename.endswith("."): - filename = filename[:-1] - thumbnail_filename = filename + "_thumbnail.jpg" - full_output_path = os.path.join(stagingdir, thumbnail_filename) - - self.log.info("output {}".format(full_output_path)) - - ffmpeg_args = [ - get_ffmpeg_tool_path("ffmpeg"), - "-y", - "-i", full_input_path, - "-vframes", "1", - full_output_path - ] - - # run subprocess - self.log.debug("{}".format(" ".join(ffmpeg_args))) - try: # temporary until oiiotool is supported cross platform - run_subprocess( - ffmpeg_args, logger=self.log - ) - except RuntimeError as exp: - if "Compression" in str(exp): - self.log.debug( - "Unsupported compression on input files. Skipping!!!" - ) - return - self.log.warning("Conversion crashed", exc_info=True) - raise - - new_repre = { - "name": "thumbnail", - "ext": "jpg", - "files": thumbnail_filename, - "stagingDir": stagingdir, - "thumbnail": True, - "tags": ["thumbnail"] - } - - # adding representation - self.log.debug("Adding: {}".format(new_repre)) - instance.data["representations"].append(new_repre) - - # Cleanup temp folder - if convert_dir is not None and os.path.exists(convert_dir): - shutil.rmtree(convert_dir) - - def _get_filtered_repres(self, instance): - filtered_repres = [] - repres = instance.data.get("representations") or [] - for repre in repres: - self.log.debug(repre) - tags = repre.get("tags") or [] - # Skip instance if already has thumbnail representation - if "thumbnail" in tags: - return [] - - if "review" not in tags: - continue - - if not repre.get("files"): - self.log.info(( - "Representation \"{}\" don't have files. Skipping" - ).format(repre["name"])) - continue - - filtered_repres.append(repre) - return filtered_repres From 34f4ee5ec79facfddd4828b1f0316c3c2a206155 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Nov 2022 18:29:51 +0100 Subject: [PATCH 157/160] OP-4371 - fix - provide full path to first file as a source for Thumbnail --- .../plugins/publish/collect_published_files.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 3aa941af42..2bf097de41 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -83,8 +83,9 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): self.log.info("task_data:: {}".format(task_data)) is_sequence = len(task_data["files"]) > 1 + first_file = task_data["files"][0] - _, extension = os.path.splitext(task_data["files"][0]) + _, extension = os.path.splitext(first_file) family, families, tags = self._get_family( self.task_type_to_family, task_type, @@ -153,8 +154,8 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): instance.data["handleEnd"] = asset_doc["data"]["handleEnd"] if "review" in tags: - instance.data["thumbnailSource"] = \ - instance.data["representations"][0] + first_file_path = os.path.join(task_dir, first_file) + instance.data["thumbnailSource"] = first_file_path instances.append(instance) self.log.info("instance.data:: {}".format(instance.data)) From a57a875b071f28b0bb0ddc234a7b95516dd41a0d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 4 Nov 2022 16:00:39 +0100 Subject: [PATCH 158/160] fix cache of asset docs --- openpype/tools/publisher/control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 10734a69f4..615f3eb8d9 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -90,9 +90,9 @@ class AssetDocsCache: return project_name = self._controller.project_name - asset_docs = get_assets( + asset_docs = list(get_assets( project_name, fields=self.projection.keys() - ) + )) asset_docs_by_name = {} task_names_by_asset_name = {} for asset_doc in asset_docs: From 4e03b94e877cf0b0c39e4a4156997c5c9c22f781 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 7 Nov 2022 21:50:02 +0100 Subject: [PATCH 159/160] hiero: adding better ranges --- openpype/hosts/hiero/api/plugin.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index ea8a9e836a..5ec1c78aaa 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -170,7 +170,10 @@ class CreatorWidget(QtWidgets.QDialog): for func, val in kwargs.items(): if getattr(item, func): func_attr = getattr(item, func) - func_attr(val) + if isinstance(val, tuple): + func_attr(*val) + else: + func_attr(val) # add to layout layout.addRow(label, item) @@ -273,8 +276,8 @@ class CreatorWidget(QtWidgets.QDialog): elif v["type"] == "QSpinBox": data[k]["value"] = self.create_row( content_layout, "QSpinBox", v["label"], - setValue=v["value"], setMinimum=0, - setMaximum=100000, setToolTip=tool_tip) + setRange=(1, 9999999), setValue=v["value"], + setToolTip=tool_tip) return data From 4c7733e6a74840b4a88318fa93de39fe82d227f6 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 9 Nov 2022 03:50:08 +0000 Subject: [PATCH 160/160] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 46bb4b1cd0..81b2925fb5 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.7-nightly.2" +__version__ = "3.14.7-nightly.3"