From e64d00e39c5c1fe64445e87b374d856d230fba91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Jul 2022 03:59:40 +0000 Subject: [PATCH 001/371] Bump terser from 5.10.0 to 5.14.2 in /website Bumps [terser](https://github.com/terser/terser) from 5.10.0 to 5.14.2. - [Release notes](https://github.com/terser/terser/releases) - [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md) - [Commits](https://github.com/terser/terser/commits) --- updated-dependencies: - dependency-name: terser dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/yarn.lock | 64 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index 04b9dd658b..38812dc6cd 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -1543,15 +1543,37 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/resolve-uri@^3.0.3": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c" - integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew== + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" "@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.11" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" - integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== "@jridgewell/trace-mapping@^0.3.0": version "0.3.4" @@ -1561,6 +1583,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.14" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" + integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@mdx-js/mdx@1.6.22", "@mdx-js/mdx@^1.6.21": version "1.6.22" resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba" @@ -2140,10 +2170,10 @@ acorn@^6.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== -acorn@^8.0.4, acorn@^8.4.1: - version "8.7.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" - integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== +acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== address@^1.0.1, address@^1.1.2: version "1.1.2" @@ -6838,11 +6868,6 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@~0.7.2: - version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - sourcemap-codec@^1.4.4: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" @@ -7048,12 +7073,13 @@ terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.4: terser "^5.7.2" terser@^5.10.0, terser@^5.7.2: - version "5.10.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.10.0.tgz#b86390809c0389105eb0a0b62397563096ddafcc" - integrity sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA== + version "5.14.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10" + integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA== dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" commander "^2.20.0" - source-map "~0.7.2" source-map-support "~0.5.20" text-table@^0.2.0: From 797d1ea59da616f84083e21b9b790c056f8f8c29 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 7 Sep 2022 13:47:02 +0200 Subject: [PATCH 002/371] 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 003/371] 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 004/371] 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 005/371] 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 006/371] 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 f2b6e954a1ddb4662835f8d786e34e70100645fc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 14 Sep 2022 00:16:06 +0200 Subject: [PATCH 007/371] Avoid name conflict where `group_name != group_node` due to maya auto renaming new node --- openpype/hosts/maya/plugins/load/load_yeti_cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_yeti_cache.py b/openpype/hosts/maya/plugins/load/load_yeti_cache.py index 8435ba2493..abc0e6003c 100644 --- a/openpype/hosts/maya/plugins/load/load_yeti_cache.py +++ b/openpype/hosts/maya/plugins/load/load_yeti_cache.py @@ -73,8 +73,8 @@ class YetiCacheLoader(load.LoaderPlugin): c = colors.get(family) if c is not None: - cmds.setAttr(group_name + ".useOutlinerColor", 1) - cmds.setAttr(group_name + ".outlinerColor", + cmds.setAttr(group_node + ".useOutlinerColor", 1) + cmds.setAttr(group_node + ".outlinerColor", (float(c[0])/255), (float(c[1])/255), (float(c[2])/255) From b54333086be343fc1524861f69bdd050e81caa8a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 21 Oct 2022 12:41:13 +0200 Subject: [PATCH 008/371] :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 009/371] :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 b6a2be53d4f1e7d2caf827ad4d6b45e366e9b1b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 26 Oct 2022 17:40:35 +0200 Subject: [PATCH 010/371] 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 011/371] 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 012/371] 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 013/371] 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 014/371] 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 cc4d158c785d8a413076830aa556445d9292b234 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 27 Oct 2022 15:12:51 +0200 Subject: [PATCH 015/371] 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 016/371] 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 017/371] 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 018/371] 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 019/371] :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 020/371] :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 021/371] 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 022/371] 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 023/371] 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 024/371] 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 025/371] 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 026/371] 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 027/371] 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 028/371] 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 029/371] 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 030/371] 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 031/371] 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 032/371] 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 033/371] 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 034/371] 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 035/371] 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 036/371] 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 037/371] 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 038/371] 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 039/371] 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 040/371] 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 041/371] 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 042/371] 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 043/371] 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 044/371] 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 045/371] 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 046/371] 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 047/371] 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 048/371] 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 049/371] 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 050/371] :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 051/371] 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 052/371] 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 053/371] 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 054/371] 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 055/371] 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 056/371] 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 057/371] 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 058/371] 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 059/371] 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 060/371] 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 57b81b4b5b5cd4ab98bfb9d73a8a69ba208bb061 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 28 Oct 2022 14:41:58 +0200 Subject: [PATCH 061/371] hiero: loading effects --- .../hosts/hiero/plugins/load/load_effects.py | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 openpype/hosts/hiero/plugins/load/load_effects.py diff --git a/openpype/hosts/hiero/plugins/load/load_effects.py b/openpype/hosts/hiero/plugins/load/load_effects.py new file mode 100644 index 0000000000..40f8d66d0c --- /dev/null +++ b/openpype/hosts/hiero/plugins/load/load_effects.py @@ -0,0 +1,259 @@ +import json +from collections import OrderedDict +from pprint import pprint +import six + +from openpype.pipeline import ( + AVALON_CONTAINER_ID, + load +) +from openpype.hosts.hiero import api as phiero +from openpype.hosts.hiero.api import tags + + +class LoadEffects(load.LoaderPlugin): + """Loading colorspace soft effect exported from nukestudio""" + + representations = ["effectJson"] + families = ["effect"] + + label = "Load Effects" + order = 0 + icon = "cc" + color = "white" + ignore_attr = ["useLifetime"] + + def load(self, context, name, namespace, data): + """ + Loading function to get the soft effects to particular read node + + Arguments: + context (dict): context of version + name (str): name of the version + namespace (str): asset name + data (dict): compulsory attribute > not used + + Returns: + nuke node: containerised nuke node object + """ + active_sequence = phiero.get_current_sequence() + active_track = phiero.get_current_track( + active_sequence, "LoadedEffects") + + # get main variables + version = context['version'] + version_data = version.get("data", {}) + vname = version.get("name", None) + namespace = namespace or context['asset']['name'] + object_name = "{}_{}".format(name, namespace) + + data_imprint = { + "source": version_data["source"], + "version": vname, + "author": version_data["author"], + } + + # getting file path + file = self.fname.replace("\\", "/") + + # getting data from json file with unicode conversion + with open(file, "r") as f: + json_f = {self.byteify(key): self.byteify(value) + for key, value in json.load(f).items()} + + # get correct order of nodes by positions on track and subtrack + nodes_order = self.reorder_nodes(json_f) + + used_subtracks = { + stitem.name(): stitem + for stitem in phiero.flatten(active_track.subTrackItems()) + } + + for ef_name, ef_val in nodes_order.items(): + pprint("_" * 100) + pprint(ef_name) + pprint(ef_val) + new_name = "{}_loaded".format(ef_name) + if new_name not in used_subtracks: + effect_track_item = active_track.createEffect( + effectType=ef_val["class"], + timelineIn=ef_val["timelineIn"], + timelineOut=ef_val["timelineOut"] + ) + effect_track_item.setName(new_name) + node = effect_track_item.node() + for knob_name, knob_value in ef_val["node"].items(): + if ( + not knob_value + or knob_name == "name" + ): + continue + node[knob_name].setValue(knob_value) + + self.containerise( + active_track, + name=name, + namespace=namespace, + object_name=object_name, + context=context, + loader=self.__class__.__name__, + data=data_imprint) + return + + def update(self, container, representation): + """Update the Loader's path + + Nuke automatically tries to reset some variables when changing + the loader's path to a new file. These automatic changes are to its + inputs: + + """ + pass + + def reorder_nodes(self, data): + new_order = OrderedDict() + trackNums = [v["trackIndex"] for k, v in data.items() + if isinstance(v, dict)] + subTrackNums = [v["subTrackIndex"] for k, v in data.items() + if isinstance(v, dict)] + + for trackIndex in range( + min(trackNums), max(trackNums) + 1): + for subTrackIndex in range( + min(subTrackNums), max(subTrackNums) + 1): + item = self.get_item(data, trackIndex, subTrackIndex) + if item is not {}: + new_order.update(item) + return new_order + + def get_item(self, data, trackIndex, subTrackIndex): + return {key: val for key, val in data.items() + if isinstance(val, dict) + if subTrackIndex == val["subTrackIndex"] + if trackIndex == val["trackIndex"]} + + def byteify(self, input): + """ + Converts unicode strings to strings + It goes through all dictionary + + Arguments: + input (dict/str): input + + Returns: + dict: with fixed values and keys + + """ + + if isinstance(input, dict): + return {self.byteify(key): self.byteify(value) + for key, value in input.items()} + elif isinstance(input, list): + return [self.byteify(element) for element in input] + elif isinstance(input, six.text_type): + return str(input) + else: + return input + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + pass + + def containerise( + self, + track, + name, + namespace, + object_name, + context, + loader=None, + data=None + ): + """Bundle Hiero's object into an assembly and imprint it with metadata + + Containerisation enables a tracking of version, author and origin + for loaded assets. + + Arguments: + track_item (hiero.core.TrackItem): object to imprint as container + name (str): Name of resulting assembly + namespace (str): Namespace under which to host container + context (dict): Asset information + loader (str, optional): Name of node used to produce this container. + + Returns: + track_item (hiero.core.TrackItem): containerised object + + """ + + data_imprint = { + object_name: { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": str(name), + "namespace": str(namespace), + "loader": str(loader), + "representation": str(context["representation"]["_id"]), + } + } + + if data: + for k, v in data.items(): + data_imprint[object_name].update({k: v}) + + self.log.debug("_ data_imprint: {}".format(data_imprint)) + self.set_track_openpype_tag(track, data_imprint) + + def set_track_openpype_tag(self, track, data=None): + """ + Set pype track item tag to input track_item. + + Attributes: + trackItem (hiero.core.TrackItem): hiero object + + Returns: + hiero.core.Tag + """ + data = data or {} + + # basic Tag's attribute + tag_data = { + "editable": "0", + "note": "OpenPype data container", + "icon": "openpype_icon.png", + "metadata": dict(data.items()) + } + # get available pype tag if any + _tag = self.get_track_openpype_tag(track) + + if _tag: + # it not tag then create one + tag = tags.update_tag(_tag, tag_data) + else: + # if pype tag available then update with input data + tag = tags.create_tag(phiero.pype_tag_name, tag_data) + # add it to the input track item + track.addTag(tag) + + return tag + + def get_track_openpype_tag(self, track): + """ + Get pype track item tag created by creator or loader plugin. + + Attributes: + trackItem (hiero.core.TrackItem): hiero object + + Returns: + hiero.core.Tag: hierarchy, orig clip attributes + """ + # get all tags from track item + _tags = track.tags() + if not _tags: + return None + for tag in _tags: + # return only correct tag defined by global name + if tag.name() == phiero.pype_tag_name: + return tag 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 062/371] 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 063/371] 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 064/371] [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 065/371] [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 066/371] [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 b04fc48fbc475f671c0876c6d05cfca79c6d95c0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 28 Oct 2022 16:49:38 +0200 Subject: [PATCH 067/371] hiero: fix - skip audio in collect effects --- openpype/hosts/hiero/plugins/publish/collect_clip_effects.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py index 8d2ed9a9c2..9489b1c4fb 100644 --- a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py +++ b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py @@ -16,6 +16,9 @@ class CollectClipEffects(pyblish.api.InstancePlugin): review_track_index = instance.context.data.get("reviewTrackIndex") item = instance.data["item"] + if "audio" in instance.data["family"]: + return + # frame range self.handle_start = instance.data["handleStart"] self.handle_end = instance.data["handleEnd"] From 21a3d2067e1732a14c3273a8ba6c2429ac8f7a19 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 28 Oct 2022 16:50:30 +0200 Subject: [PATCH 068/371] hiero: load effects update - adding order - adding clip in out definition --- .../hosts/hiero/plugins/load/load_effects.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/hiero/plugins/load/load_effects.py b/openpype/hosts/hiero/plugins/load/load_effects.py index 40f8d66d0c..3158f29d93 100644 --- a/openpype/hosts/hiero/plugins/load/load_effects.py +++ b/openpype/hosts/hiero/plugins/load/load_effects.py @@ -41,11 +41,13 @@ class LoadEffects(load.LoaderPlugin): active_sequence, "LoadedEffects") # get main variables - version = context['version'] + version = context["version"] version_data = version.get("data", {}) vname = version.get("name", None) - namespace = namespace or context['asset']['name'] + namespace = namespace or context["asset"]["name"] object_name = "{}_{}".format(name, namespace) + clip_in = context["asset"]["data"]["clipIn"] + clip_out = context["asset"]["data"]["clipOut"] data_imprint = { "source": version_data["source"], @@ -69,7 +71,8 @@ class LoadEffects(load.LoaderPlugin): for stitem in phiero.flatten(active_track.subTrackItems()) } - for ef_name, ef_val in nodes_order.items(): + loaded = False + for index_order, (ef_name, ef_val) in enumerate(nodes_order.items()): pprint("_" * 100) pprint(ef_name) pprint(ef_val) @@ -77,8 +80,10 @@ class LoadEffects(load.LoaderPlugin): if new_name not in used_subtracks: effect_track_item = active_track.createEffect( effectType=ef_val["class"], - timelineIn=ef_val["timelineIn"], - timelineOut=ef_val["timelineOut"] + timelineIn=clip_in, + timelineOut=clip_out, + subTrackIndex=index_order + ) effect_track_item.setName(new_name) node = effect_track_item.node() @@ -90,6 +95,12 @@ class LoadEffects(load.LoaderPlugin): continue node[knob_name].setValue(knob_value) + # make sure containerisation will happen + loaded = True + + if not loaded: + return + self.containerise( active_track, name=name, @@ -98,7 +109,6 @@ class LoadEffects(load.LoaderPlugin): context=context, loader=self.__class__.__name__, data=data_imprint) - return def update(self, container, representation): """Update the Loader's path From 6ecbf122e46d08e0f04879b74be6c1ef7e909a66 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 29 Oct 2022 03:47:38 +0000 Subject: [PATCH 069/371] [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 49ebb5aa0118a8535250743400789efdf952ba90 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 31 Oct 2022 11:05:19 +0100 Subject: [PATCH 070/371] hiero: abstraction for effect loader tag operations --- openpype/hosts/hiero/api/lib.py | 62 +++++++++++++++++-- openpype/hosts/hiero/api/pipeline.py | 18 ++++-- .../hosts/hiero/plugins/load/load_effects.py | 52 ---------------- 3 files changed, 70 insertions(+), 62 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index e5d35945af..9e626270f8 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -321,13 +321,67 @@ def get_track_item_pype_tag(track_item): return tag -def set_track_item_pype_tag(track_item, data=None): +def set_track_openpype_tag(track, data=None): """ - Set pype track item tag to input track_item. + Set openpype track tag to input track object. + + Attributes: + track (hiero.core.VideoTrack): hiero object + + Returns: + hiero.core.Tag + """ + data = data or {} + + # basic Tag's attribute + tag_data = { + "editable": "0", + "note": "OpenPype data container", + "icon": "openpype_icon.png", + "metadata": dict(data.items()) + } + # get available pype tag if any + _tag = get_track_openpype_tag(track) + + if _tag: + # it not tag then create one + tag = tags.update_tag(_tag, tag_data) + else: + # if pype tag available then update with input data + tag = tags.create_tag(self.pype_tag_name, tag_data) + # add it to the input track item + track.addTag(tag) + + return tag + + +def get_track_openpype_tag(track): + """ + Get pype track item tag created by creator or loader plugin. Attributes: trackItem (hiero.core.TrackItem): hiero object + Returns: + hiero.core.Tag: hierarchy, orig clip attributes + """ + # get all tags from track item + _tags = track.tags() + if not _tags: + return None + for tag in _tags: + # return only correct tag defined by global name + if tag.name() == self.pype_tag_name: + return tag + + +def set_trackitem_openpype_tag(track_item, data=None): + """ + Set openpype track tag to input track object. + + Attributes: + track (hiero.core.VideoTrack): hiero object + Returns: hiero.core.Tag """ @@ -1083,10 +1137,10 @@ def check_inventory_versions(track_items=None): project_name = legacy_io.active_project() filter_result = filter_containers(containers, project_name) for container in filter_result.latest: - set_track_color(container["_track_item"], clip_color) + set_track_color(container["_item"], clip_color) for container in filter_result.outdated: - set_track_color(container["_track_item"], clip_color_last) + set_track_color(container["_item"], clip_color_last) def selection_changed_timeline(event): diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index ea61dc4785..1b78159e04 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -17,6 +17,7 @@ from openpype.pipeline import ( ) from openpype.tools.utils import host_tools from . import lib, menu, events +import hiero log = Logger.get_logger(__name__) @@ -131,11 +132,12 @@ def ls(): yield container -def parse_container(track_item, validate=True): +def parse_container(item, validate=True): """Return container data from track_item's pype tag. Args: - track_item (hiero.core.TrackItem): A containerised track item. + item (hiero.core.TrackItem or hiero.core.VideoTrack): + A containerised track item. validate (bool)[optional]: validating with avalon scheme Returns: @@ -143,7 +145,11 @@ def parse_container(track_item, validate=True): """ # convert tag metadata to normal keys names - data = lib.get_track_item_pype_data(track_item) + if type(item) == hiero.core.VideoTrack: + data = lib.set_track_openpype_data(item) + else: + data = lib.set_track_item_pype_data(item) + if ( not data or data.get("id") != "pyblish.avalon.container" @@ -160,15 +166,15 @@ def parse_container(track_item, validate=True): required = ['schema', 'id', 'name', 'namespace', 'loader', 'representation'] - if not all(key in data for key in required): + if any(key not in data for key in required): return container = {key: data[key] for key in required} - container["objectName"] = track_item.name() + container["objectName"] = item.name() # Store reference to the node object - container["_track_item"] = track_item + container["_item"] = item return container diff --git a/openpype/hosts/hiero/plugins/load/load_effects.py b/openpype/hosts/hiero/plugins/load/load_effects.py index 3158f29d93..947655b4c8 100644 --- a/openpype/hosts/hiero/plugins/load/load_effects.py +++ b/openpype/hosts/hiero/plugins/load/load_effects.py @@ -215,55 +215,3 @@ class LoadEffects(load.LoaderPlugin): self.log.debug("_ data_imprint: {}".format(data_imprint)) self.set_track_openpype_tag(track, data_imprint) - - def set_track_openpype_tag(self, track, data=None): - """ - Set pype track item tag to input track_item. - - Attributes: - trackItem (hiero.core.TrackItem): hiero object - - Returns: - hiero.core.Tag - """ - data = data or {} - - # basic Tag's attribute - tag_data = { - "editable": "0", - "note": "OpenPype data container", - "icon": "openpype_icon.png", - "metadata": dict(data.items()) - } - # get available pype tag if any - _tag = self.get_track_openpype_tag(track) - - if _tag: - # it not tag then create one - tag = tags.update_tag(_tag, tag_data) - else: - # if pype tag available then update with input data - tag = tags.create_tag(phiero.pype_tag_name, tag_data) - # add it to the input track item - track.addTag(tag) - - return tag - - def get_track_openpype_tag(self, track): - """ - Get pype track item tag created by creator or loader plugin. - - Attributes: - trackItem (hiero.core.TrackItem): hiero object - - Returns: - hiero.core.Tag: hierarchy, orig clip attributes - """ - # get all tags from track item - _tags = track.tags() - if not _tags: - return None - for tag in _tags: - # return only correct tag defined by global name - if tag.name() == phiero.pype_tag_name: - return tag From dcf4688e1c8802510e04ac95f74c0968500a8c52 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 31 Oct 2022 11:16:47 +0100 Subject: [PATCH 071/371] hiero: renaming functions, with backward compatibility --- openpype/hosts/hiero/api/__init__.py | 12 ++-- openpype/hosts/hiero/api/lib.py | 69 +++++++++++-------- openpype/hosts/hiero/api/pipeline.py | 10 +-- .../plugins/publish/precollect_instances.py | 2 +- 4 files changed, 54 insertions(+), 39 deletions(-) diff --git a/openpype/hosts/hiero/api/__init__.py b/openpype/hosts/hiero/api/__init__.py index 781f846bbe..d0fb24b654 100644 --- a/openpype/hosts/hiero/api/__init__.py +++ b/openpype/hosts/hiero/api/__init__.py @@ -30,9 +30,9 @@ from .lib import ( get_timeline_selection, get_current_track, get_track_item_tags, - get_track_item_pype_tag, - set_track_item_pype_tag, - get_track_item_pype_data, + get_trackitem_openpype_tag, + set_trackitem_openpype_tag, + get_trackitem_openpype_data, set_publish_attribute, get_publish_attribute, imprint, @@ -85,9 +85,9 @@ __all__ = [ "get_timeline_selection", "get_current_track", "get_track_item_tags", - "get_track_item_pype_tag", - "set_track_item_pype_tag", - "get_track_item_pype_data", + "get_trackitem_openpype_tag", + "set_trackitem_openpype_tag", + "get_trackitem_openpype_data", "set_publish_attribute", "get_publish_attribute", "imprint", diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 9e626270f8..b0da4ce7b3 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -301,26 +301,6 @@ def get_track_item_tags(track_item): return returning_tag_data -def get_track_item_pype_tag(track_item): - """ - Get pype track item tag created by creator or loader plugin. - - Attributes: - trackItem (hiero.core.TrackItem): hiero object - - Returns: - hiero.core.Tag: hierarchy, orig clip attributes - """ - # get all tags from track item - _tags = track_item.tags() - if not _tags: - return None - for tag in _tags: - # return only correct tag defined by global name - if tag.name() == self.pype_tag_name: - return tag - - def set_track_openpype_tag(track, data=None): """ Set openpype track tag to input track object. @@ -375,6 +355,41 @@ def get_track_openpype_tag(track): return tag +def get_track_item_pype_tag(track_item): + # backward compatibility alias + return get_trackitem_openpype_tag(track_item) + + +def set_track_item_pype_tag(track_item, data=None): + # backward compatibility alias + return set_trackitem_openpype_tag(track_item, data) + + +def get_track_item_pype_data(track_item): + # backward compatibility alias + return get_trackitem_openpype_data(track_item) + + +def get_trackitem_openpype_tag(track_item): + """ + Get pype track item tag created by creator or loader plugin. + + Attributes: + trackItem (hiero.core.TrackItem): hiero object + + Returns: + hiero.core.Tag: hierarchy, orig clip attributes + """ + # get all tags from track item + _tags = track_item.tags() + if not _tags: + return None + for tag in _tags: + # return only correct tag defined by global name + if tag.name() == self.pype_tag_name: + return tag + + def set_trackitem_openpype_tag(track_item, data=None): """ Set openpype track tag to input track object. @@ -395,7 +410,7 @@ def set_trackitem_openpype_tag(track_item, data=None): "metadata": dict(data.items()) } # get available pype tag if any - _tag = get_track_item_pype_tag(track_item) + _tag = get_trackitem_openpype_tag(track_item) if _tag: # it not tag then create one @@ -409,7 +424,7 @@ def set_trackitem_openpype_tag(track_item, data=None): return tag -def get_track_item_pype_data(track_item): +def get_trackitem_openpype_data(track_item): """ Get track item's pype tag data. @@ -421,7 +436,7 @@ def get_track_item_pype_data(track_item): """ data = {} # get pype data tag from track item - tag = get_track_item_pype_tag(track_item) + tag = get_trackitem_openpype_tag(track_item) if not tag: return None @@ -474,7 +489,7 @@ def imprint(track_item, data=None): """ data = data or {} - tag = set_track_item_pype_tag(track_item, data) + tag = set_trackitem_openpype_tag(track_item, data) # add publish attribute set_publish_attribute(tag, True) @@ -1084,7 +1099,7 @@ def sync_clip_name_to_data_asset(track_items_list): # get name and data ti_name = track_item.name() - data = get_track_item_pype_data(track_item) + data = get_trackitem_openpype_data(track_item) # ignore if no data on the clip or not publish instance if not data: @@ -1096,10 +1111,10 @@ def sync_clip_name_to_data_asset(track_items_list): if data["asset"] != ti_name: data["asset"] = ti_name # remove the original tag - tag = get_track_item_pype_tag(track_item) + tag = get_trackitem_openpype_tag(track_item) track_item.removeTag(tag) # create new tag with updated data - set_track_item_pype_tag(track_item, data) + set_trackitem_openpype_tag(track_item, data) print("asset was changed in clip: {}".format(ti_name)) diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index 1b78159e04..0c11f7072f 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -107,7 +107,7 @@ def containerise(track_item, data_imprint.update({k: v}) log.debug("_ data_imprint: {}".format(data_imprint)) - lib.set_track_item_pype_tag(track_item, data_imprint) + lib.set_trackitem_openpype_tag(track_item, data_imprint) return track_item @@ -192,7 +192,7 @@ def update_container(track_item, data=None): """ data = data or dict() - container = lib.get_track_item_pype_data(track_item) + container = lib.get_trackitem_openpype_data(track_item) for _key, _value in container.items(): try: @@ -201,7 +201,7 @@ def update_container(track_item, data=None): pass log.info("Updating container: `{}`".format(track_item.name())) - return bool(lib.set_track_item_pype_tag(track_item, container)) + return bool(lib.set_trackitem_openpype_tag(track_item, container)) def launch_workfiles_app(*args): @@ -278,11 +278,11 @@ def on_pyblish_instance_toggled(instance, old_value, new_value): instance, old_value, new_value)) from openpype.hosts.hiero.api import ( - get_track_item_pype_tag, + get_trackitem_openpype_tag, set_publish_attribute ) # Whether instances should be passthrough based on new value track_item = instance.data["item"] - tag = get_track_item_pype_tag(track_item) + tag = get_trackitem_openpype_tag(track_item) set_publish_attribute(tag, new_value) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 1fc4b1f696..bb02919b35 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -48,7 +48,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): self.log.debug("clip_name: {}".format(clip_name)) # get openpype tag data - tag_data = phiero.get_track_item_pype_data(track_item) + tag_data = phiero.get_trackitem_openpype_data(track_item) self.log.debug("__ tag_data: {}".format(pformat(tag_data))) if not tag_data: From a9ab5baac9903c5a307d737201b12e14ecdbbf85 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 31 Oct 2022 11:26:48 +0100 Subject: [PATCH 072/371] hiero: improving bckw compatibility after rename --- openpype/hosts/hiero/api/__init__.py | 7 +++ openpype/hosts/hiero/api/lib.py | 75 +++++++++++++++++++++------- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/hiero/api/__init__.py b/openpype/hosts/hiero/api/__init__.py index d0fb24b654..f457d791f5 100644 --- a/openpype/hosts/hiero/api/__init__.py +++ b/openpype/hosts/hiero/api/__init__.py @@ -30,6 +30,9 @@ from .lib import ( get_timeline_selection, get_current_track, get_track_item_tags, + get_track_item_pype_tag, + set_track_item_pype_tag, + get_track_item_pype_data, get_trackitem_openpype_tag, set_trackitem_openpype_tag, get_trackitem_openpype_data, @@ -99,6 +102,10 @@ __all__ = [ "apply_colorspace_project", "apply_colorspace_clips", "get_sequence_pattern_and_padding", + # depricated + "get_track_item_pype_tag", + "set_track_item_pype_tag", + "get_track_item_pype_data", # plugins "CreatorWidget", diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index b0da4ce7b3..f4b80aea4e 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -7,11 +7,13 @@ import os import re import sys import platform +import functools +import warnings import ast import shutil import hiero -from Qt import QtWidgets +from Qt import QtWidgets, QtCore, QtXml from openpype.client import get_project from openpype.settings import get_project_settings @@ -20,15 +22,51 @@ from openpype.pipeline.load import filter_containers from openpype.lib import Logger from . import tags -try: - from PySide.QtCore import QFile, QTextStream - from PySide.QtXml import QDomDocument -except ImportError: - from PySide2.QtCore import QFile, QTextStream - from PySide2.QtXml import QDomDocument -# from opentimelineio import opentime -# from pprint import pformat +class DeprecatedWarning(DeprecationWarning): + pass + + +def deprecated(new_destination): + """Mark functions as deprecated. + + It will result in a warning being emitted when the function is used. + """ + + func = None + if callable(new_destination): + func = new_destination + new_destination = None + + def _decorator(decorated_func): + if new_destination is None: + warning_message = ( + " Please check content of deprecated function to figure out" + " possible replacement." + ) + else: + warning_message = " Please replace your usage with '{}'.".format( + new_destination + ) + + @functools.wraps(decorated_func) + def wrapper(*args, **kwargs): + warnings.simplefilter("always", DeprecatedWarning) + warnings.warn( + ( + "Call to deprecated function '{}'" + "\nFunction was moved or removed.{}" + ).format(decorated_func.__name__, warning_message), + category=DeprecatedWarning, + stacklevel=4 + ) + return decorated_func(*args, **kwargs) + return wrapper + + if func is None: + return _decorator + return _decorator(func) + log = Logger.get_logger(__name__) @@ -355,16 +393,19 @@ def get_track_openpype_tag(track): return tag +@deprecated("openpype.hosts.hiero.api.lib.get_trackitem_openpype_tag") def get_track_item_pype_tag(track_item): # backward compatibility alias return get_trackitem_openpype_tag(track_item) +@deprecated("openpype.hosts.hiero.api.lib.set_trackitem_openpype_tag") def set_track_item_pype_tag(track_item, data=None): # backward compatibility alias return set_trackitem_openpype_tag(track_item, data) +@deprecated("openpype.hosts.hiero.api.lib.get_trackitem_openpype_data") def get_track_item_pype_data(track_item): # backward compatibility alias return get_trackitem_openpype_data(track_item) @@ -901,22 +942,22 @@ def set_selected_track_items(track_items_list, sequence=None): def _read_doc_from_path(path): - # reading QDomDocument from HROX path - hrox_file = QFile(path) - if not hrox_file.open(QFile.ReadOnly): + # reading QtXml.QDomDocument from HROX path + hrox_file = QtCore.QFile(path) + if not hrox_file.open(QtCore.QFile.ReadOnly): raise RuntimeError("Failed to open file for reading") - doc = QDomDocument() + doc = QtXml.QDomDocument() doc.setContent(hrox_file) hrox_file.close() return doc def _write_doc_to_path(doc, path): - # write QDomDocument to path as HROX - hrox_file = QFile(path) - if not hrox_file.open(QFile.WriteOnly): + # write QtXml.QDomDocument to path as HROX + hrox_file = QtCore.QFile(path) + if not hrox_file.open(QtCore.QFile.WriteOnly): raise RuntimeError("Failed to open file for writing") - stream = QTextStream(hrox_file) + stream = QtCore.QTextStream(hrox_file) doc.save(stream, 1) hrox_file.close() From 04d1016dfa71d5630e7b920371d3b4ea42e2fcff Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 31 Oct 2022 11:43:34 +0100 Subject: [PATCH 073/371] hiero: update api --- openpype/hosts/hiero/api/__init__.py | 6 ++++ openpype/hosts/hiero/api/lib.py | 51 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/openpype/hosts/hiero/api/__init__.py b/openpype/hosts/hiero/api/__init__.py index f457d791f5..1fa40c9f74 100644 --- a/openpype/hosts/hiero/api/__init__.py +++ b/openpype/hosts/hiero/api/__init__.py @@ -30,6 +30,9 @@ from .lib import ( get_timeline_selection, get_current_track, get_track_item_tags, + get_track_openpype_tag, + set_track_openpype_tag, + get_track_openpype_data, get_track_item_pype_tag, set_track_item_pype_tag, get_track_item_pype_data, @@ -88,6 +91,9 @@ __all__ = [ "get_timeline_selection", "get_current_track", "get_track_item_tags", + "get_track_openpype_tag", + "set_track_openpype_tag", + "get_track_openpype_data", "get_trackitem_openpype_tag", "set_trackitem_openpype_tag", "get_trackitem_openpype_data", diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index f4b80aea4e..3c1d500e46 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -393,6 +393,57 @@ def get_track_openpype_tag(track): return tag +def get_track_openpype_data(track): + """ + Get track's openpype tag data. + + Attributes: + trackItem (hiero.core.VideoTrack): hiero object + + Returns: + dict: data found on pype tag + """ + return_data = {} + # get pype data tag from track item + tag = get_track_openpype_tag(track) + + if not tag: + return None + + # get tag metadata attribute + tag_data = deepcopy(dict(tag.metadata())) + + for obj_name, obj_data in tag_data.items(): + return_data[obj_name] = {} + + # convert tag metadata to normal keys names and values to correct types + for k, v in obj_data.items(): + + key = k.replace("tag.", "") + + try: + # capture exceptions which are related to strings only + if re.match(r"^[\d]+$", v): + value = int(v) + elif re.match(r"^True$", v): + value = True + elif re.match(r"^False$", v): + value = False + elif re.match(r"^None$", v): + value = None + elif re.match(r"^[\w\d_]+$", v): + value = v + else: + value = ast.literal_eval(v) + except (ValueError, SyntaxError) as msg: + log.warning(msg) + value = v + + return_data[obj_name][key] = value + + return return_data + + @deprecated("openpype.hosts.hiero.api.lib.get_trackitem_openpype_tag") def get_track_item_pype_tag(track_item): # backward compatibility alias From 66571cc8cded1b6329f839cd9425da2631531a67 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 31 Oct 2022 12:16:08 +0100 Subject: [PATCH 074/371] hiero: update parse_container and ls to new functionality accepting track containers --- openpype/hosts/hiero/api/pipeline.py | 85 +++++++++++++++++----------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index 0c11f7072f..1ce8e4e1c5 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -124,11 +124,20 @@ def ls(): """ # get all track items from current timeline - all_track_items = lib.get_track_items() + all_items = lib.get_track_items() - for track_item in all_track_items: - container = parse_container(track_item) - if container: + # append all video tracks + for track in lib.get_current_sequence(): + if type(track) != hiero.core.VideoTrack: + continue + all_items.append(track) + + for item in all_items: + container = parse_container(item) + if isinstance(container, list): + for _c in container: + yield _c + elif container: yield container @@ -144,39 +153,47 @@ def parse_container(item, validate=True): dict: The container schema data for input containerized track item. """ + def data_to_container(item, data): + if ( + not data + or data.get("id") != "pyblish.avalon.container" + ): + return + + if validate and data and data.get("schema"): + schema.validate(data) + + if not isinstance(data, dict): + return + + # If not all required data return the empty container + required = ['schema', 'id', 'name', + 'namespace', 'loader', 'representation'] + + if any(key not in data for key in required): + return + + container = {key: data[key] for key in required} + + container["objectName"] = item.name() + + # Store reference to the node object + container["_item"] = item + + return container + # convert tag metadata to normal keys names if type(item) == hiero.core.VideoTrack: - data = lib.set_track_openpype_data(item) + return_list = [] + _data = lib.get_track_openpype_data(item) + # convert the data to list and validate them + for _, obj_data in _data.items(): + cotnainer = data_to_container(item, obj_data) + return_list.append(cotnainer) + return return_list else: - data = lib.set_track_item_pype_data(item) - - if ( - not data - or data.get("id") != "pyblish.avalon.container" - ): - return - - if validate and data and data.get("schema"): - schema.validate(data) - - if not isinstance(data, dict): - return - - # If not all required data return the empty container - required = ['schema', 'id', 'name', - 'namespace', 'loader', 'representation'] - - if any(key not in data for key in required): - return - - container = {key: data[key] for key in required} - - container["objectName"] = item.name() - - # Store reference to the node object - container["_item"] = item - - return container + _data = lib.get_track_item_pype_data(item) + return data_to_container(item, _data) def update_container(track_item, data=None): From 25b61d3fdf657f38db35c741456d680cb1c24b59 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 31 Oct 2022 12:17:34 +0100 Subject: [PATCH 075/371] hiero: refactor plugin to new abstracted functionality --- openpype/hosts/hiero/plugins/load/load_effects.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/hiero/plugins/load/load_effects.py b/openpype/hosts/hiero/plugins/load/load_effects.py index 947655b4c8..fa78684838 100644 --- a/openpype/hosts/hiero/plugins/load/load_effects.py +++ b/openpype/hosts/hiero/plugins/load/load_effects.py @@ -8,7 +8,6 @@ from openpype.pipeline import ( load ) from openpype.hosts.hiero import api as phiero -from openpype.hosts.hiero.api import tags class LoadEffects(load.LoaderPlugin): @@ -53,6 +52,7 @@ class LoadEffects(load.LoaderPlugin): "source": version_data["source"], "version": vname, "author": version_data["author"], + "children_names": [] } # getting file path @@ -95,6 +95,8 @@ class LoadEffects(load.LoaderPlugin): continue node[knob_name].setValue(knob_value) + # register all loaded children + data_imprint["children_names"].append(new_name) # make sure containerisation will happen loaded = True @@ -187,11 +189,13 @@ class LoadEffects(load.LoaderPlugin): for loaded assets. Arguments: - track_item (hiero.core.TrackItem): object to imprint as container + track (hiero.core.VideoTrack): object to imprint as container name (str): Name of resulting assembly namespace (str): Namespace under which to host container + object_name (str): name of container context (dict): Asset information - loader (str, optional): Name of node used to produce this container. + loader (str, optional): Name of node used to produce this + container. Returns: track_item (hiero.core.TrackItem): containerised object @@ -214,4 +218,4 @@ class LoadEffects(load.LoaderPlugin): data_imprint[object_name].update({k: v}) self.log.debug("_ data_imprint: {}".format(data_imprint)) - self.set_track_openpype_tag(track, data_imprint) + phiero.set_track_openpype_tag(track, data_imprint) From 3d55d4d9554c5b844da7697e7f1efea6f3ffa303 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 31 Oct 2022 19:36:44 +0800 Subject: [PATCH 076/371] Alembic Loader as Arnold Standin --- .../maya/plugins/load/load_abc_to_standin.py | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 openpype/hosts/maya/plugins/load/load_abc_to_standin.py diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py new file mode 100644 index 0000000000..defed4bd73 --- /dev/null +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -0,0 +1,115 @@ +import os +import clique + +from openpype.pipeline import ( + load, + get_representation_path +) +from openpype.settings import get_project_settings + + +class AlembicStandinLoader(load.LoaderPlugin): + """Load Alembic as Arnold Standin""" + + families = ["model", "pointcache"] + representations = ["abc"] + + label = "Import Alembic as Standin" + order = -5 + icon = "code-fork" + color = "orange" + + def load(self, context, name, namespace, options): + + import maya.cmds as cmds + import pymel.core as pm + import mtoa.ui.arnoldmenu + from openpype.hosts.maya.api.pipeline import containerise + from openpype.hosts.maya.api.lib import unique_namespace + + version = context["version"] + version_data = version.get("data", {}) + + self.log.info("version_data: {}\n".format(version_data)) + + frameStart = version_data.get("frameStart", None) + + asset = context["asset"]["name"] + namespace = namespace or unique_namespace( + asset + "_", + prefix="_" if asset[0].isdigit() else "", + suffix="_", + ) + + #Root group + label = "{}:{}".format(namespace, name) + root = pm.group(name=label, empty=True) + + settings = get_project_settings(os.environ['AVALON_PROJECT']) + colors = settings["maya"]["load"]["colors"] + + c = colors.get('ass') + if c is not None: + cmds.setAttr(root + ".useOutlinerColor", 1) + cmds.setAttr(root + ".outlinerColor", + c[0], c[1], c[2]) + + transform_name = label + "_ABC" + + standinShape = pm.PyNode(mtoa.ui.arnoldmenu.createStandIn()) + standin = standinShape.getParent() + standin.rename(transform_name) + + pm.parent(standin, root) + + # Set the standin filepath + standinShape.dso.set(self.fname) + if frameStart is not None: + standinShape.useFrameExtension.set(1) + + nodes = [root, standin] + self[:] = nodes + + return containerise( + name=name, + namespace=namespace, + nodes=nodes, + context=context, + loader=self.__class__.__name__) + + def update(self, container, representation): + + import pymel.core as pm + + path = get_representation_path(representation) + + # Update the standin + standins = list() + members = pm.sets(container['objectName'], query=True) + for member in members: + shape = member.getShape() + if (shape and shape.type() == "aiStandIn"): + standins.append(shape) + + for standin in standins: + standin.dso.set(path) + standin.useFrameExtension.set(1) + + container = pm.PyNode(container["objectName"]) + container.representation.set(str(representation["_id"])) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + import maya.cmds as cmds + members = cmds.sets(container['objectName'], query=True) + cmds.lockNode(members, lock=False) + cmds.delete([container['objectName']] + members) + + # Clean up the namespace + try: + cmds.namespace(removeNamespace=container['namespace'], + deleteNamespaceContent=True) + except RuntimeError: + pass From 7f88049d2a38e46fb933cbf23859529d46976915 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 31 Oct 2022 19:56:10 +0800 Subject: [PATCH 077/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index defed4bd73..f39aa56650 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -1,5 +1,4 @@ import os -import clique from openpype.pipeline import ( load, @@ -41,7 +40,7 @@ class AlembicStandinLoader(load.LoaderPlugin): suffix="_", ) - #Root group + # Root group label = "{}:{}".format(namespace, name) root = pm.group(name=label, empty=True) From ac2f268575327c2e82d1cc9ca0a231caf54ea322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Mon, 31 Oct 2022 13:28:50 +0100 Subject: [PATCH 078/371] Feature: Auto download last published workfile as first workfile --- .../hooks/pre_copy_last_published_workfile.py | 124 ++++++++++++++++++ openpype/modules/sync_server/sync_server.py | 9 +- 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 openpype/hooks/pre_copy_last_published_workfile.py diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py new file mode 100644 index 0000000000..004f9d25e7 --- /dev/null +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -0,0 +1,124 @@ +import gc +import os +import shutil +from openpype.client.entities import ( + get_last_version_by_subset_id, + get_representations, + get_subsets, +) +from openpype.lib import PreLaunchHook +from openpype.modules.base import ModulesManager +from openpype.pipeline.load.utils import get_representation_path + + +class CopyLastPublishedWorkfile(PreLaunchHook): + """Copy last published workfile as first workfile. + + Prelaunch hook works only if last workfile leads to not existing file. + - That is possible only if it's first version. + """ + + # Before `AddLastWorkfileToLaunchArgs` + order = -1 + app_groups = ["blender", "photoshop", "tvpaint", "aftereffects"] + + def execute(self): + """Check if local workfile doesn't exist, else copy it. + + 1- Check if setting for this feature is enabled + 2- Check if workfile in work area doesn't exist + 3- Check if published workfile exists and is copied locally in publish + + Returns: + None: This is a void method. + """ + # TODO setting + self.log.info("Trying to fetch last published workfile...") + + last_workfile = self.data.get("last_workfile_path") + if os.path.exists(last_workfile): + self.log.debug( + "Last workfile exists. Skipping {} process.".format( + self.__class__.__name__ + ) + ) + return + + project_name = self.data["project_name"] + task_name = self.data["task_name"] + + project_doc = self.data.get("project_doc") + asset_doc = self.data.get("asset_doc") + anatomy = self.data.get("anatomy") + if project_doc and asset_doc: + # Get subset id + subset_id = next( + ( + subset["_id"] + for subset in get_subsets( + project_name, + asset_ids=[asset_doc["_id"]], + fields=["_id", "data.family"], + ) + if subset["data"]["family"] == "workfile" + ), + None, + ) + if not subset_id: + return + + # Get workfile representation + workfile_representation = next( + ( + representation + for representation in get_representations( + project_name, + version_ids=[ + get_last_version_by_subset_id( + project_name, subset_id, fields=["_id"] + )["_id"] + ], + ) + if representation["context"]["task"]["name"] == task_name + ), + None, + ) + + if workfile_representation: # TODO add setting + # Get sync server from Tray, which handles the asynchronous thread instance + sync_server = next( + ( + t["sync_server"] + for t in [ + obj + for obj in gc.get_objects() + if isinstance(obj, ModulesManager) + ] + if t["sync_server"].sync_server_thread + ), + None, + ) + + # Add site and reset timer + active_site = sync_server.get_active_site(project_name) + sync_server.add_site( + project_name, + workfile_representation["_id"], + active_site, + force=True, + ) + sync_server.reset_timer() + + # Wait for the download loop to end + sync_server.sync_server_thread.files_processed.wait() + + # Get paths + published_workfile_path = get_representation_path( + workfile_representation, root=anatomy.roots + ) + local_workfile_dir = os.path.dirname(last_workfile) + + # Copy file and substitute path + self.data["last_workfile_path"] = shutil.copy( + published_workfile_path, local_workfile_dir + ) diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index 8b11055e65..def9e6cfd8 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -236,7 +236,11 @@ class SyncServerThread(threading.Thread): """ def __init__(self, module): self.log = Logger.get_logger(self.__class__.__name__) - super(SyncServerThread, self).__init__() + + # Event to trigger files have been processed + self.files_processed = threading.Event() + + super(SyncServerThread, self).__init__(args=(self.files_processed,)) self.module = module self.loop = None self.is_running = False @@ -396,6 +400,8 @@ class SyncServerThread(threading.Thread): representation, site, error) + # Trigger files are processed + self.files_processed.set() duration = time.time() - start_time self.log.debug("One loop took {:.2f}s".format(duration)) @@ -454,6 +460,7 @@ class SyncServerThread(threading.Thread): async def run_timer(self, delay): """Wait for 'delay' seconds to start next loop""" + self.files_processed.clear() await asyncio.sleep(delay) def reset_timer(self): From 7647176173d3e4d28290650761370efeede53505 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 13:37:07 +0100 Subject: [PATCH 079/371] 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 080/371] 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 081/371] 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 b12bb8723040d3c76cff92f6fd1c7b1bef9a5549 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 31 Oct 2022 13:57:03 +0100 Subject: [PATCH 082/371] hiero: refactor update container function --- openpype/hosts/hiero/api/pipeline.py | 41 ++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index 1ce8e4e1c5..1e4158261c 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -196,29 +196,46 @@ def parse_container(item, validate=True): return data_to_container(item, _data) -def update_container(track_item, data=None): - """Update container data to input track_item's pype tag. +def update_container(item, data=None): + """Update container data to input track_item or track's + openpype tag. Args: - track_item (hiero.core.TrackItem): A containerised track item. + item (hiero.core.TrackItem or hiero.core.VideoTrack): + A containerised track item. data (dict)[optional]: dictionery with data to be updated Returns: bool: True if container was updated correctly """ - data = data or dict() + def update_container_data(container, data): + for key in container: + try: + container[key] = data[key] + except KeyError: + pass + return container - container = lib.get_trackitem_openpype_data(track_item) + data = data or {} - for _key, _value in container.items(): - try: - container[_key] = data[_key] - except KeyError: - pass + if type(item) == hiero.core.VideoTrack: + object_name = "{}_{}".format( + data["name"], data["namespace"]) + containers = lib.get_track_openpype_data(item) + for obj_name, container in containers.items(): + if object_name != obj_name: + continue + updated_container = update_container_data(container, data) + containers.update(updated_container) - log.info("Updating container: `{}`".format(track_item.name())) - return bool(lib.set_trackitem_openpype_tag(track_item, container)) + return bool(lib.set_track_openpype_tag(item, containers)) + else: + container = lib.get_trackitem_openpype_data(item) + updated_container = update_container_data(container, data) + + log.info("Updating container: `{}`".format(item.name())) + return bool(lib.set_trackitem_openpype_tag(item, updated_container)) def launch_workfiles_app(*args): From 4ab8fd1a822d6c2e9f60d3eb4933eee61e381208 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 31 Oct 2022 14:01:44 +0100 Subject: [PATCH 083/371] hiero: updating doc strings --- openpype/hosts/hiero/api/pipeline.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index 1e4158261c..e9e16ef5b1 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -220,13 +220,19 @@ def update_container(item, data=None): data = data or {} if type(item) == hiero.core.VideoTrack: + # form object data for test object_name = "{}_{}".format( data["name"], data["namespace"]) + + # get all available containers containers = lib.get_track_openpype_data(item) for obj_name, container in containers.items(): + # ignore all which are not the same object if object_name != obj_name: continue + # update data in container updated_container = update_container_data(container, data) + # merge updated container back to containers containers.update(updated_container) return bool(lib.set_track_openpype_tag(item, containers)) From 72e729f59f5276ecb752fc59d01921b7815656de Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 15:07:57 +0100 Subject: [PATCH 084/371] 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 085/371] 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 086/371] 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 087/371] 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 65e7c45e94ed1fb19dc512ce8ced91506dd2efec Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 31 Oct 2022 17:13:09 +0100 Subject: [PATCH 088/371] hiero: wip updating effect containers --- openpype/hosts/hiero/api/lib.py | 32 ++++--------------- openpype/hosts/hiero/api/pipeline.py | 12 ++++--- openpype/hosts/hiero/api/tags.py | 22 ++++++++----- .../hosts/hiero/plugins/load/load_effects.py | 1 + 4 files changed, 29 insertions(+), 38 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 3c1d500e46..d04a710df1 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -9,6 +9,7 @@ import sys import platform import functools import warnings +import json import ast import shutil import hiero @@ -414,32 +415,11 @@ def get_track_openpype_data(track): tag_data = deepcopy(dict(tag.metadata())) for obj_name, obj_data in tag_data.items(): - return_data[obj_name] = {} - - # convert tag metadata to normal keys names and values to correct types - for k, v in obj_data.items(): - - key = k.replace("tag.", "") - - try: - # capture exceptions which are related to strings only - if re.match(r"^[\d]+$", v): - value = int(v) - elif re.match(r"^True$", v): - value = True - elif re.match(r"^False$", v): - value = False - elif re.match(r"^None$", v): - value = None - elif re.match(r"^[\w\d_]+$", v): - value = v - else: - value = ast.literal_eval(v) - except (ValueError, SyntaxError) as msg: - log.warning(msg) - value = v - - return_data[obj_name][key] = value + obj_name = obj_name.replace("tag.", "") + print(obj_name) + if obj_name in ["applieswhole", "note", "label"]: + continue + return_data[obj_name] = json.loads(obj_data) return return_data diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index e9e16ef5b1..26c8ebe6d3 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -133,12 +133,13 @@ def ls(): all_items.append(track) for item in all_items: - container = parse_container(item) + container_data = parse_container(item) if isinstance(container, list): - for _c in container: + if isinstance(container_data, list): + for _c in container_data: yield _c - elif container: - yield container + elif container_data: + yield container_data def parse_container(item, validate=True): @@ -186,6 +187,9 @@ def parse_container(item, validate=True): if type(item) == hiero.core.VideoTrack: return_list = [] _data = lib.get_track_openpype_data(item) + log.info("_data: {}".format(_data)) + if not _data: + return # convert the data to list and validate them for _, obj_data in _data.items(): cotnainer = data_to_container(item, obj_data) diff --git a/openpype/hosts/hiero/api/tags.py b/openpype/hosts/hiero/api/tags.py index fac26da03a..918af3dc1f 100644 --- a/openpype/hosts/hiero/api/tags.py +++ b/openpype/hosts/hiero/api/tags.py @@ -1,3 +1,4 @@ +import json import re import os import hiero @@ -85,17 +86,22 @@ def update_tag(tag, data): # get metadata key from data data_mtd = data.get("metadata", {}) - # due to hiero bug we have to make sure keys which are not existent in - # data are cleared of value by `None` - for _mk in mtd.dict().keys(): - if _mk.replace("tag.", "") not in data_mtd.keys(): - mtd.setValue(_mk, str(None)) + # # due to hiero bug we have to make sure keys which are not existent in + # # data are cleared of value by `None` + # for _mk in mtd.dict().keys(): + # if _mk.replace("tag.", "") not in data_mtd.keys(): + # mtd.setValue(_mk, str(None)) # set all data metadata to tag metadata - for k, v in data_mtd.items(): + for _k, _v in data_mtd.items(): + value = str(_v) + if type(_v) == dict: + value = json.dumps(_v) + + # set the value mtd.setValue( - "tag.{}".format(str(k)), - str(v) + "tag.{}".format(str(_k)), + value ) # set note description of tag diff --git a/openpype/hosts/hiero/plugins/load/load_effects.py b/openpype/hosts/hiero/plugins/load/load_effects.py index fa78684838..16c9187ad9 100644 --- a/openpype/hosts/hiero/plugins/load/load_effects.py +++ b/openpype/hosts/hiero/plugins/load/load_effects.py @@ -52,6 +52,7 @@ class LoadEffects(load.LoaderPlugin): "source": version_data["source"], "version": vname, "author": version_data["author"], + "objectName": object_name, "children_names": [] } From 44f6d1c724ce5312dc42b044088f99870bf19494 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 18:10:44 +0100 Subject: [PATCH 089/371] 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 090/371] 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 091/371] 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 092/371] 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 093/371] 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 094/371] 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 095/371] 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 096/371] 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 097/371] 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 098/371] 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 2c4d37d1bfef0b83134bd00775b00815493366cc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 19:23:22 +0100 Subject: [PATCH 099/371] added create next overlay widget --- openpype/tools/publisher/widgets/__init__.py | 2 + openpype/tools/publisher/widgets/widgets.py | 201 +++++++++++++++++++ 2 files changed, 203 insertions(+) diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index a02c69d5e0..042985b007 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -8,6 +8,7 @@ from .widgets import ( ResetBtn, ValidateBtn, PublishBtn, + CreateNextPageOverlay, ) from .help_widget import ( HelpButton, @@ -28,6 +29,7 @@ __all__ = ( "ResetBtn", "ValidateBtn", "PublishBtn", + "CreateNextPageOverlay", "HelpButton", "HelpDialog", diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index d4c2623790..507ecedb0f 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1652,3 +1652,204 @@ class ThumbnailWidget(QtWidgets.QWidget): self.thumbnail_label = thumbnail_label self.default_pix = default_pix self.current_pix = None + + +class CreateNextPageOverlay(QtWidgets.QWidget): + max_value = 100.0 + clicked = QtCore.Signal() + + def __init__(self, parent): + super(CreateNextPageOverlay, self).__init__(parent) + + self._bg_color = QtGui.QColor(127, 127, 255) + self._arrow_color = QtGui.QColor(255, 255, 255) + + change_anim = QtCore.QVariantAnimation() + change_anim.setStartValue(0.0) + change_anim.setEndValue(self.max_value) + change_anim.setDuration(200) + change_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuad) + + change_anim.valueChanged.connect(self._on_anim) + + self._change_anim = change_anim + self._is_visible = None + self._anim_value = 0.0 + self._increasing = False + self._under_mouse = None + self._handle_show_on_own = True + self._mouse_pressed = False + self.set_visible(True) + + def set_increasing(self, increasing): + if self._increasing is increasing: + return + self._increasing = increasing + if increasing: + self._change_anim.setDirection(self._change_anim.Forward) + else: + self._change_anim.setDirection(self._change_anim.Backward) + + if self._change_anim.state() != self._change_anim.Running: + self._change_anim.start() + + def set_visible(self, visible): + if self._is_visible is visible: + return + + self._is_visible = visible + if not visible: + self.set_increasing(False) + if not self._is_anim_finished(): + return + + self.setVisible(visible) + self._check_anim_timer() + + def _is_anim_finished(self): + if self._increasing: + return self._anim_value == self.max_value + return self._anim_value == 0.0 + + def _on_anim(self, value): + self._check_anim_timer() + + self._anim_value = value + + self.update() + + if not self._is_anim_finished(): + return + + if not self._is_visible: + self.setVisible(False) + + def set_handle_show_on_own(self, handle): + if self._handle_show_on_own is handle: + return + self._handle_show_on_own = handle + self._under_mouse = None + self._check_anim_timer() + + def set_under_mouse(self, under_mouse): + if self._under_mouse is under_mouse: + return + + if self._handle_show_on_own: + self._handle_show_on_own = False + self._under_mouse = under_mouse + self.set_increasing(under_mouse) + + def _is_under_mouse(self): + mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos()) + under_mouse = self.rect().contains(mouse_pos) + return under_mouse + + def _check_anim_timer(self): + if not self.isVisible(): + return + + if self._handle_show_on_own: + under_mouse = self._is_under_mouse() + else: + under_mouse = self._under_mouse + + self.set_increasing(under_mouse) + + def enterEvent(self, event): + super(CreateNextPageOverlay, self).enterEvent(event) + if self._handle_show_on_own: + self._check_anim_timer() + + def leaveEvent(self, event): + super(CreateNextPageOverlay, self).leaveEvent(event) + if self._handle_show_on_own: + self._check_anim_timer() + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self._mouse_pressed = True + super(CreateNextPageOverlay, self).mousePressEvent(event) + + def mouseReleaseEvent(self, event): + if self._mouse_pressed: + self._mouse_pressed = False + if self.rect().contains(event.pos()): + self.clicked.emit() + + super(CreateNextPageOverlay, self).mouseReleaseEvent(event) + + def paintEvent(self, event): + painter = QtGui.QPainter() + painter.begin(self) + if self._anim_value == 0.0: + painter.end() + return + painter.setRenderHints( + painter.Antialiasing + | painter.SmoothPixmapTransform + ) + + pen = QtGui.QPen() + pen.setWidth(0) + painter.setPen(pen) + rect = QtCore.QRect(self.rect()) + + offset = rect.width() - int( + float(rect.width()) * 0.01 * self._anim_value + ) + + pos_y = rect.center().y() + left = rect.left() + offset + right = rect.right() + top = rect.top() + bottom = rect.bottom() + width = right - left + height = bottom - top + + q_height = height * 0.15 + + arrow_half_height = width * 0.2 + arrow_x_start = left + (width * 0.4) + arrow_x_end = arrow_x_start + arrow_half_height + arrow_top_y_boundry = arrow_half_height + q_height + arrow_bottom_y_boundry = height - (arrow_half_height + q_height) + offset = 0 + if pos_y < arrow_top_y_boundry: + pos_y = arrow_top_y_boundry + elif pos_y > arrow_bottom_y_boundry: + pos_y = arrow_bottom_y_boundry + + top_cubic_y = pos_y - q_height + bottom_cubic_y = pos_y + q_height + + path = QtGui.QPainterPath() + path.moveTo(right, top) + path.lineTo(right, bottom) + + path.cubicTo( + right, bottom, + left, bottom_cubic_y, + left, pos_y + ) + path.cubicTo( + left, top_cubic_y, + right, top, + right, top + ) + path.closeSubpath() + + painter.fillPath(path, self._bg_color) + + src_arrow_path = QtGui.QPainterPath() + src_arrow_path.moveTo(arrow_x_start, pos_y - arrow_half_height) + src_arrow_path.lineTo(arrow_x_end, pos_y) + src_arrow_path.lineTo(arrow_x_start, pos_y + arrow_half_height) + + arrow_stroker = QtGui.QPainterPathStroker() + arrow_stroker.setWidth(min(4, arrow_half_height * 0.2)) + arrow_path = arrow_stroker.createStroke(src_arrow_path) + + painter.fillPath(arrow_path, self._arrow_color) + + painter.end() From 30789058b34e0445da3c6a4a1bb12fafb073c3b9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 19:23:38 +0100 Subject: [PATCH 100/371] overview widget can return global geo of subset view widget --- openpype/tools/publisher/widgets/overview_widget.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index be3839b90b..1c924d1631 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -195,6 +195,16 @@ class OverviewWidget(QtWidgets.QFrame): self._subset_views_widget.setMaximumWidth(view_width) self._change_anim.start() + def get_subset_views_geo(self): + parent = self._subset_views_widget.parent() + global_pos = parent.mapToGlobal(self._subset_views_widget.pos()) + return QtCore.QRect( + global_pos.x(), + global_pos.y(), + self._subset_views_widget.width(), + self._subset_views_widget.height() + ) + def _on_create_clicked(self): """Pass signal to parent widget which should care about changing state. From 90d0dd718bce3a4537ffe8d2301484369cd67e84 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 19:24:35 +0100 Subject: [PATCH 101/371] prepared methods for set/check current tab --- openpype/tools/publisher/window.py | 31 +++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index d8a69bbeb0..7a0c34e298 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -403,7 +403,7 @@ class PublisherWindow(QtWidgets.QDialog): self._context_label.setText(label) def _update_publish_details_widget(self, force=False): - if not force and self._tabs_widget.current_tab() != "details": + if not force and not self._is_current_tab("details"): return report_data = self.controller.get_publish_report() @@ -434,7 +434,7 @@ class PublisherWindow(QtWidgets.QDialog): ) def _on_tab_change(self, old_tab, new_tab): - if old_tab == "details": + if old_tab != "details": self._publish_details_widget.close_details_popup() if new_tab in ("create", "publish"): @@ -463,14 +463,23 @@ class PublisherWindow(QtWidgets.QDialog): def _on_create_request(self): self._go_to_create_tab() + def _set_current_tab(self, identifier): + self._tabs_widget.set_current_tab(identifier) + + def _is_current_tab(self, identifier): + return self._tabs_widget.is_current_tab(identifier) + def _go_to_create_tab(self): - self._tabs_widget.set_current_tab("create") + self._set_current_tab("create") + + def _go_to_publish_tab(self): + self._set_current_tab("publish") def _go_to_details_tab(self): - self._tabs_widget.set_current_tab("details") + self._set_current_tab("details") def _go_to_report_tab(self): - self._tabs_widget.set_current_tab("report") + self._set_current_tab("report") def _set_publish_overlay_visibility(self, visible): if visible: @@ -523,10 +532,10 @@ class PublisherWindow(QtWidgets.QDialog): self._set_footer_enabled(False) self._update_publish_details_widget() if ( - not self._tabs_widget.is_current_tab("create") - and not self._tabs_widget.is_current_tab("publish") + not self._is_current_tab("create") + and not self._is_current_tab("publish") ): - self._tabs_widget.set_current_tab("publish") + self._set_current_tab("publish") def _on_publish_start(self): self._create_tab.setEnabled(False) @@ -542,8 +551,8 @@ class PublisherWindow(QtWidgets.QDialog): self._publish_details_widget.close_details_popup() - if self._tabs_widget.is_current_tab(self._create_tab): - self._tabs_widget.set_current_tab("publish") + if self._is_current_tab(self._create_tab): + self._set_current_tab("publish") def _on_publish_validated_change(self, event): if event["value"]: @@ -556,7 +565,7 @@ class PublisherWindow(QtWidgets.QDialog): publish_has_crashed = self._controller.publish_has_crashed validate_enabled = not publish_has_crashed publish_enabled = not publish_has_crashed - if self._tabs_widget.is_current_tab("publish"): + if self._is_current_tab("publish"): self._go_to_report_tab() if validate_enabled: From ee94f7c46c707846277a8faf4fb3bcf6087f1edf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 19:24:56 +0100 Subject: [PATCH 102/371] added overlay widget and necessary parts to window --- openpype/tools/publisher/window.py | 85 +++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 7 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 7a0c34e298..ddac19f2e5 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -29,6 +29,8 @@ from .widgets import ( HelpButton, HelpDialog, + + CreateNextPageOverlay, ) @@ -225,8 +227,9 @@ 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 + create_overlay_button = CreateNextPageOverlay(self) + create_overlay_button.set_handle_show_on_own(False) + show_timer = QtCore.QTimer() show_timer.setInterval(1) show_timer.timeout.connect(self._on_show_timer) @@ -255,6 +258,7 @@ class PublisherWindow(QtWidgets.QDialog): publish_btn.clicked.connect(self._on_publish_clicked) publish_frame.details_page_requested.connect(self._go_to_details_tab) + create_overlay_button.clicked.connect(self._go_to_publish_tab) controller.event_system.add_callback( "instances.refresh.finished", self._on_instances_refresh @@ -310,6 +314,7 @@ class PublisherWindow(QtWidgets.QDialog): self._publish_overlay = publish_overlay self._publish_frame = publish_frame + self._content_widget = content_widget self._content_stacked_layout = content_stacked_layout self._overview_widget = overview_widget @@ -342,6 +347,9 @@ class PublisherWindow(QtWidgets.QDialog): self._set_publish_visibility(False) + self._create_overlay_button = create_overlay_button + self._app_event_listener_installed = False + self._show_timer = show_timer self._show_counter = 0 @@ -355,11 +363,38 @@ class PublisherWindow(QtWidgets.QDialog): self._first_show = False self._on_first_show() + self._show_counter = 0 self._show_timer.start() def resizeEvent(self, event): super(PublisherWindow, self).resizeEvent(event) self._update_publish_frame_rect() + self._update_create_overlay_size() + + def closeEvent(self, event): + self._uninstall_app_event_listener() + self.save_changes() + self._reset_on_show = True + super(PublisherWindow, self).closeEvent(event) + + def eventFilter(self, obj, event): + if event.type() == QtCore.QEvent.MouseMove: + self._update_create_overlay_visibility(event.globalPos()) + return super(PublisherWindow, self).eventFilter(obj, event) + + def _install_app_event_listener(self): + if self._app_event_listener_installed: + return + self._app_event_listener_installed = True + app = QtWidgets.QApplication.instance() + app.installEventFilter(self) + + def _uninstall_app_event_listener(self): + if not self._app_event_listener_installed: + return + self._app_event_listener_installed = False + app = QtWidgets.QApplication.instance() + app.removeEventFilter(self) def _on_overlay_message(self, event): self._overlay_object.add_message( @@ -383,16 +418,16 @@ class PublisherWindow(QtWidgets.QDialog): # Reset counter when done for next show event self._show_counter = 0 + self._update_create_overlay_size() + self._update_create_overlay_visibility() + if self._is_current_tab("create"): + self._install_app_event_listener() + # Reset if requested if self._reset_on_show: self._reset_on_show = False self.reset() - def closeEvent(self, event): - self.save_changes() - self._reset_on_show = True - super(PublisherWindow, self).closeEvent(event) - def save_changes(self): self._controller.save_changes() @@ -457,6 +492,13 @@ class PublisherWindow(QtWidgets.QDialog): self._report_widget ) + is_create = new_tab == "create" + if is_create: + self._install_app_event_listener() + else: + self._uninstall_app_event_listener() + self._create_overlay_button.set_visible(is_create) + def _on_context_or_active_change(self): self._validate_create_instances() @@ -669,6 +711,35 @@ class PublisherWindow(QtWidgets.QDialog): event["title"], new_failed_info, "Convertor:" ) + def _update_create_overlay_size(self): + height = self._content_widget.height() + metrics = self._create_overlay_button.fontMetrics() + width = int(metrics.height() * 3) + pos_x = self.width() - width + + tab_pos = self._tabs_widget.parent().mapTo( + self, self._tabs_widget.pos() + ) + tab_height = self._tabs_widget.height() + pos_y = tab_pos.y() + tab_height + + self._create_overlay_button.setGeometry( + pos_x, pos_y, + width, height + ) + + def _update_create_overlay_visibility(self, global_pos=None): + if global_pos is None: + global_pos = QtGui.QCursor.pos() + + under_mouse = False + my_pos = self.mapFromGlobal(global_pos) + if self.rect().contains(my_pos): + widget_geo = self._overview_widget.get_subset_views_geo() + widget_x = widget_geo.left() + (widget_geo.width() * 0.5) + under_mouse = widget_x < global_pos.x() + self._create_overlay_button.set_under_mouse(under_mouse) + class ErrorsMessageBox(ErrorMessageBox): def __init__(self, error_title, failed_info, message_start, parent): From ea6e924dd95b86053092af0f08790b8e8a77be83 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 19:31:53 +0100 Subject: [PATCH 103/371] use gradient and different color --- openpype/tools/publisher/widgets/widgets.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 507ecedb0f..975a1faa06 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1661,7 +1661,6 @@ class CreateNextPageOverlay(QtWidgets.QWidget): def __init__(self, parent): super(CreateNextPageOverlay, self).__init__(parent) - self._bg_color = QtGui.QColor(127, 127, 255) self._arrow_color = QtGui.QColor(255, 255, 255) change_anim = QtCore.QVariantAnimation() @@ -1839,7 +1838,11 @@ class CreateNextPageOverlay(QtWidgets.QWidget): ) path.closeSubpath() - painter.fillPath(path, self._bg_color) + gradient = QtGui.QLinearGradient(left, pos_y, right, pos_y) + gradient.setColorAt(0, QtGui.QColor(22, 25, 29)) + gradient.setColorAt(1, QtGui.QColor(33, 37, 43)) + + painter.fillPath(path, gradient) src_arrow_path = QtGui.QPainterPath() src_arrow_path.moveTo(arrow_x_start, pos_y - arrow_half_height) From 42b1012e7c320ec783df7ce5c76b76a24e18896e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 31 Oct 2022 19:41:12 +0100 Subject: [PATCH 104/371] use radial gradient --- openpype/tools/publisher/widgets/widgets.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 975a1faa06..c4481d4d9d 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1838,7 +1838,10 @@ class CreateNextPageOverlay(QtWidgets.QWidget): ) path.closeSubpath() - gradient = QtGui.QLinearGradient(left, pos_y, right, pos_y) + radius = height * 0.7 + focal = QtCore.QPointF(left, pos_y) + start_p = QtCore.QPointF(right - (width * 0.5), pos_y) + gradient = QtGui.QRadialGradient(start_p, radius, focal) gradient.setColorAt(0, QtGui.QColor(22, 25, 29)) gradient.setColorAt(1, QtGui.QColor(33, 37, 43)) From 17125a62edec43fe4c144485e584be964398aa41 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 31 Oct 2022 21:03:16 +0100 Subject: [PATCH 105/371] hiero: adding fallback if incompatible knobs from version to version --- openpype/hosts/hiero/api/pipeline.py | 6 +++--- openpype/hosts/hiero/plugins/load/load_effects.py | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index 26c8ebe6d3..c48d404ede 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -134,7 +134,7 @@ def ls(): for item in all_items: container_data = parse_container(item) - if isinstance(container, list): + if isinstance(container_data, list): for _c in container_data: yield _c @@ -187,7 +187,7 @@ def parse_container(item, validate=True): if type(item) == hiero.core.VideoTrack: return_list = [] _data = lib.get_track_openpype_data(item) - log.info("_data: {}".format(_data)) + if not _data: return # convert the data to list and validate them @@ -196,7 +196,7 @@ def parse_container(item, validate=True): return_list.append(cotnainer) return return_list else: - _data = lib.get_track_item_pype_data(item) + _data = lib.get_trackitem_openpype_data(item) return data_to_container(item, _data) diff --git a/openpype/hosts/hiero/plugins/load/load_effects.py b/openpype/hosts/hiero/plugins/load/load_effects.py index 16c9187ad9..d8a388c6ed 100644 --- a/openpype/hosts/hiero/plugins/load/load_effects.py +++ b/openpype/hosts/hiero/plugins/load/load_effects.py @@ -94,7 +94,12 @@ class LoadEffects(load.LoaderPlugin): or knob_name == "name" ): continue - node[knob_name].setValue(knob_value) + + try: + node[knob_name].setValue(knob_value) + except NameError: + self.log.warning("Knob: {} cannot be set".format( + knob_name)) # register all loaded children data_imprint["children_names"].append(new_name) From 576903575e78e20ba0c7401061741b3c4cc50218 Mon Sep 17 00:00:00 2001 From: Felix David Date: Tue, 1 Nov 2022 10:00:26 +0100 Subject: [PATCH 106/371] Project setting --- .../hooks/pre_copy_last_published_workfile.py | 119 ++++++++++++------ .../defaults/project_settings/global.json | 3 +- .../schemas/schema_global_tools.json | 5 + 3 files changed, 88 insertions(+), 39 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 004f9d25e7..312548d2db 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -7,8 +7,10 @@ from openpype.client.entities import ( get_subsets, ) from openpype.lib import PreLaunchHook +from openpype.lib.profiles_filtering import filter_profiles from openpype.modules.base import ModulesManager from openpype.pipeline.load.utils import get_representation_path +from openpype.settings.lib import get_project_settings class CopyLastPublishedWorkfile(PreLaunchHook): @@ -32,9 +34,45 @@ class CopyLastPublishedWorkfile(PreLaunchHook): Returns: None: This is a void method. """ - # TODO setting + project_name = self.data["project_name"] + task_name = self.data["task_name"] + task_type = self.data["task_type"] + host_name = self.application.host_name + + # Check settings has enabled it + project_settings = get_project_settings(project_name) + profiles = project_settings["global"]["tools"]["Workfiles"][ + "last_workfile_on_startup" + ] + filter_data = { + "tasks": task_name, + "task_types": task_type, + "hosts": host_name, + } + last_workfile_settings = filter_profiles(profiles, filter_data) + use_last_published_workfile = last_workfile_settings.get( + "use_last_published_workfile" + ) + if use_last_published_workfile is None: + self.log.info( + ( + "Seems like old version of settings is used." + ' Can\'t access custom templates in host "{}".' + ).format(host_name) + ) + return + elif use_last_published_workfile is False: + self.log.info( + ( + 'Project "{}" has turned off to use last published workfile' + ' as first workfile for host "{}"' + ).format(project_name, host_name) + ) + return + self.log.info("Trying to fetch last published workfile...") + # Check there is no workfile available last_workfile = self.data.get("last_workfile_path") if os.path.exists(last_workfile): self.log.debug( @@ -44,9 +82,6 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ) return - project_name = self.data["project_name"] - task_name = self.data["task_name"] - project_doc = self.data.get("project_doc") asset_doc = self.data.get("asset_doc") anatomy = self.data.get("anatomy") @@ -65,6 +100,9 @@ class CopyLastPublishedWorkfile(PreLaunchHook): None, ) if not subset_id: + self.log.debug('No any workfile for asset "{}".').format( + asset_doc["name"] + ) return # Get workfile representation @@ -84,41 +122,46 @@ class CopyLastPublishedWorkfile(PreLaunchHook): None, ) - if workfile_representation: # TODO add setting - # Get sync server from Tray, which handles the asynchronous thread instance - sync_server = next( - ( - t["sync_server"] - for t in [ - obj - for obj in gc.get_objects() - if isinstance(obj, ModulesManager) - ] - if t["sync_server"].sync_server_thread - ), - None, - ) + if not workfile_representation: + self.log.debug( + 'No published workfile for task "{}" and host "{}".' + ).format(task_name, host_name) + return - # Add site and reset timer - active_site = sync_server.get_active_site(project_name) - sync_server.add_site( - project_name, - workfile_representation["_id"], - active_site, - force=True, - ) - sync_server.reset_timer() + # Get sync server from Tray, which handles the asynchronous thread instance + sync_server = next( + ( + t["sync_server"] + for t in [ + obj + for obj in gc.get_objects() + if isinstance(obj, ModulesManager) + ] + if t["sync_server"].sync_server_thread + ), + None, + ) - # Wait for the download loop to end - sync_server.sync_server_thread.files_processed.wait() + # Add site and reset timer + active_site = sync_server.get_active_site(project_name) + sync_server.add_site( + project_name, + workfile_representation["_id"], + active_site, + force=True, + ) + sync_server.reset_timer() - # Get paths - published_workfile_path = get_representation_path( - workfile_representation, root=anatomy.roots - ) - local_workfile_dir = os.path.dirname(last_workfile) + # Wait for the download loop to end + sync_server.sync_server_thread.files_processed.wait() - # Copy file and substitute path - self.data["last_workfile_path"] = shutil.copy( - published_workfile_path, local_workfile_dir - ) + # Get paths + published_workfile_path = get_representation_path( + workfile_representation, root=anatomy.roots + ) + local_workfile_dir = os.path.dirname(last_workfile) + + # Copy file and substitute path + self.data["last_workfile_path"] = shutil.copy( + published_workfile_path, local_workfile_dir + ) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index b128564bc2..5b1c750bf4 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -401,7 +401,8 @@ "hosts": [], "task_types": [], "tasks": [], - "enabled": true + "enabled": true, + "use_last_published_workfile": false } ], "open_workfile_tool_on_startup": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index ba446135e2..962008d476 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -149,6 +149,11 @@ "type": "boolean", "key": "enabled", "label": "Enabled" + }, + { + "type": "boolean", + "key": "use_last_published_workfile", + "label": "Use last published workfile" } ] } From 10fb9a141159302d4321d1acb8409fa5f341d7c9 Mon Sep 17 00:00:00 2001 From: Felix David Date: Tue, 1 Nov 2022 10:10:12 +0100 Subject: [PATCH 107/371] docstring --- openpype/hooks/pre_copy_last_published_workfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 312548d2db..b1b2fe2366 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -30,6 +30,7 @@ class CopyLastPublishedWorkfile(PreLaunchHook): 1- Check if setting for this feature is enabled 2- Check if workfile in work area doesn't exist 3- Check if published workfile exists and is copied locally in publish + 4- Substitute copied published workfile as first workfile Returns: None: This is a void method. From bebb9031c18534d06a3e7236f950c16c6cc88c02 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 10:12:26 +0100 Subject: [PATCH 108/371] 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 895bfbaae5aaebaf97c30233ae407f12ad52ca7d Mon Sep 17 00:00:00 2001 From: Felix David Date: Tue, 1 Nov 2022 10:14:58 +0100 Subject: [PATCH 109/371] comment length --- openpype/hooks/pre_copy_last_published_workfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index b1b2fe2366..d342151823 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -129,7 +129,8 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ).format(task_name, host_name) return - # Get sync server from Tray, which handles the asynchronous thread instance + # Get sync server from Tray, + # which handles the asynchronous thread instance sync_server = next( ( t["sync_server"] From c49017e6718ee169f052a75689b5624dc36705dc Mon Sep 17 00:00:00 2001 From: Felix David Date: Tue, 1 Nov 2022 10:50:30 +0100 Subject: [PATCH 110/371] lint --- openpype/hooks/pre_copy_last_published_workfile.py | 4 ++-- openpype/modules/sync_server/sync_server.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index d342151823..cf4edeac9b 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -65,8 +65,8 @@ class CopyLastPublishedWorkfile(PreLaunchHook): elif use_last_published_workfile is False: self.log.info( ( - 'Project "{}" has turned off to use last published workfile' - ' as first workfile for host "{}"' + 'Project "{}" has turned off to use last published' + ' workfile as first workfile for host "{}"' ).format(project_name, host_name) ) return diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index def9e6cfd8..353b39c4e1 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -239,7 +239,7 @@ class SyncServerThread(threading.Thread): # Event to trigger files have been processed self.files_processed = threading.Event() - + super(SyncServerThread, self).__init__(args=(self.files_processed,)) self.module = module self.loop = None From 32b91ef39feeb4e68eb05b2b4fe060fcf8902884 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 11:32:12 +0100 Subject: [PATCH 111/371] 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 e4e6044198a7240e21387c2931926f7d0cffdbc2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 11:56:27 +0100 Subject: [PATCH 112/371] fix last pixel --- openpype/tools/publisher/widgets/widgets.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index c4481d4d9d..b8fb2d38b9 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1800,9 +1800,10 @@ class CreateNextPageOverlay(QtWidgets.QWidget): pos_y = rect.center().y() left = rect.left() + offset - right = rect.right() top = rect.top() - bottom = rect.bottom() + # Right and bootm is pixel index + right = rect.right() + 1 + bottom = rect.bottom() + 1 width = right - left height = bottom - top From 049de296240198cdf296d0ff411c2601f1568589 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 11:57:32 +0100 Subject: [PATCH 113/371] handle leave event --- openpype/tools/publisher/window.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index ddac19f2e5..2063cdab96 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -377,6 +377,10 @@ class PublisherWindow(QtWidgets.QDialog): self._reset_on_show = True super(PublisherWindow, self).closeEvent(event) + def leaveEvent(self, event): + super(PublisherWindow, self).leaveEvent(event) + self._update_create_overlay_visibility() + def eventFilter(self, obj, event): if event.type() == QtCore.QEvent.MouseMove: self._update_create_overlay_visibility(event.globalPos()) From 638f8238250cdd4ce7935c9c637dd0c3e80746ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 12:07:03 +0100 Subject: [PATCH 114/371] 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 115/371] 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 116/371] 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 117/371] 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 118/371] 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 02fb9561d7f25a1146d9c59dd2306bce7e166edf Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 1 Nov 2022 20:36:58 +0800 Subject: [PATCH 119/371] Alembic Loader as Arnold Standin --- .../maya/plugins/load/load_abc_to_standin.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index f39aa56650..68aeb24069 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -13,7 +13,7 @@ class AlembicStandinLoader(load.LoaderPlugin): families = ["model", "pointcache"] representations = ["abc"] - label = "Import Alembic as Standin" + label = "Import Alembic as Arnold Standin" order = -5 icon = "code-fork" color = "orange" @@ -21,7 +21,6 @@ class AlembicStandinLoader(load.LoaderPlugin): def load(self, context, name, namespace, options): import maya.cmds as cmds - import pymel.core as pm import mtoa.ui.arnoldmenu from openpype.hosts.maya.api.pipeline import containerise from openpype.hosts.maya.api.lib import unique_namespace @@ -42,7 +41,7 @@ class AlembicStandinLoader(load.LoaderPlugin): # Root group label = "{}:{}".format(namespace, name) - root = pm.group(name=label, empty=True) + root = cmds.group(name=label, empty=True) settings = get_project_settings(os.environ['AVALON_PROJECT']) colors = settings["maya"]["load"]["colors"] @@ -55,16 +54,17 @@ class AlembicStandinLoader(load.LoaderPlugin): transform_name = label + "_ABC" - standinShape = pm.PyNode(mtoa.ui.arnoldmenu.createStandIn()) - standin = standinShape.getParent() - standin.rename(transform_name) + standinShape = cmds.ls(mtoa.ui.arnoldmenu.createStandIn())[0] + standin = cmds.listRelatives(standinShape, parent=True, typ="transform") + standin = cmds.rename(standin, transform_name) + standinShape = cmds.listRelatives(standin, children=True)[0] - pm.parent(standin, root) + cmds.parent(standin, root) # Set the standin filepath - standinShape.dso.set(self.fname) + cmds.setAttr(standinShape + ".dso", self.fname, type="string") if frameStart is not None: - standinShape.useFrameExtension.set(1) + cmds.setAttr(standinShape + ".useFrameExtension", 1) nodes = [root, standin] self[:] = nodes From 6b0d25cb7c6f3c0dd084bbccf0b5f06fe8fe1341 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 1 Nov 2022 20:38:51 +0800 Subject: [PATCH 120/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index 68aeb24069..5d6c52eac9 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -55,7 +55,8 @@ class AlembicStandinLoader(load.LoaderPlugin): transform_name = label + "_ABC" standinShape = cmds.ls(mtoa.ui.arnoldmenu.createStandIn())[0] - standin = cmds.listRelatives(standinShape, parent=True, typ="transform") + standin = cmds.listRelatives(standinShape, parent=True, + typ="transform") standin = cmds.rename(standin, transform_name) standinShape = cmds.listRelatives(standin, children=True)[0] From f2a2cc1c38304d33eb1ebf410961bc9bf4f23da6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 14:13:15 +0100 Subject: [PATCH 121/371] 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 122/371] '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 123/371] 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 124/371] 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 125/371] 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 72ce97a6285e1e31782b8c9a3c5e0d6bb49ab56c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 1 Nov 2022 16:07:06 +0100 Subject: [PATCH 126/371] general: fixing loader for multiselection --- 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 761d624b2e40901be713068157ad7d7aeaeb3bb4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 16:07:50 +0100 Subject: [PATCH 127/371] 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 3dd115feef6c02e0effe7b44874c63056ed8a775 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 1 Nov 2022 16:38:41 +0100 Subject: [PATCH 128/371] hiero: return specific container name --- openpype/hosts/hiero/api/lib.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index d04a710df1..e340209207 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -394,7 +394,7 @@ def get_track_openpype_tag(track): return tag -def get_track_openpype_data(track): +def get_track_openpype_data(track, container_name=None): """ Get track's openpype tag data. @@ -416,12 +416,16 @@ def get_track_openpype_data(track): for obj_name, obj_data in tag_data.items(): obj_name = obj_name.replace("tag.", "") - print(obj_name) + if obj_name in ["applieswhole", "note", "label"]: continue return_data[obj_name] = json.loads(obj_data) - return return_data + return ( + return_data[container_name] + if container_name + else return_data + ) @deprecated("openpype.hosts.hiero.api.lib.get_trackitem_openpype_tag") From 393692559e3f57b5ed4db333e8c5e2c997801437 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 1 Nov 2022 16:39:08 +0100 Subject: [PATCH 129/371] hiero: deep copy dicts --- openpype/hosts/hiero/api/pipeline.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index c48d404ede..3475bc62e4 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -1,6 +1,7 @@ """ Basic avalon integration """ +from copy import deepcopy import os import contextlib from collections import OrderedDict @@ -225,19 +226,19 @@ def update_container(item, data=None): if type(item) == hiero.core.VideoTrack: # form object data for test - object_name = "{}_{}".format( - data["name"], data["namespace"]) + object_name = data["objectName"] # get all available containers containers = lib.get_track_openpype_data(item) - for obj_name, container in containers.items(): - # ignore all which are not the same object - if object_name != obj_name: - continue - # update data in container - updated_container = update_container_data(container, data) - # merge updated container back to containers - containers.update(updated_container) + container = lib.get_track_openpype_data(item, object_name) + + containers = deepcopy(containers) + container = deepcopy(container) + + # update data in container + updated_container = update_container_data(container, data) + # merge updated container back to containers + containers.update({object_name: updated_container}) return bool(lib.set_track_openpype_tag(item, containers)) else: From 5b77f92d0bbf9cedb2f6c7b2a81964c45ccabd73 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 1 Nov 2022 16:39:23 +0100 Subject: [PATCH 130/371] hiero: removing obsolete code --- openpype/hosts/hiero/api/tags.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/hosts/hiero/api/tags.py b/openpype/hosts/hiero/api/tags.py index 918af3dc1f..cb7bc14edb 100644 --- a/openpype/hosts/hiero/api/tags.py +++ b/openpype/hosts/hiero/api/tags.py @@ -86,12 +86,6 @@ def update_tag(tag, data): # get metadata key from data data_mtd = data.get("metadata", {}) - # # due to hiero bug we have to make sure keys which are not existent in - # # data are cleared of value by `None` - # for _mk in mtd.dict().keys(): - # if _mk.replace("tag.", "") not in data_mtd.keys(): - # mtd.setValue(_mk, str(None)) - # set all data metadata to tag metadata for _k, _v in data_mtd.items(): value = str(_v) From 8c715a98aaa8bf4343b35f565400197ced021b0a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 1 Nov 2022 16:39:59 +0100 Subject: [PATCH 131/371] hiero: update effects finish --- .../hosts/hiero/plugins/load/load_effects.py | 145 ++++++++++++------ 1 file changed, 101 insertions(+), 44 deletions(-) diff --git a/openpype/hosts/hiero/plugins/load/load_effects.py b/openpype/hosts/hiero/plugins/load/load_effects.py index d8a388c6ed..3e5225ba22 100644 --- a/openpype/hosts/hiero/plugins/load/load_effects.py +++ b/openpype/hosts/hiero/plugins/load/load_effects.py @@ -1,11 +1,16 @@ import json from collections import OrderedDict -from pprint import pprint import six +from openpype.client import ( + get_version_by_id +) + from openpype.pipeline import ( AVALON_CONTAINER_ID, - load + load, + legacy_io, + get_representation_path ) from openpype.hosts.hiero import api as phiero @@ -40,18 +45,12 @@ class LoadEffects(load.LoaderPlugin): active_sequence, "LoadedEffects") # get main variables - version = context["version"] - version_data = version.get("data", {}) - vname = version.get("name", None) namespace = namespace or context["asset"]["name"] object_name = "{}_{}".format(name, namespace) clip_in = context["asset"]["data"]["clipIn"] clip_out = context["asset"]["data"]["clipOut"] data_imprint = { - "source": version_data["source"], - "version": vname, - "author": version_data["author"], "objectName": object_name, "children_names": [] } @@ -59,6 +58,31 @@ class LoadEffects(load.LoaderPlugin): # getting file path file = self.fname.replace("\\", "/") + if self._shared_loading( + file, + active_track, + clip_in, + clip_out, + data_imprint + ): + self.containerise( + active_track, + name=name, + namespace=namespace, + object_name=object_name, + context=context, + loader=self.__class__.__name__, + data=data_imprint) + + def _shared_loading( + self, + file, + active_track, + clip_in, + clip_out, + data_imprint, + update=False + ): # getting data from json file with unicode conversion with open(file, "r") as f: json_f = {self.byteify(key): self.byteify(value) @@ -74,9 +98,6 @@ class LoadEffects(load.LoaderPlugin): loaded = False for index_order, (ef_name, ef_val) in enumerate(nodes_order.items()): - pprint("_" * 100) - pprint(ef_name) - pprint(ef_val) new_name = "{}_loaded".format(ef_name) if new_name not in used_subtracks: effect_track_item = active_track.createEffect( @@ -87,46 +108,82 @@ class LoadEffects(load.LoaderPlugin): ) effect_track_item.setName(new_name) - node = effect_track_item.node() - for knob_name, knob_value in ef_val["node"].items(): - if ( - not knob_value - or knob_name == "name" - ): - continue + else: + effect_track_item = used_subtracks[new_name] - try: - node[knob_name].setValue(knob_value) - except NameError: - self.log.warning("Knob: {} cannot be set".format( - knob_name)) + node = effect_track_item.node() + for knob_name, knob_value in ef_val["node"].items(): + if ( + not knob_value + or knob_name == "name" + ): + continue - # register all loaded children - data_imprint["children_names"].append(new_name) - # make sure containerisation will happen - loaded = True + try: + node[knob_name].setValue(knob_value) + except NameError: + self.log.warning("Knob: {} cannot be set".format( + knob_name)) - if not loaded: - return + # register all loaded children + data_imprint["children_names"].append(new_name) - self.containerise( - active_track, - name=name, - namespace=namespace, - object_name=object_name, - context=context, - loader=self.__class__.__name__, - data=data_imprint) + # make sure containerisation will happen + loaded = True + + return loaded def update(self, container, representation): - """Update the Loader's path - - Nuke automatically tries to reset some variables when changing - the loader's path to a new file. These automatic changes are to its - inputs: - + """ Updating previously loaded effects """ - pass + active_track = container["_item"] + file = get_representation_path(representation).replace("\\", "/") + + # get main variables + name = container['name'] + namespace = container['namespace'] + + # get timeline in out data + project_name = legacy_io.active_project() + version_doc = get_version_by_id(project_name, representation["parent"]) + version_data = version_doc["data"] + clip_in = version_data["clipIn"] + clip_out = version_data["clipOut"] + + object_name = "{}_{}".format(name, namespace) + + # Disable previously created nodes + used_subtracks = { + stitem.name(): stitem + for stitem in phiero.flatten(active_track.subTrackItems()) + } + container = phiero.get_track_openpype_data( + active_track, object_name + ) + + loaded_subtrack_items = container["children_names"] + for loaded_stitem in loaded_subtrack_items: + if loaded_stitem not in used_subtracks: + continue + item_to_remove = used_subtracks.pop(loaded_stitem) + item_to_remove.node()["enable"].setValue(0) + + data_imprint = { + "objectName": object_name, + "name": name, + "representation": str(representation["_id"]), + "children_names": [] + } + + if self._shared_loading( + file, + active_track, + clip_in, + clip_out, + data_imprint, + update=True + ): + return phiero.update_container(active_track, data_imprint) def reorder_nodes(self, data): new_order = OrderedDict() From f3b038ec7df4e77be2a251d3c84722736dc832cc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 1 Nov 2022 16:45:27 +0100 Subject: [PATCH 132/371] hiero: removing unused attribute --- openpype/hosts/hiero/plugins/load/load_effects.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/load/load_effects.py b/openpype/hosts/hiero/plugins/load/load_effects.py index 3e5225ba22..fab426e58d 100644 --- a/openpype/hosts/hiero/plugins/load/load_effects.py +++ b/openpype/hosts/hiero/plugins/load/load_effects.py @@ -25,7 +25,6 @@ class LoadEffects(load.LoaderPlugin): order = 0 icon = "cc" color = "white" - ignore_attr = ["useLifetime"] def load(self, context, name, namespace, data): """ From 7df622df0cfd2af6092faf5918e75e97e1658473 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 18:29:21 +0100 Subject: [PATCH 133/371] 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 d655a53136e724179da0889d0e508b607d9d173c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 1 Nov 2022 18:45:07 +0100 Subject: [PATCH 134/371] use objected colors from styles --- openpype/tools/publisher/widgets/widgets.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index b8fb2d38b9..444ad4c7dc 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1661,7 +1661,15 @@ class CreateNextPageOverlay(QtWidgets.QWidget): def __init__(self, parent): super(CreateNextPageOverlay, self).__init__(parent) - self._arrow_color = QtGui.QColor(255, 255, 255) + self._arrow_color = ( + get_objected_colors("bg-buttons").get_qcolor() + ) + self._gradient_start_color = ( + get_objected_colors("publisher", "tab-bg").get_qcolor() + ) + self._gradient_end_color = ( + get_objected_colors("bg-inputs").get_qcolor() + ) change_anim = QtCore.QVariantAnimation() change_anim.setStartValue(0.0) @@ -1843,8 +1851,8 @@ class CreateNextPageOverlay(QtWidgets.QWidget): focal = QtCore.QPointF(left, pos_y) start_p = QtCore.QPointF(right - (width * 0.5), pos_y) gradient = QtGui.QRadialGradient(start_p, radius, focal) - gradient.setColorAt(0, QtGui.QColor(22, 25, 29)) - gradient.setColorAt(1, QtGui.QColor(33, 37, 43)) + gradient.setColorAt(0, self._gradient_start_color) + gradient.setColorAt(1, self._gradient_end_color) painter.fillPath(path, gradient) From 2e1f2fd912b9ceee565b489eb71c120e8228f107 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 1 Nov 2022 21:21:46 +0100 Subject: [PATCH 135/371] flame: adding menu loader with selection --- openpype/hosts/flame/api/menu.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/api/menu.py b/openpype/hosts/flame/api/menu.py index f72a352bba..319ed7afb6 100644 --- a/openpype/hosts/flame/api/menu.py +++ b/openpype/hosts/flame/api/menu.py @@ -225,7 +225,8 @@ class FlameMenuUniversal(_FlameMenuApp): menu['actions'].append({ "name": "Load...", - "execute": lambda x: self.tools_helper.show_loader() + "execute": lambda x: callback_selection( + x, self.tools_helper.show_loader) }) menu['actions'].append({ "name": "Manage...", From 51a7dea7209559bce178d53c776cc340185d2973 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 1 Nov 2022 21:22:09 +0100 Subject: [PATCH 136/371] flame: adding batch name to name of openclip --- openpype/hosts/flame/plugins/load/load_clip_batch.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/load/load_clip_batch.py b/openpype/hosts/flame/plugins/load/load_clip_batch.py index 3b049b861b..4b510c9ce8 100644 --- a/openpype/hosts/flame/plugins/load/load_clip_batch.py +++ b/openpype/hosts/flame/plugins/load/load_clip_batch.py @@ -1,3 +1,4 @@ +from copy import deepcopy import os import flame from pprint import pformat @@ -22,7 +23,7 @@ class LoadClipBatch(opfapi.ClipLoader): # settings reel_name = "OP_LoadedReel" - clip_name_template = "{asset}_{subset}<_{output}>" + clip_name_template = "{batch}_{asset}_{subset}<_{output}>" def load(self, context, name, namespace, options): @@ -40,6 +41,9 @@ class LoadClipBatch(opfapi.ClipLoader): if not context["representation"]["context"].get("output"): self.clip_name_template.replace("output", "representation") + formating_data = deepcopy(context["representation"]["context"]) + formating_data["batch"] = self.batch.name.get_value() + clip_name = StringTemplate(self.clip_name_template).format( context["representation"]["context"]) @@ -56,6 +60,7 @@ class LoadClipBatch(opfapi.ClipLoader): openclip_path = os.path.join( openclip_dir, clip_name + ".clip" ) + if not os.path.exists(openclip_dir): os.makedirs(openclip_dir) From bae7a9960a4f28e23aab10f959789eaa00c20c87 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 2 Nov 2022 04:02:57 +0000 Subject: [PATCH 137/371] [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/371] 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/371] 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/371] 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/371] 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/371] 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 dbc4b64ca138d5b78326b27d3dd1fe986e3e4223 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 2 Nov 2022 20:39:04 +0100 Subject: [PATCH 143/371] Hiero: adding formating dict with batch --- openpype/hosts/flame/plugins/load/load_clip_batch.py | 2 +- openpype/settings/defaults/project_settings/flame.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/plugins/load/load_clip_batch.py b/openpype/hosts/flame/plugins/load/load_clip_batch.py index 4b510c9ce8..17ad8075e4 100644 --- a/openpype/hosts/flame/plugins/load/load_clip_batch.py +++ b/openpype/hosts/flame/plugins/load/load_clip_batch.py @@ -45,7 +45,7 @@ class LoadClipBatch(opfapi.ClipLoader): formating_data["batch"] = self.batch.name.get_value() clip_name = StringTemplate(self.clip_name_template).format( - context["representation"]["context"]) + formating_data) # TODO: settings in imageio # convert colorspace with ocio to flame mapping diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json index 0f3080ad64..34baf9ba06 100644 --- a/openpype/settings/defaults/project_settings/flame.json +++ b/openpype/settings/defaults/project_settings/flame.json @@ -142,7 +142,7 @@ "exr16fpdwaa" ], "reel_name": "OP_LoadedReel", - "clip_name_template": "{asset}_{subset}<_{output}>" + "clip_name_template": "{batch}_{asset}_{subset}<_{output}>" } } } \ No newline at end of file From e3c339b1537a3afdde9b694c825a8ab71ad5abe2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Nov 2022 11:27:53 +0100 Subject: [PATCH 144/371] 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 145/371] 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 ed96f1d5b33649e8f5f21e5598e4ee56436f63df Mon Sep 17 00:00:00 2001 From: Felix David Date: Thu, 3 Nov 2022 11:41:59 +0100 Subject: [PATCH 146/371] requested cosmetic changes --- .../hooks/pre_copy_last_published_workfile.py | 172 +++++++++--------- 1 file changed, 90 insertions(+), 82 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index cf4edeac9b..7a835507f7 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -35,6 +35,17 @@ class CopyLastPublishedWorkfile(PreLaunchHook): Returns: None: This is a void method. """ + # Check there is no workfile available + last_workfile = self.data.get("last_workfile_path") + if os.path.exists(last_workfile): + self.log.debug( + "Last workfile exists. Skipping {} process.".format( + self.__class__.__name__ + ) + ) + return + + # Get data project_name = self.data["project_name"] task_name = self.data["task_name"] task_type = self.data["task_type"] @@ -73,97 +84,94 @@ class CopyLastPublishedWorkfile(PreLaunchHook): self.log.info("Trying to fetch last published workfile...") - # Check there is no workfile available - last_workfile = self.data.get("last_workfile_path") - if os.path.exists(last_workfile): - self.log.debug( - "Last workfile exists. Skipping {} process.".format( - self.__class__.__name__ - ) - ) - return - project_doc = self.data.get("project_doc") asset_doc = self.data.get("asset_doc") anatomy = self.data.get("anatomy") - if project_doc and asset_doc: - # Get subset id - subset_id = next( - ( - subset["_id"] - for subset in get_subsets( - project_name, - asset_ids=[asset_doc["_id"]], - fields=["_id", "data.family"], - ) - if subset["data"]["family"] == "workfile" - ), - None, - ) - if not subset_id: - self.log.debug('No any workfile for asset "{}".').format( - asset_doc["name"] - ) - return - # Get workfile representation - workfile_representation = next( - ( - representation - for representation in get_representations( - project_name, - version_ids=[ + # Check it can proceed + if not project_doc and not asset_doc: + return + + # Get subset id + subset_id = next( + ( + subset["_id"] + for subset in get_subsets( + project_name, + asset_ids=[asset_doc["_id"]], + fields=["_id", "data.family"], + ) + if subset["data"]["family"] == "workfile" + ), + None, + ) + if not subset_id: + self.log.debug('No any workfile for asset "{}".').format( + asset_doc["name"] + ) + return + + # Get workfile representation + workfile_representation = next( + ( + representation + for representation in get_representations( + project_name, + version_ids=[ + ( get_last_version_by_subset_id( project_name, subset_id, fields=["_id"] - )["_id"] - ], - ) - if representation["context"]["task"]["name"] == task_name - ), - None, - ) + ) + or {} + ).get("_id") + ], + ) + if representation["context"]["task"]["name"] == task_name + ), + None, + ) - if not workfile_representation: - self.log.debug( - 'No published workfile for task "{}" and host "{}".' - ).format(task_name, host_name) - return + if not workfile_representation: + self.log.debug( + 'No published workfile for task "{}" and host "{}".' + ).format(task_name, host_name) + return - # Get sync server from Tray, - # which handles the asynchronous thread instance - sync_server = next( - ( - t["sync_server"] - for t in [ - obj - for obj in gc.get_objects() - if isinstance(obj, ModulesManager) - ] - if t["sync_server"].sync_server_thread - ), - None, - ) + # Get sync server from Tray, + # which handles the asynchronous thread instance + sync_server = next( + ( + t["sync_server"] + for t in [ + obj + for obj in gc.get_objects() + if isinstance(obj, ModulesManager) + ] + if t["sync_server"].sync_server_thread + ), + None, + ) - # Add site and reset timer - active_site = sync_server.get_active_site(project_name) - sync_server.add_site( - project_name, - workfile_representation["_id"], - active_site, - force=True, - ) - sync_server.reset_timer() + # Add site and reset timer + active_site = sync_server.get_active_site(project_name) + sync_server.add_site( + project_name, + workfile_representation["_id"], + active_site, + force=True, + ) + sync_server.reset_timer() - # Wait for the download loop to end - sync_server.sync_server_thread.files_processed.wait() + # Wait for the download loop to end + sync_server.sync_server_thread.files_processed.wait() - # Get paths - published_workfile_path = get_representation_path( - workfile_representation, root=anatomy.roots - ) - local_workfile_dir = os.path.dirname(last_workfile) + # Get paths + published_workfile_path = get_representation_path( + workfile_representation, root=anatomy.roots + ) + local_workfile_dir = os.path.dirname(last_workfile) - # Copy file and substitute path - self.data["last_workfile_path"] = shutil.copy( - published_workfile_path, local_workfile_dir - ) + # Copy file and substitute path + self.data["last_workfile_path"] = shutil.copy( + published_workfile_path, local_workfile_dir + ) From 3c4476c2335ce760d2a49a80531f09705af79c68 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 3 Nov 2022 12:00:44 +0100 Subject: [PATCH 147/371] 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 148/371] 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 149/371] 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 150/371] 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 151/371] 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 152/371] 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 153/371] 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 154/371] 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 155/371] 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 156/371] 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 157/371] 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 158/371] 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 159/371] 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 82be7ce8d053eefcb430a6b7d26948821ea6ea11 Mon Sep 17 00:00:00 2001 From: Felix David Date: Thu, 3 Nov 2022 18:33:54 +0100 Subject: [PATCH 160/371] Change to REST API using web server --- .../hooks/pre_copy_last_published_workfile.py | 54 ++++++++------- openpype/modules/sync_server/rest_api.py | 68 +++++++++++++++++++ openpype/modules/sync_server/sync_server.py | 12 ++-- .../modules/sync_server/sync_server_module.py | 9 +++ openpype/modules/timers_manager/rest_api.py | 2 +- 5 files changed, 112 insertions(+), 33 deletions(-) create mode 100644 openpype/modules/sync_server/rest_api.py diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 7a835507f7..cefc7e5d40 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -1,14 +1,14 @@ -import gc import os import shutil +from time import sleep from openpype.client.entities import ( get_last_version_by_subset_id, get_representations, get_subsets, ) from openpype.lib import PreLaunchHook +from openpype.lib.local_settings import get_local_site_id from openpype.lib.profiles_filtering import filter_profiles -from openpype.modules.base import ModulesManager from openpype.pipeline.load.utils import get_representation_path from openpype.settings.lib import get_project_settings @@ -137,33 +137,37 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ).format(task_name, host_name) return - # Get sync server from Tray, - # which handles the asynchronous thread instance - sync_server = next( - ( - t["sync_server"] - for t in [ - obj - for obj in gc.get_objects() - if isinstance(obj, ModulesManager) - ] - if t["sync_server"].sync_server_thread - ), - None, - ) + # POST to webserver sites to add to representations + webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") + if not webserver_url: + self.log.warning("Couldn't find webserver url") + return - # Add site and reset timer - active_site = sync_server.get_active_site(project_name) - sync_server.add_site( - project_name, - workfile_representation["_id"], - active_site, - force=True, + entry_point_url = "{}/sync_server".format(webserver_url) + rest_api_url = "{}/add_sites_to_representations".format( + entry_point_url + ) + try: + import requests + except Exception: + self.log.warning( + "Couldn't add sites to representations ('requests' is not available)" + ) + return + + requests.post( + rest_api_url, + json={ + "project_name": project_name, + "sites": [get_local_site_id()], + "representations": [str(workfile_representation["_id"])], + }, ) - sync_server.reset_timer() # Wait for the download loop to end - sync_server.sync_server_thread.files_processed.wait() + rest_api_url = "{}/files_are_processed".format(entry_point_url) + while requests.get(rest_api_url).content: + sleep(5) # Get paths published_workfile_path = get_representation_path( diff --git a/openpype/modules/sync_server/rest_api.py b/openpype/modules/sync_server/rest_api.py new file mode 100644 index 0000000000..b7c5d26d15 --- /dev/null +++ b/openpype/modules/sync_server/rest_api.py @@ -0,0 +1,68 @@ +from aiohttp.web_response import Response +from openpype.lib import Logger + + +class SyncServerModuleRestApi: + """ + REST API endpoint used for calling from hosts when context change + happens in Workfile app. + """ + + def __init__(self, user_module, server_manager): + self._log = None + self.module = user_module + self.server_manager = server_manager + + self.prefix = "/sync_server" + + self.register() + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + + def register(self): + self.server_manager.add_route( + "POST", + self.prefix + "/add_sites_to_representations", + self.add_sites_to_representations, + ) + self.server_manager.add_route( + "GET", + self.prefix + "/files_are_processed", + self.files_are_processed, + ) + + async def add_sites_to_representations(self, request): + # Extract data from request + data = await request.json() + try: + project_name = data["project_name"] + sites = data["sites"] + representations = data["representations"] + except KeyError: + msg = ( + "Payload must contain fields 'project_name," + " 'sites' (list of names) and 'representations' (list of IDs)" + ) + self.log.error(msg) + return Response(status=400, message=msg) + + # Add all sites to each representation + for representation_id in representations: + for site in sites: + self.module.add_site( + project_name, representation_id, site, force=True + ) + + # Force timer to run immediately + self.module.reset_timer() + + return Response(status=200) + + async def files_are_processed(self, _request): + return Response( + body=bytes(self.module.sync_server_thread.files_are_processed) + ) diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index 353b39c4e1..7fd2311c2d 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -237,15 +237,13 @@ class SyncServerThread(threading.Thread): def __init__(self, module): self.log = Logger.get_logger(self.__class__.__name__) - # Event to trigger files have been processed - self.files_processed = threading.Event() - - super(SyncServerThread, self).__init__(args=(self.files_processed,)) + super(SyncServerThread, self).__init__() self.module = module self.loop = None self.is_running = False self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=3) self.timer = None + self.files_are_processed = False def run(self): self.is_running = True @@ -400,8 +398,8 @@ class SyncServerThread(threading.Thread): representation, site, error) - # Trigger files are processed - self.files_processed.set() + # Trigger files process finished + self.files_are_processed = False duration = time.time() - start_time self.log.debug("One loop took {:.2f}s".format(duration)) @@ -460,7 +458,6 @@ class SyncServerThread(threading.Thread): async def run_timer(self, delay): """Wait for 'delay' seconds to start next loop""" - self.files_processed.clear() await asyncio.sleep(delay) def reset_timer(self): @@ -469,6 +466,7 @@ class SyncServerThread(threading.Thread): if self.timer: self.timer.cancel() self.timer = None + self.files_are_processed = True def _working_sites(self, project_name): if self.module.is_project_paused(project_name): diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index a478faa9ef..7aaf42006c 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -2089,6 +2089,15 @@ class SyncServerModule(OpenPypeModule, ITrayModule): def cli(self, click_group): click_group.add_command(cli_main) + # Webserver module implementation + def webserver_initialization(self, server_manager): + """Add routes for syncs.""" + if self.tray_initialized: + from .rest_api import SyncServerModuleRestApi + self.rest_api_obj = SyncServerModuleRestApi( + self, server_manager + ) + @click.group(SyncServerModule.name, help="SyncServer module related commands.") def cli_main(): diff --git a/openpype/modules/timers_manager/rest_api.py b/openpype/modules/timers_manager/rest_api.py index 4a2e9e6575..979db9075b 100644 --- a/openpype/modules/timers_manager/rest_api.py +++ b/openpype/modules/timers_manager/rest_api.py @@ -21,7 +21,7 @@ class TimersManagerModuleRestApi: @property def log(self): if self._log is None: - self._log = Logger.get_logger(self.__ckass__.__name__) + self._log = Logger.get_logger(self.__class__.__name__) return self._log def register(self): From bca965cf9cae5eff205edd0e191288731770fa1a Mon Sep 17 00:00:00 2001 From: Felix David Date: Thu, 3 Nov 2022 18:37:13 +0100 Subject: [PATCH 161/371] lint --- openpype/hooks/pre_copy_last_published_workfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index cefc7e5d40..6bec4f7d2c 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -151,7 +151,8 @@ class CopyLastPublishedWorkfile(PreLaunchHook): import requests except Exception: self.log.warning( - "Couldn't add sites to representations ('requests' is not available)" + "Couldn't add sites to representations " + "('requests' is not available)" ) return From e359fb3d8451949ccba65204d69adb39d4a711cf Mon Sep 17 00:00:00 2001 From: Felix David Date: Fri, 4 Nov 2022 10:06:59 +0100 Subject: [PATCH 162/371] legacy compatibility --- openpype/hooks/pre_copy_last_published_workfile.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 6bec4f7d2c..f3293fa511 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -99,9 +99,11 @@ class CopyLastPublishedWorkfile(PreLaunchHook): for subset in get_subsets( project_name, asset_ids=[asset_doc["_id"]], - fields=["_id", "data.family"], + fields=["_id", "data.family", "data.families"], ) - if subset["data"]["family"] == "workfile" + if subset["data"].get("family") == "workfile" + # Legacy compatibility + or "workfile" in subset["data"].get("families", {}) ), None, ) From a57a875b071f28b0bb0ddc234a7b95516dd41a0d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 4 Nov 2022 16:00:39 +0100 Subject: [PATCH 163/371] 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 55b520128dd984ad8f9be31737f03c753c5a008e Mon Sep 17 00:00:00 2001 From: clement hector Date: Fri, 4 Nov 2022 17:45:51 +0100 Subject: [PATCH 164/371] get kitsu login to fill username burnin --- openpype/plugins/publish/extract_burnin.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 4179199317..5f6f0acc97 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -856,12 +856,17 @@ class ExtractBurnin(publish.Extractor): )) continue + kitsu_user = self.get_kitsu_user() + # Burnin values burnin_values = {} for key, value in tuple(burnin_def.items()): key_low = key.lower() if key_low in self.positions and value: - burnin_values[key_low] = value + if key_low == "bottom_left" and kitsu_user: + burnin_values[key_low] = kitsu_user + else: + burnin_values[key_low] = value # Skip processing if burnin values are not set if not burnin_values: @@ -882,6 +887,15 @@ class ExtractBurnin(publish.Extractor): return filtered_burnin_defs + def get_kitsu_user(self): + kitsu_login = os.environ.get("KITSU_LOGIN") + if not kitsu_login: + return None + + kitsu_user = kitsu_login.split("@")[0] + kitsu_user = kitsu_user.replace('.', ' ').title() + return kitsu_user + def families_filter_validation(self, families, output_families_filter): """Determine if entered families intersect with families filters. From befd6889ccf35216e1153eec5742d0b16edcceed Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 4 Nov 2022 21:25:12 +0100 Subject: [PATCH 165/371] use much simpler UI for the button --- openpype/tools/publisher/widgets/widgets.py | 112 +++++--------------- openpype/tools/publisher/window.py | 19 ++-- 2 files changed, 35 insertions(+), 96 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 444ad4c7dc..a180107380 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1660,22 +1660,19 @@ class CreateNextPageOverlay(QtWidgets.QWidget): def __init__(self, parent): super(CreateNextPageOverlay, self).__init__(parent) - + self.setCursor(QtCore.Qt.PointingHandCursor) self._arrow_color = ( get_objected_colors("bg-buttons").get_qcolor() ) - self._gradient_start_color = ( + self._bg_color = ( get_objected_colors("publisher", "tab-bg").get_qcolor() ) - self._gradient_end_color = ( - get_objected_colors("bg-inputs").get_qcolor() - ) change_anim = QtCore.QVariantAnimation() change_anim.setStartValue(0.0) change_anim.setEndValue(self.max_value) - change_anim.setDuration(200) - change_anim.setEasingCurve(QtCore.QEasingCurve.InOutQuad) + change_anim.setDuration(400) + change_anim.setEasingCurve(QtCore.QEasingCurve.OutBounce) change_anim.valueChanged.connect(self._on_anim) @@ -1731,19 +1728,10 @@ class CreateNextPageOverlay(QtWidgets.QWidget): if not self._is_visible: self.setVisible(False) - def set_handle_show_on_own(self, handle): - if self._handle_show_on_own is handle: - return - self._handle_show_on_own = handle - self._under_mouse = None - self._check_anim_timer() - def set_under_mouse(self, under_mouse): if self._under_mouse is under_mouse: return - if self._handle_show_on_own: - self._handle_show_on_own = False self._under_mouse = under_mouse self.set_increasing(under_mouse) @@ -1756,22 +1744,7 @@ class CreateNextPageOverlay(QtWidgets.QWidget): if not self.isVisible(): return - if self._handle_show_on_own: - under_mouse = self._is_under_mouse() - else: - under_mouse = self._under_mouse - - self.set_increasing(under_mouse) - - def enterEvent(self, event): - super(CreateNextPageOverlay, self).enterEvent(event) - if self._handle_show_on_own: - self._check_anim_timer() - - def leaveEvent(self, event): - super(CreateNextPageOverlay, self).leaveEvent(event) - if self._handle_show_on_own: - self._check_anim_timer() + self.set_increasing(self._under_mouse) def mousePressEvent(self, event): if event.button() == QtCore.Qt.LeftButton: @@ -1792,74 +1765,41 @@ class CreateNextPageOverlay(QtWidgets.QWidget): if self._anim_value == 0.0: painter.end() return + + painter.setClipRect(event.rect()) painter.setRenderHints( painter.Antialiasing | painter.SmoothPixmapTransform ) - pen = QtGui.QPen() - pen.setWidth(0) - painter.setPen(pen) + painter.setPen(QtCore.Qt.NoPen) + rect = QtCore.QRect(self.rect()) + rect_width = rect.width() + rect_height = rect.height() - offset = rect.width() - int( - float(rect.width()) * 0.01 * self._anim_value - ) + size = rect_width * 0.9 - pos_y = rect.center().y() - left = rect.left() + offset - top = rect.top() - # Right and bootm is pixel index - right = rect.right() + 1 - bottom = rect.bottom() + 1 - width = right - left - height = bottom - top + x_offset = (rect_width - size) * 0.5 + y_offset = (rect_height - size) * 0.5 + if self._anim_value != self.max_value: + x_offset += rect_width - (rect_width * 0.01 * self._anim_value) - q_height = height * 0.15 - - arrow_half_height = width * 0.2 - arrow_x_start = left + (width * 0.4) + arrow_half_height = size * 0.2 + arrow_x_start = x_offset + (size * 0.4) arrow_x_end = arrow_x_start + arrow_half_height - arrow_top_y_boundry = arrow_half_height + q_height - arrow_bottom_y_boundry = height - (arrow_half_height + q_height) - offset = 0 - if pos_y < arrow_top_y_boundry: - pos_y = arrow_top_y_boundry - elif pos_y > arrow_bottom_y_boundry: - pos_y = arrow_bottom_y_boundry + center_y = rect.center().y() - top_cubic_y = pos_y - q_height - bottom_cubic_y = pos_y + q_height - - path = QtGui.QPainterPath() - path.moveTo(right, top) - path.lineTo(right, bottom) - - path.cubicTo( - right, bottom, - left, bottom_cubic_y, - left, pos_y + painter.setBrush(self._bg_color) + painter.drawEllipse( + x_offset, y_offset, + size, size ) - path.cubicTo( - left, top_cubic_y, - right, top, - right, top - ) - path.closeSubpath() - - radius = height * 0.7 - focal = QtCore.QPointF(left, pos_y) - start_p = QtCore.QPointF(right - (width * 0.5), pos_y) - gradient = QtGui.QRadialGradient(start_p, radius, focal) - gradient.setColorAt(0, self._gradient_start_color) - gradient.setColorAt(1, self._gradient_end_color) - - painter.fillPath(path, gradient) src_arrow_path = QtGui.QPainterPath() - src_arrow_path.moveTo(arrow_x_start, pos_y - arrow_half_height) - src_arrow_path.lineTo(arrow_x_end, pos_y) - src_arrow_path.lineTo(arrow_x_start, pos_y + arrow_half_height) + src_arrow_path.moveTo(arrow_x_start, center_y - arrow_half_height) + src_arrow_path.lineTo(arrow_x_end, center_y) + src_arrow_path.lineTo(arrow_x_start, center_y + arrow_half_height) arrow_stroker = QtGui.QPainterPathStroker() arrow_stroker.setWidth(min(4, arrow_half_height * 0.2)) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 2063cdab96..82a2576ff4 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -228,7 +228,6 @@ class PublisherWindow(QtWidgets.QDialog): publish_frame = PublishFrame(controller, self.footer_border, self) create_overlay_button = CreateNextPageOverlay(self) - create_overlay_button.set_handle_show_on_own(False) show_timer = QtCore.QTimer() show_timer.setInterval(1) @@ -716,20 +715,20 @@ class PublisherWindow(QtWidgets.QDialog): ) def _update_create_overlay_size(self): - height = self._content_widget.height() metrics = self._create_overlay_button.fontMetrics() - width = int(metrics.height() * 3) - pos_x = self.width() - width + size = int(metrics.height() * 3) + end_pos_x = self.width() + start_pos_x = end_pos_x - size - tab_pos = self._tabs_widget.parent().mapTo( - self, self._tabs_widget.pos() + center = self._content_widget.parent().mapTo( + self, + self._content_widget.rect().center() ) - tab_height = self._tabs_widget.height() - pos_y = tab_pos.y() + tab_height + pos_y = center.y() - (size * 0.5) self._create_overlay_button.setGeometry( - pos_x, pos_y, - width, height + start_pos_x, pos_y, + size, size ) def _update_create_overlay_visibility(self, global_pos=None): From 9ec78651547738a2d2ed3cf266ebb9428b44a6b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 4 Nov 2022 21:32:09 +0100 Subject: [PATCH 166/371] removred unnecessary restart --- openpype/tools/publisher/window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 7cf3ae0da8..0daa31938d 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -362,7 +362,6 @@ class PublisherWindow(QtWidgets.QDialog): self._first_show = False self._on_first_show() - self._show_counter = 0 self._show_timer.start() def resizeEvent(self, event): From a852973e1139e5f2bba380f5c1e103ab3a817a54 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 4 Nov 2022 21:32:56 +0100 Subject: [PATCH 167/371] fix details dialog close --- openpype/tools/publisher/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 0daa31938d..281c7ad2a1 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -472,7 +472,7 @@ class PublisherWindow(QtWidgets.QDialog): ) def _on_tab_change(self, old_tab, new_tab): - if old_tab != "details": + if old_tab == "details": self._publish_details_widget.close_details_popup() if new_tab in ("create", "publish"): From a2c0e7228bff885bfca0c8df737408cd526c7d92 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 7 Nov 2022 12:24:52 +0100 Subject: [PATCH 168/371] Nuke: resolve hashes in file name only for frame token --- openpype/hosts/nuke/plugins/load/load_clip.py | 77 +++++++++++-------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 666312167f..ec4f735522 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -1,7 +1,7 @@ -import os import nuke import qargparse - +from copy import deepcopy +from openpype.lib import Logger from openpype.client import ( get_version_by_id, get_last_version_by_subset_id, @@ -22,6 +22,8 @@ from openpype.hosts.nuke.api import ( ) from openpype.hosts.nuke.api import plugin +log = Logger.get_logger(__name__) + class LoadClip(plugin.NukeLoader): """Load clip into Nuke @@ -85,24 +87,19 @@ 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(str(frame)) - basename = basename.replace(frame, "#" * padding) - return os.path.join(dirname, basename).replace("\\", "/") - def load(self, context, name, namespace, options): - repre = context["representation"] + representation = context["representation"] # reste container id so it is always unique for each instance self.reset_container_id() - is_sequence = len(repre["files"]) > 1 + is_sequence = len(representation["files"]) > 1 - filepath = self.fname.replace("\\", "/") + if is_sequence: + representation = self._representation_with_hash_in_frame( + representation + ) + filepath = get_representation_path(representation).replace("\\", "/") + log.debug("_ filepath: {}".format(filepath)) start_at_workfile = options.get( "start_at_workfile", self.options_defaults["start_at_workfile"]) @@ -112,12 +109,10 @@ class LoadClip(plugin.NukeLoader): version = context['version'] version_data = version.get("data", {}) - repre_id = repre["_id"] + repre_id = representation["_id"] - repre_cont = repre["context"] - - self.log.info("version_data: {}\n".format(version_data)) - self.log.debug( + log.info("version_data: {}\n".format(version_data)) + log.debug( "Representation id `{}` ".format(repre_id)) self.handle_start = version_data.get("handleStart", 0) @@ -132,19 +127,17 @@ class LoadClip(plugin.NukeLoader): duration = last - first first = 1 last = first + duration - 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 filepath: - self.log.warning( + log.warning( "Representation id `{}` is failing to load".format(repre_id)) return - read_name = self._get_node_name(repre) + read_name = self._get_node_name(representation) # Create the Loader with the filename path set read_node = nuke.createNode( @@ -157,7 +150,7 @@ class LoadClip(plugin.NukeLoader): read_node["file"].setValue(filepath) used_colorspace = self._set_colorspace( - read_node, version_data, repre["data"]) + read_node, version_data, representation["data"]) self._set_range_to_node(read_node, first, last, start_at_workfile) @@ -179,7 +172,7 @@ class LoadClip(plugin.NukeLoader): data_imprint[k] = version elif k == 'colorspace': - colorspace = repre["data"].get(k) + colorspace = representation["data"].get(k) colorspace = colorspace or version_data.get(k) data_imprint["db_colorspace"] = colorspace if used_colorspace: @@ -213,6 +206,20 @@ class LoadClip(plugin.NukeLoader): def switch(self, container, representation): self.update(container, representation) + def _representation_with_hash_in_frame(self, representation): + """Convert frame key value to padded hash + + Args: + representation (dict): representation data + + Returns: + dict: altered representation data + """ + representation = deepcopy(representation) + frame = representation["context"]["frame"] + representation["context"]["frame"] = "#" * len(str(frame)) + return representation + def update(self, container, representation): """Update the Loader's path @@ -225,7 +232,13 @@ class LoadClip(plugin.NukeLoader): is_sequence = len(representation["files"]) > 1 read_node = nuke.toNode(container['objectName']) + + if is_sequence: + representation = self._representation_with_hash_in_frame( + representation + ) filepath = get_representation_path(representation).replace("\\", "/") + log.debug("_ filepath: {}".format(filepath)) start_at_workfile = "start at" in read_node['frame_mode'].value() @@ -240,8 +253,6 @@ class LoadClip(plugin.NukeLoader): version_data = version_doc.get("data", {}) repre_id = representation["_id"] - repre_cont = representation["context"] - # colorspace profile colorspace = representation["data"].get("colorspace") colorspace = colorspace or version_data.get("colorspace") @@ -258,11 +269,9 @@ class LoadClip(plugin.NukeLoader): duration = last - first first = 1 last = first + duration - elif "#" not in filepath: - filepath = self._fix_path_for_knob(filepath, repre_cont) if not filepath: - self.log.warning( + log.warning( "Representation id `{}` is failing to load".format(repre_id)) return @@ -312,7 +321,7 @@ class LoadClip(plugin.NukeLoader): read_node, updated_dict ) - self.log.info( + log.info( "updated to version: {}".format(version_doc.get("name")) ) @@ -348,8 +357,8 @@ class LoadClip(plugin.NukeLoader): time_warp_nodes = version_data.get('timewarps', []) last_node = None source_id = self.get_container_id(parent_node) - self.log.info("__ source_id: {}".format(source_id)) - self.log.info("__ members: {}".format(self.get_members(parent_node))) + log.info("__ source_id: {}".format(source_id)) + log.info("__ members: {}".format(self.get_members(parent_node))) dependent_nodes = self.clear_members(parent_node) with maintained_selection(): From 833829693584f2db9c08c83a35791a20675c1185 Mon Sep 17 00:00:00 2001 From: clement hector Date: Mon, 7 Nov 2022 15:02:30 +0100 Subject: [PATCH 169/371] remove the get kitsu username function --- openpype/plugins/publish/extract_burnin.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 5f6f0acc97..4179199317 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -856,17 +856,12 @@ class ExtractBurnin(publish.Extractor): )) continue - kitsu_user = self.get_kitsu_user() - # Burnin values burnin_values = {} for key, value in tuple(burnin_def.items()): key_low = key.lower() if key_low in self.positions and value: - if key_low == "bottom_left" and kitsu_user: - burnin_values[key_low] = kitsu_user - else: - burnin_values[key_low] = value + burnin_values[key_low] = value # Skip processing if burnin values are not set if not burnin_values: @@ -887,15 +882,6 @@ class ExtractBurnin(publish.Extractor): return filtered_burnin_defs - def get_kitsu_user(self): - kitsu_login = os.environ.get("KITSU_LOGIN") - if not kitsu_login: - return None - - kitsu_user = kitsu_login.split("@")[0] - kitsu_user = kitsu_user.replace('.', ' ').title() - return kitsu_user - def families_filter_validation(self, families, output_families_filter): """Determine if entered families intersect with families filters. From c0f2de08d9285aa7eb5359d6376b9bb41c065336 Mon Sep 17 00:00:00 2001 From: clement hector Date: Mon, 7 Nov 2022 16:11:38 +0100 Subject: [PATCH 170/371] get username from kitsu login --- .../plugins/publish/collect_kitsu_username.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py new file mode 100644 index 0000000000..846adac30a --- /dev/null +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +import os +import re + +import pyblish.api + + +class CollectKitsuUsername(pyblish.api.ContextPlugin): + """Collect Kitsu username from the kitsu login""" + + order = pyblish.api.CollectorOrder + 0.499 + label = "Kitsu username" + + def process(self, context): + for instance in context: + kitsu_login = os.environ['KITSU_LOGIN'] + + if kitsu_login: + kitsu_username = kitsu_login.split("@")[0] + kitsu_username = kitsu_username.split('.') + kitsu_username = ' '.join(kitsu_username) + + new_username = re.sub('[^a-zA-Z]', ' ', kitsu_username) + + instance.data['customData'] = { + "kitsuUsername": new_username.title() + } From 4e03b94e877cf0b0c39e4a4156997c5c9c22f781 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 7 Nov 2022 21:50:02 +0100 Subject: [PATCH 171/371] 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 017ec79552eeb000edc6159960867dc781275655 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 7 Nov 2022 23:20:29 +0100 Subject: [PATCH 172/371] change colors --- 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 6c8ee3b332..ece27cd8cc 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1718,10 +1718,10 @@ class CreateNextPageOverlay(QtWidgets.QWidget): super(CreateNextPageOverlay, self).__init__(parent) self.setCursor(QtCore.Qt.PointingHandCursor) self._arrow_color = ( - get_objected_colors("bg-buttons").get_qcolor() + get_objected_colors("font").get_qcolor() ) self._bg_color = ( - get_objected_colors("publisher", "tab-bg").get_qcolor() + get_objected_colors("bg-buttons").get_qcolor() ) change_anim = QtCore.QVariantAnimation() From b75356d631f26048330e65ff24e78107dc0bbd0c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 7 Nov 2022 23:20:35 +0100 Subject: [PATCH 173/371] change easing curve --- 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 ece27cd8cc..f170992c1a 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1728,7 +1728,7 @@ class CreateNextPageOverlay(QtWidgets.QWidget): change_anim.setStartValue(0.0) change_anim.setEndValue(self.max_value) change_anim.setDuration(400) - change_anim.setEasingCurve(QtCore.QEasingCurve.OutBounce) + change_anim.setEasingCurve(QtCore.QEasingCurve.OutCubic) change_anim.valueChanged.connect(self._on_anim) From 94114d5ed0ecb4c785403c99e55a94f9b2f3cb6b Mon Sep 17 00:00:00 2001 From: clement hector Date: Tue, 8 Nov 2022 11:21:52 +0100 Subject: [PATCH 174/371] add instance name and extension checks to filter only reviewMain file --- .../kitsu/plugins/publish/integrate_kitsu_review.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index bf80095225..61d5a13660 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import os import gazu import pyblish.api @@ -31,9 +32,13 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): continue review_path = representation.get("published_path") + file_name, file_extension = os.path.splitext(review_path) + + if instance.data.get('name') != 'reviewMain' \ + or file_extension != '.mp4': + continue self.log.debug("Found review at: {}".format(review_path)) - gazu.task.add_preview( task, comment, review_path, normalize_movie=True ) From 0549bb68fd776488a63b32be7cbdf5af6c79a5d6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Nov 2022 15:28:03 +0100 Subject: [PATCH 175/371] nuke: returning logging from self --- openpype/hosts/nuke/plugins/load/load_clip.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index ec4f735522..b17356c5c7 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -1,5 +1,6 @@ import nuke import qargparse +from pprint import pformat from copy import deepcopy from openpype.lib import Logger from openpype.client import ( @@ -22,14 +23,13 @@ from openpype.hosts.nuke.api import ( ) from openpype.hosts.nuke.api import plugin -log = Logger.get_logger(__name__) - class LoadClip(plugin.NukeLoader): """Load clip into Nuke Either it is image sequence or video file. """ + log = Logger.get_logger(__name__) families = [ "source", @@ -99,7 +99,7 @@ class LoadClip(plugin.NukeLoader): representation ) filepath = get_representation_path(representation).replace("\\", "/") - log.debug("_ filepath: {}".format(filepath)) + self.log.debug("_ filepath: {}".format(filepath)) start_at_workfile = options.get( "start_at_workfile", self.options_defaults["start_at_workfile"]) @@ -111,8 +111,9 @@ class LoadClip(plugin.NukeLoader): version_data = version.get("data", {}) repre_id = representation["_id"] - log.info("version_data: {}\n".format(version_data)) - log.debug( + self.log.debug("_ version_data: {}\n".format( + pformat(version_data))) + self.log.debug( "Representation id `{}` ".format(repre_id)) self.handle_start = version_data.get("handleStart", 0) @@ -133,7 +134,7 @@ class LoadClip(plugin.NukeLoader): namespace = context['asset']['name'] if not filepath: - log.warning( + self.log.warning( "Representation id `{}` is failing to load".format(repre_id)) return @@ -238,7 +239,7 @@ class LoadClip(plugin.NukeLoader): representation ) filepath = get_representation_path(representation).replace("\\", "/") - log.debug("_ filepath: {}".format(filepath)) + self.log.debug("_ filepath: {}".format(filepath)) start_at_workfile = "start at" in read_node['frame_mode'].value() @@ -271,7 +272,7 @@ class LoadClip(plugin.NukeLoader): last = first + duration if not filepath: - log.warning( + self.log.warning( "Representation id `{}` is failing to load".format(repre_id)) return @@ -321,7 +322,7 @@ class LoadClip(plugin.NukeLoader): read_node, updated_dict ) - log.info( + self.log.info( "updated to version: {}".format(version_doc.get("name")) ) @@ -357,8 +358,10 @@ class LoadClip(plugin.NukeLoader): time_warp_nodes = version_data.get('timewarps', []) last_node = None source_id = self.get_container_id(parent_node) - log.info("__ source_id: {}".format(source_id)) - log.info("__ members: {}".format(self.get_members(parent_node))) + self.log.debug("__ source_id: {}".format(source_id)) + self.log.debug("__ members: {}".format( + self.get_members(parent_node))) + dependent_nodes = self.clear_members(parent_node) with maintained_selection(): From 3dbfa8ee5143d411adf6bbe2357966078cb819e4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Nov 2022 16:30:40 +0100 Subject: [PATCH 176/371] removed max value and use 1.0 --- openpype/tools/publisher/widgets/widgets.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index f170992c1a..7ab6294817 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1711,7 +1711,6 @@ class SubsetAttributesWidget(QtWidgets.QWidget): class CreateNextPageOverlay(QtWidgets.QWidget): - max_value = 100.0 clicked = QtCore.Signal() def __init__(self, parent): @@ -1726,7 +1725,7 @@ class CreateNextPageOverlay(QtWidgets.QWidget): change_anim = QtCore.QVariantAnimation() change_anim.setStartValue(0.0) - change_anim.setEndValue(self.max_value) + change_anim.setEndValue(1.0) change_anim.setDuration(400) change_anim.setEasingCurve(QtCore.QEasingCurve.OutCubic) @@ -1768,7 +1767,7 @@ class CreateNextPageOverlay(QtWidgets.QWidget): def _is_anim_finished(self): if self._increasing: - return self._anim_value == self.max_value + return self._anim_value == 1.0 return self._anim_value == 0.0 def _on_anim(self, value): @@ -1838,8 +1837,8 @@ class CreateNextPageOverlay(QtWidgets.QWidget): x_offset = (rect_width - size) * 0.5 y_offset = (rect_height - size) * 0.5 - if self._anim_value != self.max_value: - x_offset += rect_width - (rect_width * 0.01 * self._anim_value) + if self._anim_value != 1.0: + x_offset += rect_width - (rect_width * self._anim_value) arrow_half_height = size * 0.2 arrow_x_start = x_offset + (size * 0.4) From a76ad6035110e917e44b36e799222787bf87fd9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Nov 2022 16:41:19 +0100 Subject: [PATCH 177/371] use 'created_dt' of representation --- openpype/client/entities.py | 28 +++++++++++++++++++ .../hooks/pre_copy_last_published_workfile.py | 19 +++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 43afccf2f1..43c2874f57 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -6,6 +6,7 @@ that has project name as a context (e.g. on 'ProjectEntity'?). + We will need more specific functions doing wery specific queires really fast. """ +from datetime import datetime import re import collections @@ -1367,6 +1368,33 @@ def get_representation_parents(project_name, representation): return parents_by_repre_id[repre_id] +def get_representation_last_created_time_on_site( + representation: dict, site_name: str +) -> datetime: + """Get `created_dt` value for representation on site. + + Args: + representation (dict): Representation to get creation date of + site_name (str): Site from which to get the creation date + + Returns: + datetime: Created time of representation on site + """ + created_time = next( + ( + site.get("created_dt") + for site in representation["files"][0].get("sites", []) + if site["name"] == site_name + ), + None, + ) + if created_time: + return created_time + else: + # Use epoch as 'zero' time + return datetime.utcfromtimestamp(0) + + def get_thumbnail_id_from_source(project_name, src_type, src_id): """Receive thumbnail id from source entity. diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index f3293fa511..4eb66f6f85 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -3,6 +3,8 @@ import shutil from time import sleep from openpype.client.entities import ( get_last_version_by_subset_id, + get_representation_by_id, + get_representation_last_created_time_on_site, get_representations, get_subsets, ) @@ -158,18 +160,29 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ) return + local_site_id = get_local_site_id() requests.post( rest_api_url, json={ "project_name": project_name, - "sites": [get_local_site_id()], + "sites": [local_site_id], "representations": [str(workfile_representation["_id"])], }, ) # Wait for the download loop to end - rest_api_url = "{}/files_are_processed".format(entry_point_url) - while requests.get(rest_api_url).content: + last_created_time = get_representation_last_created_time_on_site( + workfile_representation, local_site_id + ) + while ( + last_created_time + >= get_representation_last_created_time_on_site( + get_representation_by_id( + project_name, workfile_representation["_id"] + ), + local_site_id, + ) + ): sleep(5) # Get paths From 7b1069f708dc9c0d8153fc669a1106bc6d79d030 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 9 Nov 2022 00:32:22 +0800 Subject: [PATCH 178/371] Alembic Loader as Arnold Standin --- .../hosts/maya/plugins/load/load_abc_to_standin.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index 5d6c52eac9..94bb974917 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -10,7 +10,7 @@ from openpype.settings import get_project_settings class AlembicStandinLoader(load.LoaderPlugin): """Load Alembic as Arnold Standin""" - families = ["model", "pointcache"] + families = ["animation", "model", "pointcache"] representations = ["abc"] label = "Import Alembic as Arnold Standin" @@ -31,6 +31,7 @@ class AlembicStandinLoader(load.LoaderPlugin): self.log.info("version_data: {}\n".format(version_data)) frameStart = version_data.get("frameStart", None) + frameEnd = version_data.get("frameEnd", None) asset = context["asset"]["name"] namespace = namespace or unique_namespace( @@ -64,7 +65,13 @@ class AlembicStandinLoader(load.LoaderPlugin): # Set the standin filepath cmds.setAttr(standinShape + ".dso", self.fname, type="string") - if frameStart is not None: + cmds.setAttr(standinShape + ".abcFPS", 25) + + if frameStart is None: + cmds.setAttr(standinShape + ".useFrameExtension", 0) + elif frameStart == 1 and frameEnd == 1: + cmds.setAttr(standinShape + ".useFrameExtension", 0) + else: cmds.setAttr(standinShape + ".useFrameExtension", 1) nodes = [root, standin] @@ -93,7 +100,8 @@ class AlembicStandinLoader(load.LoaderPlugin): for standin in standins: standin.dso.set(path) - standin.useFrameExtension.set(1) + standin.useFrameExtension.set(0) + standin.abcFPS.set(25) container = pm.PyNode(container["objectName"]) container.representation.set(str(representation["_id"])) From 3ad8e95ca436c4229ae8f3eadbee5b41c8326a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Nov 2022 18:57:02 +0100 Subject: [PATCH 179/371] add priority to add_site --- openpype/modules/sync_server/rest_api.py | 17 ++++++----------- openpype/modules/sync_server/sync_server.py | 4 ---- .../modules/sync_server/sync_server_module.py | 13 ++++++++++--- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/openpype/modules/sync_server/rest_api.py b/openpype/modules/sync_server/rest_api.py index b7c5d26d15..e92ddc8eee 100644 --- a/openpype/modules/sync_server/rest_api.py +++ b/openpype/modules/sync_server/rest_api.py @@ -1,4 +1,5 @@ from aiohttp.web_response import Response +from openpype.client.entities import get_representation_by_id from openpype.lib import Logger @@ -29,11 +30,6 @@ class SyncServerModuleRestApi: self.prefix + "/add_sites_to_representations", self.add_sites_to_representations, ) - self.server_manager.add_route( - "GET", - self.prefix + "/files_are_processed", - self.files_are_processed, - ) async def add_sites_to_representations(self, request): # Extract data from request @@ -54,15 +50,14 @@ class SyncServerModuleRestApi: for representation_id in representations: for site in sites: self.module.add_site( - project_name, representation_id, site, force=True + project_name, + representation_id, + site, + force=True, + priority=99, ) # Force timer to run immediately self.module.reset_timer() return Response(status=200) - - async def files_are_processed(self, _request): - return Response( - body=bytes(self.module.sync_server_thread.files_are_processed) - ) diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index 7fd2311c2d..d0a40a60ff 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -243,7 +243,6 @@ class SyncServerThread(threading.Thread): self.is_running = False self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=3) self.timer = None - self.files_are_processed = False def run(self): self.is_running = True @@ -398,8 +397,6 @@ class SyncServerThread(threading.Thread): representation, site, error) - # Trigger files process finished - self.files_are_processed = False duration = time.time() - start_time self.log.debug("One loop took {:.2f}s".format(duration)) @@ -466,7 +463,6 @@ class SyncServerThread(threading.Thread): if self.timer: self.timer.cancel() self.timer = None - self.files_are_processed = True def _working_sites(self, project_name): if self.module.is_project_paused(project_name): diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 7aaf42006c..788032180e 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -136,7 +136,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """ Start of Public API """ def add_site(self, project_name, representation_id, site_name=None, - force=False): + force=False, priority=None): """ Adds new site to representation to be synced. @@ -152,6 +152,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): representation_id (string): MongoDB _id value site_name (string): name of configured and active site force (bool): reset site if exists + priority (int): set priority Throws: SiteAlreadyPresentError - if adding already existing site and @@ -167,7 +168,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): self.reset_site_on_representation(project_name, representation_id, site_name=site_name, - force=force) + force=force, + priority=priority) def remove_site(self, project_name, representation_id, site_name, remove_local_files=False): @@ -1655,7 +1657,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): def reset_site_on_representation(self, project_name, representation_id, side=None, file_id=None, site_name=None, - remove=False, pause=None, force=False): + remove=False, pause=None, force=False, priority=None): """ Reset information about synchronization for particular 'file_id' and provider. @@ -1678,6 +1680,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): remove (bool): if True remove site altogether pause (bool or None): if True - pause, False - unpause force (bool): hard reset - currently only for add_site + priority (int): set priority Raises: SiteAlreadyPresentError - if adding already existing site and @@ -1705,6 +1708,10 @@ class SyncServerModule(OpenPypeModule, ITrayModule): elem = {"name": site_name} + # Add priority + if priority: + elem["priority"] = priority + if file_id: # reset site for particular file self._reset_site_for_file(project_name, representation_id, elem, file_id, site_name) From f19c2b3a7936b5ca0c3742ddc98f1d0fc38555ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Nov 2022 18:58:19 +0100 Subject: [PATCH 180/371] clean --- openpype/modules/sync_server/rest_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/sync_server/rest_api.py b/openpype/modules/sync_server/rest_api.py index e92ddc8eee..0c3b914833 100644 --- a/openpype/modules/sync_server/rest_api.py +++ b/openpype/modules/sync_server/rest_api.py @@ -1,5 +1,4 @@ from aiohttp.web_response import Response -from openpype.client.entities import get_representation_by_id from openpype.lib import Logger From 255d5d8b9b0c34d10b2ac17913e1369d41cc1108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Nov 2022 18:59:03 +0100 Subject: [PATCH 181/371] clean --- openpype/modules/sync_server/sync_server_module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 788032180e..94a97e9f37 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -1657,7 +1657,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): def reset_site_on_representation(self, project_name, representation_id, side=None, file_id=None, site_name=None, - remove=False, pause=None, force=False, priority=None): + remove=False, pause=None, force=False, + priority=None): """ Reset information about synchronization for particular 'file_id' and provider. From 4408ea9b02a00a0c0ff5adf4b45b2cb1f5168793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Nov 2022 19:02:29 +0100 Subject: [PATCH 182/371] sort fields --- openpype/hooks/pre_copy_last_published_workfile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 4eb66f6f85..acbc9ec1c7 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -178,7 +178,9 @@ class CopyLastPublishedWorkfile(PreLaunchHook): last_created_time >= get_representation_last_created_time_on_site( get_representation_by_id( - project_name, workfile_representation["_id"] + project_name, + workfile_representation["_id"], + fields=["files"], ), local_site_id, ) From bf6af7f7175ff1536b81e6ac8ae3f4c233271a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Nov 2022 19:04:03 +0100 Subject: [PATCH 183/371] clean --- openpype/modules/sync_server/sync_server_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 94a97e9f37..4d848958e8 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -1657,7 +1657,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): def reset_site_on_representation(self, project_name, representation_id, side=None, file_id=None, site_name=None, - remove=False, pause=None, force=False, + remove=False, pause=None, force=False, priority=None): """ Reset information about synchronization for particular 'file_id' From cf50722e1fee7c6ab227dedefc74a479713264fb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Nov 2022 21:42:26 +0100 Subject: [PATCH 184/371] flame: load with native colorspace resolved from mapping --- openpype/hosts/flame/api/plugin.py | 13 +++++++++++++ openpype/hosts/flame/plugins/load/load_clip.py | 4 ++-- .../hosts/flame/plugins/load/load_clip_batch.py | 4 ++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 092ce9d106..45fa7fd9a4 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -690,6 +690,19 @@ class ClipLoader(LoaderPlugin): ) ] + _mapping = None + + def get_native_colorspace(self, input_colorspace): + if not self._mapping: + settings = get_current_project_settings()["flame"] + mapping = settings["imageio"]["profilesMapping"]["inputs"] + self._mapping = { + input["ocioName"]: input["flameName"] + for input in mapping + } + + return self._mapping.get(input_colorspace) + class OpenClipSolver(flib.MediaInfoFile): create_new_clip = False diff --git a/openpype/hosts/flame/plugins/load/load_clip.py b/openpype/hosts/flame/plugins/load/load_clip.py index 0843dde76a..23879b923e 100644 --- a/openpype/hosts/flame/plugins/load/load_clip.py +++ b/openpype/hosts/flame/plugins/load/load_clip.py @@ -40,10 +40,10 @@ class LoadClip(opfapi.ClipLoader): clip_name = StringTemplate(self.clip_name_template).format( context["representation"]["context"]) - # TODO: settings in imageio # convert colorspace with ocio to flame mapping # in imageio flame section - colorspace = colorspace + colorspace = self.get_native_colorspace(colorspace) + self.log.info("Loading with colorspace: `{}`".format(colorspace)) # create workfile path workfile_dir = os.environ["AVALON_WORKDIR"] diff --git a/openpype/hosts/flame/plugins/load/load_clip_batch.py b/openpype/hosts/flame/plugins/load/load_clip_batch.py index 3b049b861b..2de75df116 100644 --- a/openpype/hosts/flame/plugins/load/load_clip_batch.py +++ b/openpype/hosts/flame/plugins/load/load_clip_batch.py @@ -43,10 +43,10 @@ class LoadClipBatch(opfapi.ClipLoader): clip_name = StringTemplate(self.clip_name_template).format( context["representation"]["context"]) - # TODO: settings in imageio # convert colorspace with ocio to flame mapping # in imageio flame section - colorspace = colorspace + colorspace = self.get_native_colorspace(colorspace) + self.log.info("Loading with colorspace: `{}`".format(colorspace)) # create workfile path workfile_dir = options.get("workdir") or os.environ["AVALON_WORKDIR"] From 1618c6bbf502cf9a7bdcc546f4e04735b1573e90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Nov 2022 00:29:25 +0000 Subject: [PATCH 185/371] Bump loader-utils from 1.4.0 to 1.4.1 in /website Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.1/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.1) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/yarn.lock | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index 04b9dd658b..7af15e9145 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -4782,9 +4782,9 @@ loader-runner@^4.2.0: integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== loader-utils@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + version "1.4.1" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.1.tgz#278ad7006660bccc4d2c0c1578e17c5c78d5c0e0" + integrity sha512-1Qo97Y2oKaU+Ro2xnDMR26g1BwMT29jNbem1EvcujW2jqt+j5COXyscjM7bLQkM9HaxI7pkWeW7gnI072yMI9Q== dependencies: big.js "^5.2.2" emojis-list "^3.0.0" @@ -5124,7 +5124,12 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.2.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + +minimist@^1.2.5: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== From ff565317d0a1abe63671ab5e4b62ce599a000ff7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 9 Nov 2022 08:39:52 +0800 Subject: [PATCH 186/371] Alembic Loader as Arnold Standin --- .../hosts/maya/plugins/load/load_abc_to_standin.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index 94bb974917..19e60d33da 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -1,6 +1,7 @@ import os from openpype.pipeline import ( + legacy_io, load, get_representation_path ) @@ -46,6 +47,7 @@ class AlembicStandinLoader(load.LoaderPlugin): settings = get_project_settings(os.environ['AVALON_PROJECT']) colors = settings["maya"]["load"]["colors"] + fps = legacy_io.Session["AVALON_FPS"] c = colors.get('ass') if c is not None: @@ -65,12 +67,14 @@ class AlembicStandinLoader(load.LoaderPlugin): # Set the standin filepath cmds.setAttr(standinShape + ".dso", self.fname, type="string") - cmds.setAttr(standinShape + ".abcFPS", 25) + cmds.setAttr(standinShape + ".abcFPS", float(fps)) if frameStart is None: cmds.setAttr(standinShape + ".useFrameExtension", 0) + elif frameStart == 1 and frameEnd == 1: cmds.setAttr(standinShape + ".useFrameExtension", 0) + else: cmds.setAttr(standinShape + ".useFrameExtension", 1) @@ -89,7 +93,7 @@ class AlembicStandinLoader(load.LoaderPlugin): import pymel.core as pm path = get_representation_path(representation) - + fps = legacy_io.Session["AVALON_FPS"] # Update the standin standins = list() members = pm.sets(container['objectName'], query=True) @@ -101,7 +105,7 @@ class AlembicStandinLoader(load.LoaderPlugin): for standin in standins: standin.dso.set(path) standin.useFrameExtension.set(0) - standin.abcFPS.set(25) + standin.abcFPS.set(float(fps)) container = pm.PyNode(container["objectName"]) container.representation.set(str(representation["_id"])) From 4c7733e6a74840b4a88318fa93de39fe82d227f6 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 9 Nov 2022 03:50:08 +0000 Subject: [PATCH 187/371] [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" From 8936512b67228af868e4c04a639bfb5ec3a68803 Mon Sep 17 00:00:00 2001 From: clement hector Date: Wed, 9 Nov 2022 11:17:07 +0100 Subject: [PATCH 188/371] simplifcation + check customData to avoid overriding it --- .../plugins/publish/collect_kitsu_username.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py index 846adac30a..9539518ebf 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py @@ -12,16 +12,17 @@ class CollectKitsuUsername(pyblish.api.ContextPlugin): label = "Kitsu username" def process(self, context): + kitsu_login = os.environ['KITSU_LOGIN'] + + if not kitsu_login: + return + + kitsu_username = kitsu_login.split("@")[0].replace('.', ' ') + new_username = re.sub('[^a-zA-Z]', ' ', kitsu_username).title() + for instance in context: - kitsu_login = os.environ['KITSU_LOGIN'] + # Don't override customData if it already exists + if 'customData' not in instance.data: + instance.data['customData'] = {} - if kitsu_login: - kitsu_username = kitsu_login.split("@")[0] - kitsu_username = kitsu_username.split('.') - kitsu_username = ' '.join(kitsu_username) - - new_username = re.sub('[^a-zA-Z]', ' ', kitsu_username) - - instance.data['customData'] = { - "kitsuUsername": new_username.title() - } + instance.data['customData']["kitsuUsername"] = new_username From 8feedcbc155387958b325e8eccc4c08bfad8a18e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:18:23 +0100 Subject: [PATCH 189/371] fix last version check --- .../hooks/pre_copy_last_published_workfile.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index acbc9ec1c7..96b5ccadb2 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -116,19 +116,19 @@ class CopyLastPublishedWorkfile(PreLaunchHook): return # Get workfile representation + last_version_doc = get_last_version_by_subset_id( + project_name, subset_id, fields=["_id"] + ) + if not last_version_doc: + self.log.debug("Subset does not have any versions") + return + workfile_representation = next( ( representation for representation in get_representations( project_name, - version_ids=[ - ( - get_last_version_by_subset_id( - project_name, subset_id, fields=["_id"] - ) - or {} - ).get("_id") - ], + version_ids=[last_version_doc["_id"]] ) if representation["context"]["task"]["name"] == task_name ), From 50afec52223e415c6118999f7f9ac465ed721d3e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:19:03 +0100 Subject: [PATCH 190/371] replaced 'add_sites_to_representations' with 'reset_timer' in rest api --- openpype/modules/sync_server/rest_api.py | 31 +++--------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/openpype/modules/sync_server/rest_api.py b/openpype/modules/sync_server/rest_api.py index 0c3b914833..51769cd4fb 100644 --- a/openpype/modules/sync_server/rest_api.py +++ b/openpype/modules/sync_server/rest_api.py @@ -26,36 +26,11 @@ class SyncServerModuleRestApi: def register(self): self.server_manager.add_route( "POST", - self.prefix + "/add_sites_to_representations", - self.add_sites_to_representations, + self.prefix + "/reset_timer", + self.reset_timer, ) - async def add_sites_to_representations(self, request): - # Extract data from request - data = await request.json() - try: - project_name = data["project_name"] - sites = data["sites"] - representations = data["representations"] - except KeyError: - msg = ( - "Payload must contain fields 'project_name," - " 'sites' (list of names) and 'representations' (list of IDs)" - ) - self.log.error(msg) - return Response(status=400, message=msg) - - # Add all sites to each representation - for representation_id in representations: - for site in sites: - self.module.add_site( - project_name, - representation_id, - site, - force=True, - priority=99, - ) - + async def reset_timer(self, request): # Force timer to run immediately self.module.reset_timer() From 0ca0173e9b48b8cddc6b42dc4360d7bad8649d0f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:19:15 +0100 Subject: [PATCH 191/371] added ability to rese timer from add_site --- openpype/modules/sync_server/sync_server_module.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 4d848958e8..7dc1e15322 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -136,7 +136,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """ Start of Public API """ def add_site(self, project_name, representation_id, site_name=None, - force=False, priority=None): + force=False, priority=None, reset_timer=False): """ Adds new site to representation to be synced. @@ -171,6 +171,9 @@ class SyncServerModule(OpenPypeModule, ITrayModule): force=force, priority=priority) + if reset_timer: + self.reset_timer() + def remove_site(self, project_name, representation_id, site_name, remove_local_files=False): """ From a137258b1b2a24c503b9db5e47735c9ac9f293d3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:19:32 +0100 Subject: [PATCH 192/371] 'reset_timer' can reset timer via rest api endpoint --- .../modules/sync_server/sync_server_module.py | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 7dc1e15322..26a6abbbf4 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -916,7 +916,42 @@ class SyncServerModule(OpenPypeModule, ITrayModule): In case of user's involvement (reset site), start that right away. """ - self.sync_server_thread.reset_timer() + + if not self.enabled: + return + + if self.sync_server_thread is None: + self._reset_timer_with_rest_api() + else: + self.sync_server_thread.reset_timer() + + def is_representaion_on_site( + self, project_name, representation_id, site_id + ): + # TODO implement + return False + + def _reset_timer_with_rest_api(self): + # POST to webserver sites to add to representations + webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") + if not webserver_url: + self.log.warning("Couldn't find webserver url") + return + + rest_api_url = "{}/sync_server/reset_timer".format( + webserver_url + ) + + try: + import requests + except Exception: + self.log.warning( + "Couldn't add sites to representations " + "('requests' is not available)" + ) + return + + requests.post(rest_api_url) def get_enabled_projects(self): """Returns list of projects which have SyncServer enabled.""" From 109c52809c1f599a4d93b8dc15d99f4836c03e26 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:20:21 +0100 Subject: [PATCH 193/371] updated prelaunch hook with new abilities of sync server --- .../hooks/pre_copy_last_published_workfile.py | 52 +++++-------------- 1 file changed, 12 insertions(+), 40 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 96b5ccadb2..6fd50a64d6 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -141,49 +141,21 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ).format(task_name, host_name) return - # POST to webserver sites to add to representations - webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") - if not webserver_url: - self.log.warning("Couldn't find webserver url") - return - - entry_point_url = "{}/sync_server".format(webserver_url) - rest_api_url = "{}/add_sites_to_representations".format( - entry_point_url - ) - try: - import requests - except Exception: - self.log.warning( - "Couldn't add sites to representations " - "('requests' is not available)" - ) - return - local_site_id = get_local_site_id() - requests.post( - rest_api_url, - json={ - "project_name": project_name, - "sites": [local_site_id], - "representations": [str(workfile_representation["_id"])], - }, + sync_server = self.modules_manager.get("sync_server") + sync_server.add_site( + project_name, + workfile_representation["_id"], + local_site_id, + force=True, + priority=99, + reset_timer=True ) - # Wait for the download loop to end - last_created_time = get_representation_last_created_time_on_site( - workfile_representation, local_site_id - ) - while ( - last_created_time - >= get_representation_last_created_time_on_site( - get_representation_by_id( - project_name, - workfile_representation["_id"], - fields=["files"], - ), - local_site_id, - ) + while not sync_server.is_representaion_on_site( + project_name, + workfile_representation["_id"], + local_site_id ): sleep(5) From e89051466efa9d12b0700b95711b049ab3b944ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:22:56 +0100 Subject: [PATCH 194/371] check if is sync server enabled --- openpype/hooks/pre_copy_last_published_workfile.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 6fd50a64d6..69e3d6efe4 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -37,6 +37,12 @@ class CopyLastPublishedWorkfile(PreLaunchHook): Returns: None: This is a void method. """ + + sync_server = self.modules_manager.get("sync_server") + if not sync_server or not sync_server.enabled: + self.log.deubg("Sync server module is not enabled or available") + return + # Check there is no workfile available last_workfile = self.data.get("last_workfile_path") if os.path.exists(last_workfile): @@ -142,7 +148,6 @@ class CopyLastPublishedWorkfile(PreLaunchHook): return local_site_id = get_local_site_id() - sync_server = self.modules_manager.get("sync_server") sync_server.add_site( project_name, workfile_representation["_id"], From f90e9f64128667e15e6ae375cc2a1e9dd85495a6 Mon Sep 17 00:00:00 2001 From: Thomas Fricard <51854004+friquette@users.noreply.github.com> Date: Wed, 9 Nov 2022 11:30:49 +0100 Subject: [PATCH 195/371] Update openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../modules/kitsu/plugins/publish/collect_kitsu_username.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py index 9539518ebf..896050f7e2 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_username.py @@ -12,7 +12,7 @@ class CollectKitsuUsername(pyblish.api.ContextPlugin): label = "Kitsu username" def process(self, context): - kitsu_login = os.environ['KITSU_LOGIN'] + kitsu_login = os.environ.get('KITSU_LOGIN') if not kitsu_login: return From 4b1a19f3bf868813f5f2a1749eda8277cd2882ba Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:36:03 +0100 Subject: [PATCH 196/371] removed 'get_representation_last_created_time_on_site' function --- openpype/client/entities.py | 27 ------------------- .../hooks/pre_copy_last_published_workfile.py | 2 -- 2 files changed, 29 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 43c2874f57..91d4b499b0 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1368,33 +1368,6 @@ def get_representation_parents(project_name, representation): return parents_by_repre_id[repre_id] -def get_representation_last_created_time_on_site( - representation: dict, site_name: str -) -> datetime: - """Get `created_dt` value for representation on site. - - Args: - representation (dict): Representation to get creation date of - site_name (str): Site from which to get the creation date - - Returns: - datetime: Created time of representation on site - """ - created_time = next( - ( - site.get("created_dt") - for site in representation["files"][0].get("sites", []) - if site["name"] == site_name - ), - None, - ) - if created_time: - return created_time - else: - # Use epoch as 'zero' time - return datetime.utcfromtimestamp(0) - - def get_thumbnail_id_from_source(project_name, src_type, src_id): """Receive thumbnail id from source entity. diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 69e3d6efe4..884b0f54b6 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -3,8 +3,6 @@ import shutil from time import sleep from openpype.client.entities import ( get_last_version_by_subset_id, - get_representation_by_id, - get_representation_last_created_time_on_site, get_representations, get_subsets, ) From cc7a3e8581293e7fa2c3a678a43ca9c579c40e0e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 9 Nov 2022 18:41:48 +0800 Subject: [PATCH 197/371] adding the switching on off for multipart and force muiltilayer options --- openpype/hosts/maya/api/lib_rendersettings.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 2b996702c3..2fc7547c8c 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -154,6 +154,16 @@ class RenderSettings(object): self._set_global_output_settings() cmds.setAttr("redshiftOptions.imageFormat", img_ext) + if redshift_render_presets["multilayer_exr"]: + cmds.setAttr("redshiftOptions.exrMultipart", 1) + else: + cmds.setAttr("redshiftOptions.exrMultipart", 0) + + if redshift_render_presets["force_combine"]: + cmds.setAttr("redshiftOptions.exrForceMultilayer", 1) + else: + cmds.setAttr("redshiftOptions.exrForceMultilayer", 0) + cmds.setAttr("defaultResolution.width", width) cmds.setAttr("defaultResolution.height", height) self._additional_attribs_setter(additional_options) From 3ee386543bc6b91ee7a0ab3c95424ac7955d7d98 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 9 Nov 2022 11:43:55 +0100 Subject: [PATCH 198/371] hiero: adding animated knobs also making track per subset --- .../hosts/hiero/plugins/load/load_effects.py | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/hiero/plugins/load/load_effects.py b/openpype/hosts/hiero/plugins/load/load_effects.py index fab426e58d..0819d1d1b7 100644 --- a/openpype/hosts/hiero/plugins/load/load_effects.py +++ b/openpype/hosts/hiero/plugins/load/load_effects.py @@ -13,6 +13,7 @@ from openpype.pipeline import ( get_representation_path ) from openpype.hosts.hiero import api as phiero +from openpype.lib import Logger class LoadEffects(load.LoaderPlugin): @@ -26,6 +27,8 @@ class LoadEffects(load.LoaderPlugin): icon = "cc" color = "white" + log = Logger.get_logger(__name__) + def load(self, context, name, namespace, data): """ Loading function to get the soft effects to particular read node @@ -41,7 +44,7 @@ class LoadEffects(load.LoaderPlugin): """ active_sequence = phiero.get_current_sequence() active_track = phiero.get_current_track( - active_sequence, "LoadedEffects") + active_sequence, "Loaded_{}".format(name)) # get main variables namespace = namespace or context["asset"]["name"] @@ -119,7 +122,27 @@ class LoadEffects(load.LoaderPlugin): continue try: - node[knob_name].setValue(knob_value) + # assume list means animation + # except 4 values could be RGBA or vector + if isinstance(knob_value, list) and len(knob_value) > 4: + node[knob_name].setAnimated() + for i, value in enumerate(knob_value): + if isinstance(value, list): + # list can have vector animation + for ci, cv in enumerate(value): + node[knob_name].setValueAt( + cv, + (clip_in + i), + ci + ) + else: + # list is single values + node[knob_name].setValueAt( + value, + (clip_in + i) + ) + else: + node[knob_name].setValue(knob_value) except NameError: self.log.warning("Knob: {} cannot be set".format( knob_name)) From 189dc17ce3440f9a45d68c631b19be6ddb017907 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:51:00 +0100 Subject: [PATCH 199/371] fix typo --- openpype/hooks/pre_copy_last_published_workfile.py | 2 +- openpype/modules/sync_server/sync_server_module.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 884b0f54b6..0e561334e1 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -155,7 +155,7 @@ class CopyLastPublishedWorkfile(PreLaunchHook): reset_timer=True ) - while not sync_server.is_representaion_on_site( + while not sync_server.is_representation_on_site( project_name, workfile_representation["_id"], local_site_id diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 26a6abbbf4..7228f43f84 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -925,7 +925,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): else: self.sync_server_thread.reset_timer() - def is_representaion_on_site( + def is_representation_on_site( self, project_name, representation_id, site_id ): # TODO implement From 21833283b833327b3b079604e389efe23af7f3c8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Nov 2022 12:27:07 +0100 Subject: [PATCH 200/371] added method to check if representation has all files on site --- .../modules/sync_server/sync_server_module.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 7228f43f84..dc20e37a12 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -926,10 +926,27 @@ class SyncServerModule(OpenPypeModule, ITrayModule): self.sync_server_thread.reset_timer() def is_representation_on_site( - self, project_name, representation_id, site_id + self, project_name, representation_id, site_name ): - # TODO implement - return False + """Checks if 'representation_id' has all files avail. on 'site_name'""" + representation = get_representation_by_id(project_name, + representation_id, + fields=["_id", "files"]) + if not representation: + return False + + on_site = False + for file_info in representation.get("files", []): + for site in file_info.get("sites", []): + if site["name"] != site_name: + continue + + if (site.get("progress") or site.get("error") or + not site.get("created_dt")): + return False + on_site = True + + return on_site def _reset_timer_with_rest_api(self): # POST to webserver sites to add to representations From d8c7ff2d15b912f00588771cd7aff8effef7f6df Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Nov 2022 12:48:52 +0100 Subject: [PATCH 201/371] small updates to docstrings --- openpype/modules/sync_server/sync_server_module.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index dc20e37a12..9a2ff97ed6 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -143,7 +143,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): 'project_name' must have synchronization enabled (globally or project only) - Used as a API endpoint from outside applications (Loader etc). + Used as an API endpoint from outside applications (Loader etc). Use 'force' to reset existing site. @@ -153,6 +153,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): site_name (string): name of configured and active site force (bool): reset site if exists priority (int): set priority + reset_timer (bool): if delay timer should be reset, eg. user mark + some representation to be synced manually Throws: SiteAlreadyPresentError - if adding already existing site and @@ -1601,12 +1603,12 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Args: project_name (string): name of project - force to db connection as each file might come from different collection - new_file_id (string): + new_file_id (string): only present if file synced successfully file (dictionary): info about processed file (pulled from DB) representation (dictionary): parent repr of file (from DB) site (string): label ('gdrive', 'S3') error (string): exception message - progress (float): 0-1 of progress of upload/download + progress (float): 0-0.99 of progress of upload/download priority (int): 0-100 set priority Returns: From 756bb9d85acf7d8d286eb21ca205185d0a18eed1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 9 Nov 2022 16:26:44 +0100 Subject: [PATCH 202/371] hiero: improving management of versions --- openpype/hosts/hiero/api/lib.py | 10 ++++++++-- openpype/hosts/hiero/api/pipeline.py | 21 ++++++++++++--------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index e340209207..2829fe2bf5 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -11,6 +11,7 @@ import functools import warnings import json import ast +import secrets import shutil import hiero @@ -350,6 +351,8 @@ def set_track_openpype_tag(track, data=None): Returns: hiero.core.Tag """ + hash = secrets.token_hex(nbytes=4) + data = data or {} # basic Tag's attribute @@ -367,7 +370,10 @@ def set_track_openpype_tag(track, data=None): tag = tags.update_tag(_tag, tag_data) else: # if pype tag available then update with input data - tag = tags.create_tag(self.pype_tag_name, tag_data) + tag = tags.create_tag( + "{}_{}".format(self.pype_tag_name, hash), + tag_data + ) # add it to the input track item track.addTag(tag) @@ -390,7 +396,7 @@ def get_track_openpype_tag(track): return None for tag in _tags: # return only correct tag defined by global name - if tag.name() == self.pype_tag_name: + if self.pype_tag_name in tag.name(): return tag diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index 3475bc62e4..4ab73e7d19 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -201,6 +201,15 @@ def parse_container(item, validate=True): return data_to_container(item, _data) +def _update_container_data(container, data): + for key in container: + try: + container[key] = data[key] + except KeyError: + pass + return container + + def update_container(item, data=None): """Update container data to input track_item or track's openpype tag. @@ -214,15 +223,9 @@ def update_container(item, data=None): bool: True if container was updated correctly """ - def update_container_data(container, data): - for key in container: - try: - container[key] = data[key] - except KeyError: - pass - return container data = data or {} + data = deepcopy(data) if type(item) == hiero.core.VideoTrack: # form object data for test @@ -236,14 +239,14 @@ def update_container(item, data=None): container = deepcopy(container) # update data in container - updated_container = update_container_data(container, data) + updated_container = _update_container_data(container, data) # merge updated container back to containers containers.update({object_name: updated_container}) return bool(lib.set_track_openpype_tag(item, containers)) else: container = lib.get_trackitem_openpype_data(item) - updated_container = update_container_data(container, data) + updated_container = _update_container_data(container, data) log.info("Updating container: `{}`".format(item.name())) return bool(lib.set_trackitem_openpype_tag(item, updated_container)) From f111fc3763a2ae2b43330a08c4bf2064ef646cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 9 Nov 2022 17:09:38 +0100 Subject: [PATCH 203/371] clean --- openpype/client/entities.py | 1 - openpype/modules/sync_server/rest_api.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 91d4b499b0..43afccf2f1 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -6,7 +6,6 @@ that has project name as a context (e.g. on 'ProjectEntity'?). + We will need more specific functions doing wery specific queires really fast. """ -from datetime import datetime import re import collections diff --git a/openpype/modules/sync_server/rest_api.py b/openpype/modules/sync_server/rest_api.py index 51769cd4fb..a7d9dd80b7 100644 --- a/openpype/modules/sync_server/rest_api.py +++ b/openpype/modules/sync_server/rest_api.py @@ -30,8 +30,8 @@ class SyncServerModuleRestApi: self.reset_timer, ) - async def reset_timer(self, request): - # Force timer to run immediately + async def reset_timer(self, _request): + """Force timer to run immediately.""" self.module.reset_timer() return Response(status=200) From 9996c3f1afbe2e2b3adb110382586ffefd82a3ae Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 13:44:20 +0800 Subject: [PATCH 204/371] AOV Filtering --- openpype/hosts/maya/api/lib_renderproducts.py | 3 ++- openpype/hosts/maya/api/lib_rendersettings.py | 10 ---------- .../deadline/plugins/publish/submit_publish_job.py | 10 ++++++---- vendor/configs/OpenColorIO-Configs | 1 + 4 files changed, 9 insertions(+), 15 deletions(-) create mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index cd204445b7..ef75391638 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1016,7 +1016,8 @@ class RenderProductsRedshift(ARenderProducts): # due to some AOVs still being written into separate files, # like Cryptomatte. # AOVs are merged in multi-channel file - multipart = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) + multipart = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) or \ + bool(self._get_attr("redshiftOptions.exrMultipart")) # Get Redshift Extension from image format image_format = self._get_attr("redshiftOptions.imageFormat") # integer diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 2fc7547c8c..2b996702c3 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -154,16 +154,6 @@ class RenderSettings(object): self._set_global_output_settings() cmds.setAttr("redshiftOptions.imageFormat", img_ext) - if redshift_render_presets["multilayer_exr"]: - cmds.setAttr("redshiftOptions.exrMultipart", 1) - else: - cmds.setAttr("redshiftOptions.exrMultipart", 0) - - if redshift_render_presets["force_combine"]: - cmds.setAttr("redshiftOptions.exrForceMultilayer", 1) - else: - cmds.setAttr("redshiftOptions.exrForceMultilayer", 0) - cmds.setAttr("defaultResolution.width", width) cmds.setAttr("defaultResolution.height", height) self._additional_attribs_setter(additional_options) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 35f2532c16..615be78794 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -494,12 +494,14 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): else: render_file_name = os.path.basename(col) aov_patterns = self.aov_filter - preview = match_aov_pattern(app, aov_patterns, render_file_name) - + self.log.info("aov_pattern:{}".format(aov_patterns)) # toggle preview on if multipart is on - if instance_data.get("multipartExr"): + preview = match_aov_pattern(app, aov_patterns, render_file_name) + #if instance_data.get("multipartExr"): + if "Cryptomatte" in render_file_name: # for redshift preview = True + self.log.info("preview:{}".format(preview)) new_instance = deepcopy(instance_data) new_instance["subset"] = subset_name new_instance["subsetGroup"] = group_name @@ -542,7 +544,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if new_instance.get("extendFrames", False): self._copy_extend_frames(new_instance, rep) instances.append(new_instance) - + self.log.info("instances:{}".format(instances)) return instances def _get_representations(self, instance, exp_files): diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs new file mode 160000 index 0000000000..0bb079c08b --- /dev/null +++ b/vendor/configs/OpenColorIO-Configs @@ -0,0 +1 @@ +Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 From 7e2ba84911dec742654ab07f28062c0ccbf0a731 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 13:57:15 +0800 Subject: [PATCH 205/371] AOV Filtering --- openpype/hosts/maya/api/lib_renderproducts.py | 3 ++- .../modules/deadline/plugins/publish/submit_publish_job.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index ef75391638..f89441cfc7 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1016,7 +1016,8 @@ class RenderProductsRedshift(ARenderProducts): # due to some AOVs still being written into separate files, # like Cryptomatte. # AOVs are merged in multi-channel file - multipart = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) or \ + multipart = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) \ + or \ bool(self._get_attr("redshiftOptions.exrMultipart")) # Get Redshift Extension from image format diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 615be78794..18fc769d49 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -497,7 +497,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("aov_pattern:{}".format(aov_patterns)) # toggle preview on if multipart is on preview = match_aov_pattern(app, aov_patterns, render_file_name) - #if instance_data.get("multipartExr"): if "Cryptomatte" in render_file_name: # for redshift preview = True From 252859ce0206a011828a1314e1530dbc12db5ea7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 18:03:46 +0800 Subject: [PATCH 206/371] AOV Filtering --- openpype/hosts/maya/api/lib_renderproducts.py | 6 ++++-- .../modules/deadline/plugins/publish/submit_publish_job.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index f89441cfc7..a95c1c4932 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -536,6 +536,7 @@ class RenderProductsArnold(ARenderProducts): products = [] aov_name = self._get_attr(aov, "name") + multipart = bool(self._get_attr("defaultArnoldDriver.multipart")) ai_drivers = cmds.listConnections("{}.outputs".format(aov), source=True, destination=False, @@ -589,6 +590,7 @@ class RenderProductsArnold(ARenderProducts): ext=ext, aov=aov_name, driver=ai_driver, + multipart=multipart, camera=camera) products.append(product) @@ -1016,9 +1018,9 @@ class RenderProductsRedshift(ARenderProducts): # due to some AOVs still being written into separate files, # like Cryptomatte. # AOVs are merged in multi-channel file + multipart = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) \ - or \ - bool(self._get_attr("redshiftOptions.exrMultipart")) + or bool(self._get_attr("redshiftOptions.exrMultipart")) # Get Redshift Extension from image format image_format = self._get_attr("redshiftOptions.imageFormat") # integer diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 18fc769d49..27400bb269 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -494,13 +494,13 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): else: render_file_name = os.path.basename(col) aov_patterns = self.aov_filter - self.log.info("aov_pattern:{}".format(aov_patterns)) + # toggle preview on if multipart is on preview = match_aov_pattern(app, aov_patterns, render_file_name) - if "Cryptomatte" in render_file_name: # for redshift + + if instance_data.get("multipartExr"): preview = True - self.log.info("preview:{}".format(preview)) new_instance = deepcopy(instance_data) new_instance["subset"] = subset_name new_instance["subsetGroup"] = group_name From 3cd1918f04ef5c13ab10e003064699b1659f8fb0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Nov 2022 11:23:32 +0100 Subject: [PATCH 207/371] shorter animation --- 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 a33e6e7565..71f476c4ef 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1726,7 +1726,7 @@ class CreateNextPageOverlay(QtWidgets.QWidget): change_anim = QtCore.QVariantAnimation() change_anim.setStartValue(0.0) change_anim.setEndValue(1.0) - change_anim.setDuration(400) + change_anim.setDuration(200) change_anim.setEasingCurve(QtCore.QEasingCurve.OutCubic) change_anim.valueChanged.connect(self._on_anim) From 69f4253084bc53440d26d6eb1f880f571b1a7294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 10 Nov 2022 12:07:23 +0100 Subject: [PATCH 208/371] :bug: fix regex to match semver better this fixes issues determining staging version from file name where multiple hyphens are used in pre-releas/buildmetadata part of the version string --- igniter/bootstrap_repos.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index addcbed24c..077f56d769 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -63,7 +63,8 @@ class OpenPypeVersion(semver.VersionInfo): """ staging = False path = None - _VERSION_REGEX = re.compile(r"(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?") # noqa: E501 + # this should match any string complying with https://semver.org/ + _VERSION_REGEX = re.compile(r"(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P[a-zA-Z\d\-.]*))?(?:\+(?P[a-zA-Z\d\-.]*))?") # noqa: E501 _installed_version = None def __init__(self, *args, **kwargs): @@ -211,6 +212,8 @@ class OpenPypeVersion(semver.VersionInfo): OpenPypeVersion: of detected or None. """ + # strip .zip ext if present + string = re.sub(r"\.zip$", "", string, flags=re.IGNORECASE) m = re.search(OpenPypeVersion._VERSION_REGEX, string) if not m: return None From 8eb704aeb2703c5809f6b236c7ec8f6b24fd2941 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 19:09:13 +0800 Subject: [PATCH 209/371] Alembic Loader as Arnold Standin --- .../maya/plugins/load/load_abc_to_standin.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index 19e60d33da..a192d9c357 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -28,11 +28,10 @@ class AlembicStandinLoader(load.LoaderPlugin): version = context["version"] version_data = version.get("data", {}) - + family = version["data"]["families"] self.log.info("version_data: {}\n".format(version_data)) - + self.log.info("family: {}\n".format(family)) frameStart = version_data.get("frameStart", None) - frameEnd = version_data.get("frameEnd", None) asset = context["asset"]["name"] namespace = namespace or unique_namespace( @@ -48,12 +47,14 @@ class AlembicStandinLoader(load.LoaderPlugin): settings = get_project_settings(os.environ['AVALON_PROJECT']) colors = settings["maya"]["load"]["colors"] fps = legacy_io.Session["AVALON_FPS"] - - c = colors.get('ass') + c = colors.get(family[0]) if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - c[0], c[1], c[2]) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) transform_name = label + "_ABC" @@ -72,7 +73,7 @@ class AlembicStandinLoader(load.LoaderPlugin): if frameStart is None: cmds.setAttr(standinShape + ".useFrameExtension", 0) - elif frameStart == 1 and frameEnd == 1: + elif "model" in family: cmds.setAttr(standinShape + ".useFrameExtension", 0) else: From fe47deca3ce1832f00f336051c27f5a4627964d1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 19:10:41 +0800 Subject: [PATCH 210/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index a192d9c357..8ce1aee3ac 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -30,7 +30,6 @@ class AlembicStandinLoader(load.LoaderPlugin): version_data = version.get("data", {}) family = version["data"]["families"] self.log.info("version_data: {}\n".format(version_data)) - self.log.info("family: {}\n".format(family)) frameStart = version_data.get("frameStart", None) asset = context["asset"]["name"] @@ -51,10 +50,10 @@ class AlembicStandinLoader(load.LoaderPlugin): if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - (float(c[0])/255), - (float(c[1])/255), - (float(c[2])/255) - ) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) transform_name = label + "_ABC" From bb924595c88fa0268eb3c6e3615ced6af5d6c755 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 19:11:56 +0800 Subject: [PATCH 211/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index 8ce1aee3ac..d93c85f8a4 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -50,10 +50,10 @@ class AlembicStandinLoader(load.LoaderPlugin): if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - (float(c[0])/255), - (float(c[1])/255), - (float(c[2])/255) - ) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) transform_name = label + "_ABC" From 17c3b1f96ae5fefc1bcec3bc43fcbdc8bf8bb4fc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 19:13:04 +0800 Subject: [PATCH 212/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index d93c85f8a4..dafe999d9d 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -50,9 +50,9 @@ class AlembicStandinLoader(load.LoaderPlugin): if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - (float(c[0])/255), - (float(c[1])/255), - (float(c[2])/255) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) ) transform_name = label + "_ABC" From b42160eead77da1d7e63059361c52236e1ea6385 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Nov 2022 12:14:40 +0100 Subject: [PATCH 213/371] fix button position on row inser --- openpype/tools/publisher/publish_report_viewer/window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py index 2c249d058c..646ae69e7f 100644 --- a/openpype/tools/publisher/publish_report_viewer/window.py +++ b/openpype/tools/publisher/publish_report_viewer/window.py @@ -367,6 +367,7 @@ class LoadedFilesView(QtWidgets.QTreeView): def _on_rows_inserted(self): header = self.header() header.resizeSections(header.ResizeToContents) + self._update_remove_btn() def resizeEvent(self, event): super(LoadedFilesView, self).resizeEvent(event) From 790cca2569aaad88c74f85a1412432cea64ba6f2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Nov 2022 12:14:50 +0100 Subject: [PATCH 214/371] fix issue when last item is removed --- openpype/tools/publisher/publish_report_viewer/widgets.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/publisher/publish_report_viewer/widgets.py b/openpype/tools/publisher/publish_report_viewer/widgets.py index ff388fb277..0d35ac3512 100644 --- a/openpype/tools/publisher/publish_report_viewer/widgets.py +++ b/openpype/tools/publisher/publish_report_viewer/widgets.py @@ -27,6 +27,9 @@ class PluginLoadReportModel(QtGui.QStandardItemModel): parent = self.invisibleRootItem() parent.removeRows(0, parent.rowCount()) + if report is None: + return + new_items = [] new_items_by_filepath = {} for filepath in report.crashed_plugin_paths.keys(): From 98244c77b08989959f04293a1b54aa43d3b2c67f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 19:15:11 +0800 Subject: [PATCH 215/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index dafe999d9d..d93c85f8a4 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -50,9 +50,9 @@ class AlembicStandinLoader(load.LoaderPlugin): if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - (float(c[0])/255), - (float(c[1])/255), - (float(c[2])/255) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) ) transform_name = label + "_ABC" From c5547766074b7999ea880b76705c14fba2828cd3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 19:16:10 +0800 Subject: [PATCH 216/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index d93c85f8a4..8ce1aee3ac 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -50,10 +50,10 @@ class AlembicStandinLoader(load.LoaderPlugin): if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - (float(c[0])/255), - (float(c[1])/255), - (float(c[2])/255) - ) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) transform_name = label + "_ABC" From d73ac24f59490554c20eb89af7675ae3bbcb0496 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 19:19:11 +0800 Subject: [PATCH 217/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index 8ce1aee3ac..9583063c7e 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -30,6 +30,7 @@ class AlembicStandinLoader(load.LoaderPlugin): version_data = version.get("data", {}) family = version["data"]["families"] self.log.info("version_data: {}\n".format(version_data)) + self.log.info("family: {}\n".format(family)) frameStart = version_data.get("frameStart", None) asset = context["asset"]["name"] @@ -48,12 +49,12 @@ class AlembicStandinLoader(load.LoaderPlugin): fps = legacy_io.Session["AVALON_FPS"] c = colors.get(family[0]) if c is not None: + r = (float(c[0]) / 255) + g = (float(c[1]) / 255) + b = (float(c[2]) / 255) cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - (float(c[0])/255), - (float(c[1])/255), - (float(c[2])/255) - ) + r, g, b) transform_name = label + "_ABC" From ddd4e653919adfe58b10caa857948ccf98066868 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 10 Nov 2022 13:39:01 +0100 Subject: [PATCH 218/371] hiero: unification of openpype tags --- openpype/hosts/hiero/api/lib.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 2829fe2bf5..7f0cf8149a 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -341,6 +341,11 @@ def get_track_item_tags(track_item): return returning_tag_data +def _get_tag_unique_hash(): + # sourcery skip: avoid-builtin-shadow + return secrets.token_hex(nbytes=4) + + def set_track_openpype_tag(track, data=None): """ Set openpype track tag to input track object. @@ -351,8 +356,6 @@ def set_track_openpype_tag(track, data=None): Returns: hiero.core.Tag """ - hash = secrets.token_hex(nbytes=4) - data = data or {} # basic Tag's attribute @@ -371,7 +374,10 @@ def set_track_openpype_tag(track, data=None): else: # if pype tag available then update with input data tag = tags.create_tag( - "{}_{}".format(self.pype_tag_name, hash), + "{}_{}".format( + self.pype_tag_name, + _get_tag_unique_hash() + ), tag_data ) # add it to the input track item @@ -468,7 +474,7 @@ def get_trackitem_openpype_tag(track_item): return None for tag in _tags: # return only correct tag defined by global name - if tag.name() == self.pype_tag_name: + if self.pype_tag_name in tag.name(): return tag @@ -493,13 +499,18 @@ def set_trackitem_openpype_tag(track_item, data=None): } # get available pype tag if any _tag = get_trackitem_openpype_tag(track_item) - if _tag: # it not tag then create one tag = tags.update_tag(_tag, tag_data) else: # if pype tag available then update with input data - tag = tags.create_tag(self.pype_tag_name, tag_data) + tag = tags.create_tag( + "{}_{}".format( + self.pype_tag_name, + _get_tag_unique_hash() + ), + tag_data + ) # add it to the input track item track_item.addTag(tag) From c5d3e8a45788ce03c996096f5af89df967e735a0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 10 Nov 2022 13:39:25 +0100 Subject: [PATCH 219/371] hiero: loading effects not able delete previous nodes --- openpype/hosts/hiero/plugins/load/load_effects.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/load/load_effects.py b/openpype/hosts/hiero/plugins/load/load_effects.py index 0819d1d1b7..a3fcd63b5b 100644 --- a/openpype/hosts/hiero/plugins/load/load_effects.py +++ b/openpype/hosts/hiero/plugins/load/load_effects.py @@ -188,7 +188,9 @@ class LoadEffects(load.LoaderPlugin): if loaded_stitem not in used_subtracks: continue item_to_remove = used_subtracks.pop(loaded_stitem) - item_to_remove.node()["enable"].setValue(0) + # TODO: find a way to erase nodes + self.log.debug( + "This node needs to be removed: {}".format(item_to_remove)) data_imprint = { "objectName": object_name, From 79eb997e4b7d49510615606cb6fa1c05ddec67d7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 10 Nov 2022 14:19:49 +0100 Subject: [PATCH 220/371] flame: convert color mapping to classmethod --- openpype/hosts/flame/api/plugin.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 45fa7fd9a4..9efbd5c1bc 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -692,16 +692,17 @@ class ClipLoader(LoaderPlugin): _mapping = None - def get_native_colorspace(self, input_colorspace): - if not self._mapping: + @classmethod + def get_native_colorspace(cls, input_colorspace): + if not cls._mapping: settings = get_current_project_settings()["flame"] mapping = settings["imageio"]["profilesMapping"]["inputs"] - self._mapping = { + cls._mapping = { input["ocioName"]: input["flameName"] for input in mapping } - return self._mapping.get(input_colorspace) + return cls._mapping.get(input_colorspace) class OpenClipSolver(flib.MediaInfoFile): From 0f392dd99455eec17b81a73ac7894d3286d7fa17 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 10 Nov 2022 17:11:42 +0100 Subject: [PATCH 221/371] falame: better colorspace loading --- openpype/hosts/flame/api/plugin.py | 38 ++++++++++++++++++- .../hosts/flame/plugins/load/load_clip.py | 3 +- .../flame/plugins/load/load_clip_batch.py | 2 +- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 9efbd5c1bc..26129ebaa6 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -4,13 +4,13 @@ import shutil from copy import deepcopy from xml.etree import ElementTree as ET +import qargparse from Qt import QtCore, QtWidgets -import qargparse from openpype import style -from openpype.settings import get_current_project_settings from openpype.lib import Logger from openpype.pipeline import LegacyCreator, LoaderPlugin +from openpype.settings import get_current_project_settings from . import constants from . import lib as flib @@ -692,8 +692,42 @@ class ClipLoader(LoaderPlugin): _mapping = None + def get_colorspace(self, context): + """Get colorspace name + + Look either to version data or representation data. + + Args: + context (dict): version context data + + Returns: + str: colorspace name or None + """ + version = context['version'] + version_data = version.get("data", {}) + colorspace = version_data.get( + "colorspace", None + ) + + if ( + not colorspace + or colorspace == "Unknown" + ): + colorspace = context["representation"]["data"].get( + "colorspace", None) + + return colorspace + @classmethod def get_native_colorspace(cls, input_colorspace): + """Return native colorspace name. + + Args: + input_colorspace (str | None): colorspace name + + Returns: + str: native colorspace name defined in mapping or None + """ if not cls._mapping: settings = get_current_project_settings()["flame"] mapping = settings["imageio"]["profilesMapping"]["inputs"] diff --git a/openpype/hosts/flame/plugins/load/load_clip.py b/openpype/hosts/flame/plugins/load/load_clip.py index 23879b923e..f8cb7b3e11 100644 --- a/openpype/hosts/flame/plugins/load/load_clip.py +++ b/openpype/hosts/flame/plugins/load/load_clip.py @@ -36,7 +36,8 @@ class LoadClip(opfapi.ClipLoader): version = context['version'] version_data = version.get("data", {}) version_name = version.get("name", None) - colorspace = version_data.get("colorspace", None) + colorspace = self.get_colorspace(context) + clip_name = StringTemplate(self.clip_name_template).format( context["representation"]["context"]) diff --git a/openpype/hosts/flame/plugins/load/load_clip_batch.py b/openpype/hosts/flame/plugins/load/load_clip_batch.py index 19c0ed1ef0..048ac19431 100644 --- a/openpype/hosts/flame/plugins/load/load_clip_batch.py +++ b/openpype/hosts/flame/plugins/load/load_clip_batch.py @@ -35,7 +35,7 @@ class LoadClipBatch(opfapi.ClipLoader): version = context['version'] version_data = version.get("data", {}) version_name = version.get("name", None) - colorspace = version_data.get("colorspace", None) + colorspace = self.get_colorspace(context) # in case output is not in context replace key to representation if not context["representation"]["context"].get("output"): From 9a722cb8bb8acd5deb744acfd11fab3528ae6289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 10 Nov 2022 17:30:10 +0100 Subject: [PATCH 222/371] :art: creator for online family --- .../plugins/create/create_online.py | 98 +++++++++++++++++++ .../plugins/publish/collect_online_file.py | 24 +++++ 2 files changed, 122 insertions(+) create mode 100644 openpype/hosts/traypublisher/plugins/create/create_online.py create mode 100644 openpype/hosts/traypublisher/plugins/publish/collect_online_file.py diff --git a/openpype/hosts/traypublisher/plugins/create/create_online.py b/openpype/hosts/traypublisher/plugins/create/create_online.py new file mode 100644 index 0000000000..e8092e8eaf --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/create/create_online.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +"""Creator of online files. + +Online file retain their original name and use it as subset name. To +avoid conflicts, this creator checks if subset with this name already +exists under selected asset. +""" +import copy +import os +import re +from pathlib import Path + +from openpype.client import get_subset_by_name, get_asset_by_name +from openpype.lib.attribute_definitions import FileDef +from openpype.pipeline import ( + CreatedInstance, + CreatorError +) +from openpype.pipeline.create import ( + get_subset_name, + TaskNotSetError, +) + +from openpype.hosts.traypublisher.api.plugin import TrayPublishCreator + + +class OnlineCreator(TrayPublishCreator): + """Creates instance from file and retains its original name.""" + + identifier = "io.openpype.creators.traypublisher.online" + label = "Online" + family = "online" + description = "Publish file retaining its original file name" + extensions = [".mov", ".mp4", ".mfx", ".m4v", ".mpg"] + + def get_detail_description(self): + return """# Publish batch of .mov to multiple assets. + + File names must then contain only asset name, or asset name + version. + (eg. 'chair.mov', 'chair_v001.mov', not really safe `my_chair_v001.mov` + """ + + def get_icon(self): + return "fa.file" + + def create(self, subset_name, instance_data, pre_create_data): + if not pre_create_data.get("representation_file")["filenames"]: + raise CreatorError("No files specified") + + asset = get_asset_by_name(self.project_name, instance_data["asset"]) + origin_basename = Path(pre_create_data.get( + "representation_file")["filenames"][0]).stem + + if get_subset_by_name( + self.project_name, origin_basename, asset["_id"]): + raise CreatorError(f"subset with {origin_basename} already " + "exists in selected asset") + + instance_data["originalBasename"] = origin_basename + subset_name = origin_basename + path = (Path( + pre_create_data.get( + "representation_file")["directory"] + ) / pre_create_data.get( + "representation_file")["filenames"][0]).as_posix() + + instance_data["creator_attributes"] = {"path": path} + + # Create new instance + new_instance = CreatedInstance(self.family, subset_name, + instance_data, self) + self._store_new_instance(new_instance) + + def get_pre_create_attr_defs(self): + return [ + FileDef( + "representation_file", + folders=False, + extensions=self.extensions, + allow_sequences=False, + single_item=True, + label="Representation", + ) + ] + + def get_subset_name( + self, + variant, + task_name, + asset_doc, + project_name, + host_name=None, + instance=None + ): + if instance is None: + return "{originalBasename}" + + return instance.data["subset"] diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_online_file.py b/openpype/hosts/traypublisher/plugins/publish/collect_online_file.py new file mode 100644 index 0000000000..1d173c326b --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/collect_online_file.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from pathlib import Path + + +class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): + """Collect online file and retain its file name.""" + label = "Collect online file" + families = ["online"] + hosts = ["traypublisher"] + + def process(self, instance): + file = Path(instance.data["creator_attributes"]["path"]) + + if not instance.data.get("representations"): + instance.data["representations"] = [ + { + "name": file.suffix.lstrip("."), + "ext": file.suffix.lstrip("."), + "files": file.name, + "stagingDir": file.parent.as_posix() + } + ] + From 2b8846766f8cb65f9a6f7528c15ae840849097e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 10 Nov 2022 17:30:57 +0100 Subject: [PATCH 223/371] :art: defaults for online family --- .../defaults/project_anatomy/templates.json | 8 +++++++- .../settings/defaults/project_settings/global.json | 14 +++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index 3415c4451f..0ac56a4dad 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -48,10 +48,16 @@ "file": "{originalBasename}_{@version}.{ext}", "path": "{@folder}/{@file}" }, + "online": { + "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}", + "file": "{originalBasename}<.{@frame}><_{udim}>.{ext}", + "path": "{@folder}/{@file}" + }, "__dynamic_keys_labels__": { "maya2unreal": "Maya to Unreal", "simpleUnrealTextureHero": "Simple Unreal Texture - Hero", - "simpleUnrealTexture": "Simple Unreal Texture" + "simpleUnrealTexture": "Simple Unreal Texture", + "online": "online" } } } \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 9c3f2f1e1b..0409ce802c 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -483,7 +483,19 @@ ] }, "publish": { - "template_name_profiles": [], + "template_name_profiles": [ + { + "families": [ + "online" + ], + "hosts": [ + "traypublisher" + ], + "task_types": [], + "task_names": [], + "template_name": "online" + } + ], "hero_template_name_profiles": [] } }, From bf76b73f2434668bccb60cc26dc12ba0016b0028 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 10 Nov 2022 17:54:36 +0100 Subject: [PATCH 224/371] milestone assign only if PR merged [closed] --- .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 c5a231e59e..4b52dfc30d 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, synchronize] + types: [closed] jobs: run_if_release: From 8d467b1a96426646498ffb93f2db31e2ab7344c0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Nov 2022 18:52:59 +0100 Subject: [PATCH 225/371] renamed 'CollectAvalonEntities' to 'CollectContextEntities' --- .../hosts/tvpaint/plugins/publish/collect_instance_frames.py | 2 +- openpype/hosts/tvpaint/plugins/publish/validate_marks.py | 2 +- .../plugins/publish/validate_tvpaint_workfile_data.py | 2 +- ...collect_avalon_entities.py => collect_context_entities.py} | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename openpype/plugins/publish/{collect_avalon_entities.py => collect_context_entities.py} (97%) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py b/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py index f291c363b8..d5b79758ad 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py @@ -6,7 +6,7 @@ class CollectOutputFrameRange(pyblish.api.ContextPlugin): When instances are collected context does not contain `frameStart` and `frameEnd` keys yet. They are collected in global plugin - `CollectAvalonEntities`. + `CollectContextEntities`. """ label = "Collect output frame range" order = pyblish.api.CollectorOrder diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_marks.py b/openpype/hosts/tvpaint/plugins/publish/validate_marks.py index 12d50e17ff..0030b0fd1c 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_marks.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_marks.py @@ -39,7 +39,7 @@ class ValidateMarks(pyblish.api.ContextPlugin): def get_expected_data(context): scene_mark_in = context.data["sceneMarkIn"] - # Data collected in `CollectAvalonEntities` + # Data collected in `CollectContextEntities` frame_end = context.data["frameEnd"] frame_start = context.data["frameStart"] handle_start = context.data["handleStart"] diff --git a/openpype/hosts/webpublisher/plugins/publish/validate_tvpaint_workfile_data.py b/openpype/hosts/webpublisher/plugins/publish/validate_tvpaint_workfile_data.py index a5e4868411..d8b7bb9078 100644 --- a/openpype/hosts/webpublisher/plugins/publish/validate_tvpaint_workfile_data.py +++ b/openpype/hosts/webpublisher/plugins/publish/validate_tvpaint_workfile_data.py @@ -13,7 +13,7 @@ class ValidateWorkfileData(pyblish.api.ContextPlugin): targets = ["tvpaint_worker"] def process(self, context): - # Data collected in `CollectAvalonEntities` + # Data collected in `CollectContextEntities` frame_start = context.data["frameStart"] frame_end = context.data["frameEnd"] handle_start = context.data["handleStart"] diff --git a/openpype/plugins/publish/collect_avalon_entities.py b/openpype/plugins/publish/collect_context_entities.py similarity index 97% rename from openpype/plugins/publish/collect_avalon_entities.py rename to openpype/plugins/publish/collect_context_entities.py index 3b05b6ae98..0a6072a820 100644 --- a/openpype/plugins/publish/collect_avalon_entities.py +++ b/openpype/plugins/publish/collect_context_entities.py @@ -16,11 +16,11 @@ from openpype.client import get_project, get_asset_by_name from openpype.pipeline import legacy_io, KnownPublishError -class CollectAvalonEntities(pyblish.api.ContextPlugin): +class CollectContextEntities(pyblish.api.ContextPlugin): """Collect Anatomy into Context.""" order = pyblish.api.CollectorOrder - 0.1 - label = "Collect Avalon Entities" + label = "Collect Context Entities" def process(self, context): legacy_io.install() From 292e8d45c40d4d3af25196dd8e9c979b9aa2a9f9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Nov 2022 18:53:34 +0100 Subject: [PATCH 226/371] get "asset" and "task" from context --- openpype/plugins/publish/collect_context_entities.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/collect_context_entities.py b/openpype/plugins/publish/collect_context_entities.py index 0a6072a820..31fbeb5dbd 100644 --- a/openpype/plugins/publish/collect_context_entities.py +++ b/openpype/plugins/publish/collect_context_entities.py @@ -3,6 +3,8 @@ Requires: session -> AVALON_ASSET context -> projectName + context -> asset + context -> task Provides: context -> projectEntity - Project document from database. @@ -13,20 +15,19 @@ Provides: import pyblish.api from openpype.client import get_project, get_asset_by_name -from openpype.pipeline import legacy_io, KnownPublishError +from openpype.pipeline import KnownPublishError class CollectContextEntities(pyblish.api.ContextPlugin): - """Collect Anatomy into Context.""" + """Collect entities into Context.""" order = pyblish.api.CollectorOrder - 0.1 label = "Collect Context Entities" def process(self, context): - legacy_io.install() project_name = context.data["projectName"] - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] + asset_name = context.data["asset"] + task_name = context.data["task"] project_entity = get_project(project_name) if not project_entity: From 91937c6c287f76a93bbbf13b2aa02deb72d74212 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Nov 2022 19:00:27 +0100 Subject: [PATCH 227/371] get "task" from context in anatomy context data --- openpype/plugins/publish/collect_anatomy_context_data.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 8433816908..55ce8e06f4 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -15,7 +15,6 @@ Provides: import json import pyblish.api -from openpype.pipeline import legacy_io from openpype.pipeline.template_data import get_template_data @@ -53,7 +52,7 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): asset_entity = context.data.get("assetEntity") task_name = None if asset_entity: - task_name = legacy_io.Session["AVALON_TASK"] + task_name = context.data["task"] anatomy_data = get_template_data( project_entity, asset_entity, task_name, host_name, system_settings From 81451300611b4eb7aab753ad1267848ec1965e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 11 Nov 2022 10:00:16 +0100 Subject: [PATCH 228/371] :label: fix type hint --- openpype/pipeline/create/creator_plugins.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 782534d589..bb5ce00452 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -393,8 +393,9 @@ class BaseCreator: asset_doc(dict): Asset document for which subset is created. project_name(str): Project name. host_name(str): Which host creates subset. - instance(str|None): Object of 'CreatedInstance' for which is - subset name updated. Passed only on subset name update. + instance(CreatedInstance|None): Object of 'CreatedInstance' for + which is subset name updated. Passed only on subset name + update. """ dynamic_data = self.get_dynamic_data( From 2edcb15fbb1640dd57286d83828d9bb05e908c42 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 11 Nov 2022 17:55:26 +0800 Subject: [PATCH 229/371] fixing te multipart boolean option --- openpype/hosts/maya/api/lib_renderproducts.py | 20 ++++++++++++------- .../plugins/publish/submit_publish_job.py | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index a95c1c4932..78a0a89472 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -536,7 +536,11 @@ class RenderProductsArnold(ARenderProducts): products = [] aov_name = self._get_attr(aov, "name") - multipart = bool(self._get_attr("defaultArnoldDriver.multipart")) + multipart = False + multilayer = bool(self._get_attr("defaultArnoldDriver.multipart")) + merge_AOVs = bool(self._get_attr("defaultArnoldDriver.mergeAOVs")) + if multilayer or merge_AOVs: + multipart = True ai_drivers = cmds.listConnections("{}.outputs".format(aov), source=True, destination=False, @@ -1018,9 +1022,11 @@ class RenderProductsRedshift(ARenderProducts): # due to some AOVs still being written into separate files, # like Cryptomatte. # AOVs are merged in multi-channel file - - multipart = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) \ - or bool(self._get_attr("redshiftOptions.exrMultipart")) + multipart = False + force_layer = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) + exMultipart = bool(self._get_attr("redshiftOptions.exrMultipart")) + if exMultipart or force_layer: + multipart = True # Get Redshift Extension from image format image_format = self._get_attr("redshiftOptions.imageFormat") # integer @@ -1048,7 +1054,7 @@ class RenderProductsRedshift(ARenderProducts): # Any AOVs that still get processed, like Cryptomatte # by themselves are not multipart files. - aov_multipart = not multipart + # aov_multipart = not multipart # Redshift skips rendering of masterlayer without AOV suffix # when a Beauty AOV is rendered. It overrides the main layer. @@ -1079,7 +1085,7 @@ class RenderProductsRedshift(ARenderProducts): productName=aov_light_group_name, aov=aov_name, ext=ext, - multipart=aov_multipart, + multipart=multipart, camera=camera) products.append(product) @@ -1093,7 +1099,7 @@ class RenderProductsRedshift(ARenderProducts): product = RenderProduct(productName=aov_name, aov=aov_name, ext=ext, - multipart=aov_multipart, + multipart=multipart, camera=camera) products.append(product) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 27400bb269..e87cc6beeb 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -500,7 +500,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if instance_data.get("multipartExr"): preview = True - + self.log.info("preview:{}".format(preview)) new_instance = deepcopy(instance_data) new_instance["subset"] = subset_name new_instance["subsetGroup"] = group_name From 81d09b98ffa87983d08ee8fb6e5ef83f23f231d2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 11 Nov 2022 17:58:26 +0800 Subject: [PATCH 230/371] fixing te multipart boolean option --- openpype/hosts/maya/api/lib_renderproducts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 78a0a89472..58fcd2d281 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1023,7 +1023,7 @@ class RenderProductsRedshift(ARenderProducts): # like Cryptomatte. # AOVs are merged in multi-channel file multipart = False - force_layer = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) + force_layer = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) # noqa exMultipart = bool(self._get_attr("redshiftOptions.exrMultipart")) if exMultipart or force_layer: multipart = True From 9324bf25383a773d8789a7d6debeea200b179b6f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 11 Nov 2022 18:05:42 +0800 Subject: [PATCH 231/371] fixing te multipart boolean option --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index e87cc6beeb..c1e9dd4015 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -495,8 +495,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): render_file_name = os.path.basename(col) aov_patterns = self.aov_filter - # toggle preview on if multipart is on preview = match_aov_pattern(app, aov_patterns, render_file_name) + # toggle preview on if multipart is on if instance_data.get("multipartExr"): preview = True From 84f021ffd82f00f30c9026aa7354201ba6a0e290 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 11 Nov 2022 12:18:21 +0100 Subject: [PATCH 232/371] don't close publisher window on escape button --- openpype/tools/publisher/window.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index f07995acc6..5875f7aa68 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -361,6 +361,13 @@ class PublisherWindow(QtWidgets.QDialog): super(PublisherWindow, self).resizeEvent(event) self._update_publish_frame_rect() + def keyPressEvent(self, event): + # Ignore escape button to close window + if event.key() == QtCore.Qt.Key_Escape: + event.accept() + return + super(PublisherWindow, self).keyPressEvent(event) + def _on_overlay_message(self, event): self._overlay_object.add_message( event["message"], From a09ab62eb7ab9c06dd99fb1b44d6946a30bf3d12 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 11 Nov 2022 15:07:51 +0100 Subject: [PATCH 233/371] :recycle: some tweaks --- .../plugins/create/create_online.py | 20 +++++++------------ .../plugins/publish/collect_online_file.py | 6 +++--- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_online.py b/openpype/hosts/traypublisher/plugins/create/create_online.py index e8092e8eaf..91016dc794 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_online.py +++ b/openpype/hosts/traypublisher/plugins/create/create_online.py @@ -5,9 +5,6 @@ Online file retain their original name and use it as subset name. To avoid conflicts, this creator checks if subset with this name already exists under selected asset. """ -import copy -import os -import re from pathlib import Path from openpype.client import get_subset_by_name, get_asset_by_name @@ -16,11 +13,6 @@ from openpype.pipeline import ( CreatedInstance, CreatorError ) -from openpype.pipeline.create import ( - get_subset_name, - TaskNotSetError, -) - from openpype.hosts.traypublisher.api.plugin import TrayPublishCreator @@ -31,14 +23,16 @@ class OnlineCreator(TrayPublishCreator): label = "Online" family = "online" description = "Publish file retaining its original file name" - extensions = [".mov", ".mp4", ".mfx", ".m4v", ".mpg"] + extensions = [".mov", ".mp4", ".mxf", ".m4v", ".mpg"] def get_detail_description(self): - return """# Publish batch of .mov to multiple assets. + return """# Create file retaining its original file name. - File names must then contain only asset name, or asset name + version. - (eg. 'chair.mov', 'chair_v001.mov', not really safe `my_chair_v001.mov` - """ + This will publish files using template helping to retain original + file name and that file name is used as subset name. + + Bz default it tries to guard against multiple publishes of the same + file.""" def get_icon(self): return "fa.file" diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_online_file.py b/openpype/hosts/traypublisher/plugins/publish/collect_online_file.py index 1d173c326b..459ee463aa 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_online_file.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_online_file.py @@ -3,9 +3,10 @@ import pyblish.api from pathlib import Path -class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): +class CollectOnlineFile(pyblish.api.InstancePlugin): """Collect online file and retain its file name.""" - label = "Collect online file" + label = "Collect Online File" + order = pyblish.api.CollectorOrder families = ["online"] hosts = ["traypublisher"] @@ -21,4 +22,3 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): "stagingDir": file.parent.as_posix() } ] - From b8b184b1b6c90fefcba386886554ebb32f99798c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 11 Nov 2022 15:08:02 +0100 Subject: [PATCH 234/371] :art: add validator --- .../plugins/publish/validate_online_file.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 openpype/hosts/traypublisher/plugins/publish/validate_online_file.py diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_online_file.py b/openpype/hosts/traypublisher/plugins/publish/validate_online_file.py new file mode 100644 index 0000000000..86b9334184 --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/validate_online_file.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +import pyblish.api + +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError, + OptionalPyblishPluginMixin, +) +from openpype.client import get_subset_by_name, get_asset_by_name + + +class ValidateOnlineFile(OptionalPyblishPluginMixin, + pyblish.api.InstancePlugin): + """Validate that subset doesn't exist yet.""" + label = "Validate Existing Online Files" + hosts = ["traypublisher"] + families = ["online"] + order = ValidateContentsOrder + + optional = True + + def process(self, instance): + project_name = instance.context.data["projectName"] + asset_id = instance.data["assetEntity"]["_id"] + subset = get_subset_by_name( + project_name, instance.data["subset"], asset_id) + + if subset: + raise PublishValidationError( + "Subset to be published already exists.", + title=self.label + ) From 9d304f07da447f9a5686be702d6a930c0dc774dd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 11 Nov 2022 15:08:29 +0100 Subject: [PATCH 235/371] :art: add family to integrator --- openpype/plugins/publish/integrate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 0998e643e6..401270a788 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -129,7 +129,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "mvUsd", "mvUsdComposition", "mvUsdOverride", - "simpleUnrealTexture" + "simpleUnrealTexture", + "online" ] default_template_name = "publish" From cae09e0002ba379bbd5b39ce6720e6a2ff07b1ca Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 11 Nov 2022 15:08:55 +0100 Subject: [PATCH 236/371] :label: fix docstring hints --- openpype/client/entities.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 43afccf2f1..bbef8dc65e 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -389,10 +389,11 @@ def get_subset_by_name(project_name, subset_name, asset_id, fields=None): returned if 'None' is passed. Returns: - None: If subset with specified filters was not found. - Dict: Subset document which can be reduced to specified 'fields'. - """ + Union[str, Dict]: None if subset with specified filters was not found. + or dict subset document which can be reduced to + specified 'fields'. + """ if not subset_name: return None From deac4a33d41d9914a41437e21db6ac0af81d797c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 11 Nov 2022 15:19:40 +0100 Subject: [PATCH 237/371] :rotating_light: fix hound :dog: --- .../traypublisher/plugins/create/create_online.py | 12 ++++++------ .../plugins/publish/validate_online_file.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_online.py b/openpype/hosts/traypublisher/plugins/create/create_online.py index 91016dc794..22d4b73aee 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_online.py +++ b/openpype/hosts/traypublisher/plugins/create/create_online.py @@ -30,7 +30,7 @@ class OnlineCreator(TrayPublishCreator): This will publish files using template helping to retain original file name and that file name is used as subset name. - + Bz default it tries to guard against multiple publishes of the same file.""" @@ -52,11 +52,11 @@ class OnlineCreator(TrayPublishCreator): instance_data["originalBasename"] = origin_basename subset_name = origin_basename - path = (Path( - pre_create_data.get( - "representation_file")["directory"] - ) / pre_create_data.get( - "representation_file")["filenames"][0]).as_posix() + path = ( + Path( + pre_create_data.get("representation_file")["directory"] + ) / pre_create_data.get("representation_file")["filenames"][0] + ).as_posix() instance_data["creator_attributes"] = {"path": path} diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_online_file.py b/openpype/hosts/traypublisher/plugins/publish/validate_online_file.py index 86b9334184..12b2e72ced 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_online_file.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_online_file.py @@ -6,7 +6,7 @@ from openpype.pipeline.publish import ( PublishValidationError, OptionalPyblishPluginMixin, ) -from openpype.client import get_subset_by_name, get_asset_by_name +from openpype.client import get_subset_by_name class ValidateOnlineFile(OptionalPyblishPluginMixin, From dbd00b3751eb6e9ffa378eb0b0c5985afbfdf41e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 11 Nov 2022 15:22:29 +0100 Subject: [PATCH 238/371] :rotating_light: hound fix 2 --- .../hosts/traypublisher/plugins/create/create_online.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_online.py b/openpype/hosts/traypublisher/plugins/create/create_online.py index 22d4b73aee..5a6373730d 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_online.py +++ b/openpype/hosts/traypublisher/plugins/create/create_online.py @@ -52,11 +52,7 @@ class OnlineCreator(TrayPublishCreator): instance_data["originalBasename"] = origin_basename subset_name = origin_basename - path = ( - Path( - pre_create_data.get("representation_file")["directory"] - ) / pre_create_data.get("representation_file")["filenames"][0] - ).as_posix() + path = (Path(pre_create_data.get("representation_file")["directory"]) / pre_create_data.get("representation_file")["filenames"][0]).as_posix() # noqa instance_data["creator_attributes"] = {"path": path} From cf0cba1dba0d14b60ca1bff0f9d9170aff88bb43 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 11 Nov 2022 15:43:38 +0100 Subject: [PATCH 239/371] fix variable check in collect anatomy instance data --- openpype/plugins/publish/collect_anatomy_instance_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index f67d3373d9..909b49a07d 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -188,7 +188,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): for subset_doc in subset_docs: subset_id = subset_doc["_id"] last_version_doc = last_version_docs_by_subset_id.get(subset_id) - if last_version_docs_by_subset_id is None: + if last_version_doc is None: continue asset_id = subset_doc["parent"] From fedf91934dc529e4882f31b69641060114517cac Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 12 Nov 2022 03:44:20 +0000 Subject: [PATCH 240/371] [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 81b2925fb5..1953d0d6a5 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.3" +__version__ = "3.14.7-nightly.4" From 88bf8840bd7757a19a20a460091800c0fc2741bb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 14 Nov 2022 17:57:32 +0800 Subject: [PATCH 241/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index 9583063c7e..605a492e4d 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -98,6 +98,7 @@ class AlembicStandinLoader(load.LoaderPlugin): # Update the standin standins = list() members = pm.sets(container['objectName'], query=True) + self.log.info("container:{}".format(container)) for member in members: shape = member.getShape() if (shape and shape.type() == "aiStandIn"): @@ -105,8 +106,11 @@ class AlembicStandinLoader(load.LoaderPlugin): for standin in standins: standin.dso.set(path) - standin.useFrameExtension.set(0) standin.abcFPS.set(float(fps)) + if "modelMain" in container['objectName']: + standin.useFrameExtension.set(0) + else: + standin.useFrameExtension.set(1) container = pm.PyNode(container["objectName"]) container.representation.set(str(representation["_id"])) From ae8342c57932806f05b7e13a7d82ad7d0b5c4d0b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 14 Nov 2022 18:40:20 +0800 Subject: [PATCH 242/371] aov Filtering --- vendor/configs/OpenColorIO-Configs | 1 - 1 file changed, 1 deletion(-) delete mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs deleted file mode 160000 index 0bb079c08b..0000000000 --- a/vendor/configs/OpenColorIO-Configs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 From f6495ca956c709cf33654d12c80cadedb5a272d3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 14 Nov 2022 13:30:41 +0100 Subject: [PATCH 243/371] OP-4394 - extension is lowercased in Setting and in uploaded files --- .../webpublisher/plugins/publish/collect_published_files.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 2bf097de41..ac4ade4e48 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -86,6 +86,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): first_file = task_data["files"][0] _, extension = os.path.splitext(first_file) + extension = extension.lower() family, families, tags = self._get_family( self.task_type_to_family, task_type, @@ -244,7 +245,10 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): for config in families_config: if is_sequence != config["is_sequence"]: continue - if (extension in config["extensions"] or + + lower_extensions = [ext.lower() + for ext in config.get("extensions", [])] + if (extension.lower() in lower_extensions or '' in config["extensions"]): # all extensions setting found_family = config["result_family"] break From 93b9dd7224e669c4f453dc0578ebc57ce0812c6f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 14 Nov 2022 13:40:28 +0100 Subject: [PATCH 244/371] OP-4394 - extension is lowercased in Setting and in uploaded files --- .../webpublisher/plugins/publish/collect_published_files.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index ac4ade4e48..40f4da9403 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -247,9 +247,9 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): continue lower_extensions = [ext.lower() - for ext in config.get("extensions", [])] + for ext in config.get("extensions", [''])] if (extension.lower() in lower_extensions or - '' in config["extensions"]): # all extensions setting + lower_extensions[0] == ''): # all extensions setting found_family = config["result_family"] break From 1e995ea6d921611f221c3352a958cc1d960e8884 Mon Sep 17 00:00:00 2001 From: clement hector Date: Mon, 14 Nov 2022 15:06:12 +0100 Subject: [PATCH 245/371] remove reviewMain checks --- .../kitsu/plugins/publish/integrate_kitsu_review.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index 61d5a13660..bf77f2c892 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -32,13 +32,8 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): continue review_path = representation.get("published_path") - file_name, file_extension = os.path.splitext(review_path) - - if instance.data.get('name') != 'reviewMain' \ - or file_extension != '.mp4': - continue - self.log.debug("Found review at: {}".format(review_path)) + gazu.task.add_preview( task, comment, review_path, normalize_movie=True ) From 6934b3e0ef92101871909d4b643c444001a4c478 Mon Sep 17 00:00:00 2001 From: clement hector Date: Mon, 14 Nov 2022 15:09:18 +0100 Subject: [PATCH 246/371] add an option to chose which families will be uploaded to kitsu --- .../settings/defaults/project_settings/tvpaint.json | 6 ++++++ .../projects_schema/schema_project_tvpaint.json | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 88b5a598cd..2e413f50cd 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -11,6 +11,12 @@ 255, 255, 255 + ], + "families_to_upload": [ + "review", + "renderpass", + "renderlayer", + "renderscene" ] }, "ValidateProjectSettings": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 20fe5b0855..0392c9089b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -56,6 +56,18 @@ "key": "review_bg", "label": "Review BG color", "use_alpha": false + }, + { + "type": "enum", + "key": "families_to_upload", + "label": "Families to upload", + "multiselection": true, + "enum_items": [ + {"review": "review"}, + {"renderpass": "renderPass"}, + {"renderlayer": "renderLayer"}, + {"renderscene": "renderScene"} + ] } ] }, From 4c1d1f961511e6fe9a0a87d84bc16b1b3b710011 Mon Sep 17 00:00:00 2001 From: clement hector Date: Mon, 14 Nov 2022 15:10:12 +0100 Subject: [PATCH 247/371] add review tag to the selected families in the tvpaint project settings --- openpype/hosts/tvpaint/plugins/publish/extract_sequence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 77712347bd..d8aef1ab6b 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -127,9 +127,9 @@ class ExtractSequence(pyblish.api.Extractor): output_frame_start ) - # Fill tags and new families + # Fill tags and new families from project settings tags = [] - if family_lowered in ("review", "renderlayer", "renderscene"): + if family_lowered in self.families_to_upload: tags.append("review") # Sequence of one frame From fd08bbf17026aa3be3045804503342ce5f9a02c7 Mon Sep 17 00:00:00 2001 From: clement hector Date: Mon, 14 Nov 2022 15:13:11 +0100 Subject: [PATCH 248/371] remove useless import --- openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index bf77f2c892..e5e6439439 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import os import gazu import pyblish.api From 20ea1c8212a5d9226d162ce5532ad641ad9a2b73 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 14 Nov 2022 18:27:11 +0100 Subject: [PATCH 249/371] ignore case sensitivity of extension in files widget --- openpype/lib/attribute_definitions.py | 7 +++++++ openpype/tools/attribute_defs/files_widget.py | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 589a4ef9ab..6baeaec045 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -541,6 +541,13 @@ class FileDefItem(object): return ext return None + @property + def lower_ext(self): + ext = self.ext + if ext is not None: + return ext.lower() + return ext + @property def is_dir(self): if self.is_empty: diff --git a/openpype/tools/attribute_defs/files_widget.py b/openpype/tools/attribute_defs/files_widget.py index 3f1e6a34e1..738e50ba07 100644 --- a/openpype/tools/attribute_defs/files_widget.py +++ b/openpype/tools/attribute_defs/files_widget.py @@ -349,7 +349,7 @@ class FilesModel(QtGui.QStandardItemModel): 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.lower_ext, EXT_ROLE) item.setData(file_item.is_dir, IS_DIR_ROLE) item.setData(file_item.is_sequence, IS_SEQUENCE_ROLE) @@ -463,7 +463,7 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel): for filepath in filepaths: if os.path.isfile(filepath): _, ext = os.path.splitext(filepath) - if ext in self._allowed_extensions: + if ext.lower() in self._allowed_extensions: return True elif self._allow_folders: @@ -475,7 +475,7 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel): for filepath in filepaths: if os.path.isfile(filepath): _, ext = os.path.splitext(filepath) - if ext in self._allowed_extensions: + if ext.lower() in self._allowed_extensions: filtered_paths.append(filepath) elif self._allow_folders: From 088e7507b3144a7deb3cafe4b51286649e446079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 15 Nov 2022 09:06:09 +0100 Subject: [PATCH 250/371] logging format --- .../hooks/pre_copy_last_published_workfile.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 0e561334e1..44144e5fff 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -75,16 +75,20 @@ class CopyLastPublishedWorkfile(PreLaunchHook): self.log.info( ( "Seems like old version of settings is used." - ' Can\'t access custom templates in host "{}".' - ).format(host_name) + ' Can\'t access custom templates in host "{}".'.format( + host_name + ) + ) ) return elif use_last_published_workfile is False: self.log.info( ( 'Project "{}" has turned off to use last published' - ' workfile as first workfile for host "{}"' - ).format(project_name, host_name) + ' workfile as first workfile for host "{}"'.format( + project_name, host_name + ) + ) ) return @@ -114,8 +118,8 @@ class CopyLastPublishedWorkfile(PreLaunchHook): None, ) if not subset_id: - self.log.debug('No any workfile for asset "{}".').format( - asset_doc["name"] + self.log.debug( + 'No any workfile for asset "{}".'.format(asset_doc["name"]) ) return @@ -131,8 +135,7 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ( representation for representation in get_representations( - project_name, - version_ids=[last_version_doc["_id"]] + project_name, version_ids=[last_version_doc["_id"]] ) if representation["context"]["task"]["name"] == task_name ), @@ -141,8 +144,10 @@ class CopyLastPublishedWorkfile(PreLaunchHook): if not workfile_representation: self.log.debug( - 'No published workfile for task "{}" and host "{}".' - ).format(task_name, host_name) + 'No published workfile for task "{}" and host "{}".'.format( + task_name, host_name + ) + ) return local_site_id = get_local_site_id() @@ -152,13 +157,11 @@ class CopyLastPublishedWorkfile(PreLaunchHook): local_site_id, force=True, priority=99, - reset_timer=True + reset_timer=True, ) while not sync_server.is_representation_on_site( - project_name, - workfile_representation["_id"], - local_site_id + project_name, workfile_representation["_id"], local_site_id ): sleep(5) From 3b9662f97cfb19be8a44b95e16fa50785e22ea21 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 15 Nov 2022 13:26:22 +0100 Subject: [PATCH 251/371] added settings for validate frame range in tray publisher --- .../project_settings/traypublisher.json | 7 +++++ .../schema_project_traypublisher.json | 18 +++++++++++++ .../schemas/template_validate_plugin.json | 26 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/template_validate_plugin.json diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 5db2a79772..e99b96b8c4 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -303,5 +303,12 @@ "extensions": [ ".mov" ] + }, + "publish": { + "ValidateFrameRange": { + "enabled": true, + "optional": true, + "active": true + } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 7c61aeed50..faa5033d2a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -311,6 +311,24 @@ "object_type": "text" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "publish", + "label": "Publish plugins", + "children": [ + { + "type": "schema_template", + "name": "template_validate_plugin", + "template_data": [ + { + "key": "ValidateFrameRange", + "label": "Validate frame range" + } + ] + } + ] } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_validate_plugin.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_validate_plugin.json new file mode 100644 index 0000000000..b57cad6719 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_validate_plugin.json @@ -0,0 +1,26 @@ +[ + { + "type": "dict", + "collapsible": true, + "key": "{key}", + "label": "{label}", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + } + ] + } +] From f18efd29b2aebe89f3cc8dbbbf03dc9bdfdff5b2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Nov 2022 17:36:30 +0100 Subject: [PATCH 252/371] OP-4394 - fix - lowercase extension everywhere Without it it would be stored in DB uppercased and final name would also be uppercased. --- .../webpublisher/plugins/publish/collect_published_files.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 40f4da9403..265e78a6c7 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -181,6 +181,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): def _get_single_repre(self, task_dir, files, tags): _, ext = os.path.splitext(files[0]) + ext = ext.lower() repre_data = { "name": ext[1:], "ext": ext[1:], @@ -200,6 +201,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): frame_start = list(collections[0].indexes)[0] frame_end = list(collections[0].indexes)[-1] ext = collections[0].tail + ext = ext.lower() repre_data = { "frameStart": frame_start, "frameEnd": frame_end, From fbd7531a311d1a0287c45babb12a7b029cd50a7d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 15 Nov 2022 18:42:46 +0100 Subject: [PATCH 253/371] change label of stopped publishing --- openpype/tools/publisher/widgets/validations_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/validations_widget.py b/openpype/tools/publisher/widgets/validations_widget.py index 8c483e8088..935a12bc73 100644 --- a/openpype/tools/publisher/widgets/validations_widget.py +++ b/openpype/tools/publisher/widgets/validations_widget.py @@ -511,7 +511,7 @@ class ValidationsWidget(QtWidgets.QFrame): ) # After success publishing publish_started_widget = ValidationArtistMessage( - "Publishing went smoothly", self + "So far so good", self ) # After success publishing publish_stop_ok_widget = ValidationArtistMessage( From 4dd276fc4682f379bc1eaf2b088c24252920eeef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Nov 2022 23:23:26 +0000 Subject: [PATCH 254/371] Bump loader-utils from 1.4.1 to 1.4.2 in /website Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.1 to 1.4.2. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v1.4.1...v1.4.2) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index 177a4a3802..220a489dfa 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -4812,9 +4812,9 @@ loader-runner@^4.2.0: integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== loader-utils@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.1.tgz#278ad7006660bccc4d2c0c1578e17c5c78d5c0e0" - integrity sha512-1Qo97Y2oKaU+Ro2xnDMR26g1BwMT29jNbem1EvcujW2jqt+j5COXyscjM7bLQkM9HaxI7pkWeW7gnI072yMI9Q== + version "1.4.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" + integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== dependencies: big.js "^5.2.2" emojis-list "^3.0.0" From ae51001ee89812ff0c34a1175318983ad708380b Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 16 Nov 2022 03:40:10 +0000 Subject: [PATCH 255/371] [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 1953d0d6a5..268f33083a 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.4" +__version__ = "3.14.7-nightly.5" From 0645089ad61f0a893ce717a5cf4574ca81cd8ef2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 16 Nov 2022 10:38:08 +0100 Subject: [PATCH 256/371] size of button is fully defined by style --- openpype/style/style.css | 4 ++++ openpype/tools/publisher/window.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index 887c044dae..a7a48cdb9d 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1126,6 +1126,10 @@ ValidationArtistMessage QLabel { background: transparent; } +CreateNextPageOverlay { + font-size: 32pt; +} + /* Settings - NOT USED YET - we need to define font family for settings UI */ diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 281c7ad2a1..febf55b919 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -716,7 +716,7 @@ class PublisherWindow(QtWidgets.QDialog): def _update_create_overlay_size(self): metrics = self._create_overlay_button.fontMetrics() - size = int(metrics.height() * 3) + size = int(metrics.height()) end_pos_x = self.width() start_pos_x = end_pos_x - size From 20dacc342b5b4f5ff407fd616d0dc7818c551844 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 16 Nov 2022 10:40:22 +0100 Subject: [PATCH 257/371] change style of button --- openpype/tools/publisher/widgets/widgets.py | 17 +++++++++-------- openpype/tools/publisher/window.py | 17 ++++++++++++----- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 71f476c4ef..ce3d91ce63 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1832,23 +1832,24 @@ class CreateNextPageOverlay(QtWidgets.QWidget): rect = QtCore.QRect(self.rect()) rect_width = rect.width() rect_height = rect.height() + radius = rect_width * 0.2 - size = rect_width * 0.9 - - x_offset = (rect_width - size) * 0.5 - y_offset = (rect_height - size) * 0.5 + x_offset = 0 + y_offset = 0 if self._anim_value != 1.0: x_offset += rect_width - (rect_width * self._anim_value) - arrow_half_height = size * 0.2 - arrow_x_start = x_offset + (size * 0.4) + arrow_height = rect_height * 0.4 + arrow_half_height = arrow_height * 0.5 + arrow_x_start = x_offset + ((rect_width - arrow_half_height) * 0.5) arrow_x_end = arrow_x_start + arrow_half_height center_y = rect.center().y() painter.setBrush(self._bg_color) - painter.drawEllipse( + painter.drawRoundedRect( x_offset, y_offset, - size, size + rect_width + radius, rect_height, + radius, radius ) src_arrow_path = QtGui.QPainterPath() diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index febf55b919..de26630312 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -257,7 +257,9 @@ class PublisherWindow(QtWidgets.QDialog): publish_btn.clicked.connect(self._on_publish_clicked) publish_frame.details_page_requested.connect(self._go_to_details_tab) - create_overlay_button.clicked.connect(self._go_to_publish_tab) + create_overlay_button.clicked.connect( + self._on_create_overlay_button_click + ) controller.event_system.add_callback( "instances.refresh.finished", self._on_instances_refresh @@ -471,6 +473,10 @@ class PublisherWindow(QtWidgets.QDialog): self._help_dialog.width(), self._help_dialog.height() ) + def _on_create_overlay_button_click(self): + self._create_overlay_button.set_under_mouse(False) + self._go_to_publish_tab() + def _on_tab_change(self, old_tab, new_tab): if old_tab == "details": self._publish_details_widget.close_details_popup() @@ -716,19 +722,20 @@ class PublisherWindow(QtWidgets.QDialog): def _update_create_overlay_size(self): metrics = self._create_overlay_button.fontMetrics() - size = int(metrics.height()) + height = int(metrics.height()) + width = int(height * 0.7) end_pos_x = self.width() - start_pos_x = end_pos_x - size + start_pos_x = end_pos_x - width center = self._content_widget.parent().mapTo( self, self._content_widget.rect().center() ) - pos_y = center.y() - (size * 0.5) + pos_y = center.y() - (height * 0.5) self._create_overlay_button.setGeometry( start_pos_x, pos_y, - size, size + width, height ) def _update_create_overlay_visibility(self, global_pos=None): From 91a4a06ab6e9f9e9a8c6378aeebe014fbe6c9a21 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 16 Nov 2022 11:32:26 +0100 Subject: [PATCH 258/371] change maximum number of frame start/end and clip in/out in anatomy settings --- .../schemas/schema_anatomy_attributes.json | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json index a2a566da0e..3667c9d5d8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json @@ -16,22 +16,26 @@ { "type": "number", "key": "frameStart", - "label": "Frame Start" + "label": "Frame Start", + "maximum": 999999999 }, { "type": "number", "key": "frameEnd", - "label": "Frame End" + "label": "Frame End", + "maximum": 999999999 }, { "type": "number", "key": "clipIn", - "label": "Clip In" + "label": "Clip In", + "maximum": 999999999 }, { "type": "number", "key": "clipOut", - "label": "Clip Out" + "label": "Clip Out", + "maximum": 999999999 }, { "type": "number", From 33656d00550c64f50d9e42088c389a43315cd905 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 16 Nov 2022 11:32:43 +0100 Subject: [PATCH 259/371] project manager has higher max numbers --- openpype/tools/project_manager/project_manager/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index cca892ef72..8d1fe54e83 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -28,7 +28,7 @@ class NameDef: class NumberDef: def __init__(self, minimum=None, maximum=None, decimals=None): self.minimum = 0 if minimum is None else minimum - self.maximum = 999999 if maximum is None else maximum + self.maximum = 999999999 if maximum is None else maximum self.decimals = 0 if decimals is None else decimals From 8c6abf1c8faeea4ff5ecafa6c0e5dbfb22ce06cb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 16 Nov 2022 11:32:59 +0100 Subject: [PATCH 260/371] remove duplicated key --- openpype/tools/settings/settings/constants.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/settings/settings/constants.py b/openpype/tools/settings/settings/constants.py index d98d18c8bf..23526e4de9 100644 --- a/openpype/tools/settings/settings/constants.py +++ b/openpype/tools/settings/settings/constants.py @@ -24,7 +24,6 @@ __all__ = ( "SETTINGS_PATH_KEY", "ROOT_KEY", - "SETTINGS_PATH_KEY", "VALUE_KEY", "SAVE_TIME_KEY", "PROJECT_NAME_KEY", From e11815b663d3910032fc6f2ec492df857ce91590 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 16 Nov 2022 14:41:35 +0100 Subject: [PATCH 261/371] OP-4394 - safer handling of Settings extensions Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../plugins/publish/collect_published_files.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 265e78a6c7..181f8b4ab7 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -247,11 +247,17 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): for config in families_config: if is_sequence != config["is_sequence"]: continue + extensions = config.get("extensions") or [] + lower_extensions = set() + for ext in extensions: + if ext: + ext = ext.lower() + if ext.startswith("."): + ext = ext[1:] + lower_extensions.add(ext) - lower_extensions = [ext.lower() - for ext in config.get("extensions", [''])] - if (extension.lower() in lower_extensions or - lower_extensions[0] == ''): # all extensions setting + # all extensions setting + if not lower_extensions or extension in lower_extensions: found_family = config["result_family"] break From ed7795061f946ca71e7c3b09977c68525e3cd24c Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 12 Nov 2022 03:44:20 +0000 Subject: [PATCH 262/371] [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 81b2925fb5..1953d0d6a5 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.3" +__version__ = "3.14.7-nightly.4" From f9732a8385a75384d91a424ac007285a082c9a2a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Nov 2022 18:52:59 +0100 Subject: [PATCH 263/371] renamed 'CollectAvalonEntities' to 'CollectContextEntities' --- .../hosts/tvpaint/plugins/publish/collect_instance_frames.py | 2 +- openpype/hosts/tvpaint/plugins/publish/validate_marks.py | 2 +- .../plugins/publish/validate_tvpaint_workfile_data.py | 2 +- ...collect_avalon_entities.py => collect_context_entities.py} | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename openpype/plugins/publish/{collect_avalon_entities.py => collect_context_entities.py} (97%) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py b/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py index f291c363b8..d5b79758ad 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py @@ -6,7 +6,7 @@ class CollectOutputFrameRange(pyblish.api.ContextPlugin): When instances are collected context does not contain `frameStart` and `frameEnd` keys yet. They are collected in global plugin - `CollectAvalonEntities`. + `CollectContextEntities`. """ label = "Collect output frame range" order = pyblish.api.CollectorOrder diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_marks.py b/openpype/hosts/tvpaint/plugins/publish/validate_marks.py index 12d50e17ff..0030b0fd1c 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_marks.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_marks.py @@ -39,7 +39,7 @@ class ValidateMarks(pyblish.api.ContextPlugin): def get_expected_data(context): scene_mark_in = context.data["sceneMarkIn"] - # Data collected in `CollectAvalonEntities` + # Data collected in `CollectContextEntities` frame_end = context.data["frameEnd"] frame_start = context.data["frameStart"] handle_start = context.data["handleStart"] diff --git a/openpype/hosts/webpublisher/plugins/publish/validate_tvpaint_workfile_data.py b/openpype/hosts/webpublisher/plugins/publish/validate_tvpaint_workfile_data.py index a5e4868411..d8b7bb9078 100644 --- a/openpype/hosts/webpublisher/plugins/publish/validate_tvpaint_workfile_data.py +++ b/openpype/hosts/webpublisher/plugins/publish/validate_tvpaint_workfile_data.py @@ -13,7 +13,7 @@ class ValidateWorkfileData(pyblish.api.ContextPlugin): targets = ["tvpaint_worker"] def process(self, context): - # Data collected in `CollectAvalonEntities` + # Data collected in `CollectContextEntities` frame_start = context.data["frameStart"] frame_end = context.data["frameEnd"] handle_start = context.data["handleStart"] diff --git a/openpype/plugins/publish/collect_avalon_entities.py b/openpype/plugins/publish/collect_context_entities.py similarity index 97% rename from openpype/plugins/publish/collect_avalon_entities.py rename to openpype/plugins/publish/collect_context_entities.py index 3b05b6ae98..0a6072a820 100644 --- a/openpype/plugins/publish/collect_avalon_entities.py +++ b/openpype/plugins/publish/collect_context_entities.py @@ -16,11 +16,11 @@ from openpype.client import get_project, get_asset_by_name from openpype.pipeline import legacy_io, KnownPublishError -class CollectAvalonEntities(pyblish.api.ContextPlugin): +class CollectContextEntities(pyblish.api.ContextPlugin): """Collect Anatomy into Context.""" order = pyblish.api.CollectorOrder - 0.1 - label = "Collect Avalon Entities" + label = "Collect Context Entities" def process(self, context): legacy_io.install() From 910b7d7120be3982548bf8913410cbb71669f0e9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Nov 2022 18:53:34 +0100 Subject: [PATCH 264/371] get "asset" and "task" from context --- openpype/plugins/publish/collect_context_entities.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/collect_context_entities.py b/openpype/plugins/publish/collect_context_entities.py index 0a6072a820..31fbeb5dbd 100644 --- a/openpype/plugins/publish/collect_context_entities.py +++ b/openpype/plugins/publish/collect_context_entities.py @@ -3,6 +3,8 @@ Requires: session -> AVALON_ASSET context -> projectName + context -> asset + context -> task Provides: context -> projectEntity - Project document from database. @@ -13,20 +15,19 @@ Provides: import pyblish.api from openpype.client import get_project, get_asset_by_name -from openpype.pipeline import legacy_io, KnownPublishError +from openpype.pipeline import KnownPublishError class CollectContextEntities(pyblish.api.ContextPlugin): - """Collect Anatomy into Context.""" + """Collect entities into Context.""" order = pyblish.api.CollectorOrder - 0.1 label = "Collect Context Entities" def process(self, context): - legacy_io.install() project_name = context.data["projectName"] - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] + asset_name = context.data["asset"] + task_name = context.data["task"] project_entity = get_project(project_name) if not project_entity: From 017720d754d5912d4df5a233168c64f3caccd56f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Nov 2022 19:00:27 +0100 Subject: [PATCH 265/371] get "task" from context in anatomy context data --- openpype/plugins/publish/collect_anatomy_context_data.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 8433816908..55ce8e06f4 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -15,7 +15,6 @@ Provides: import json import pyblish.api -from openpype.pipeline import legacy_io from openpype.pipeline.template_data import get_template_data @@ -53,7 +52,7 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): asset_entity = context.data.get("assetEntity") task_name = None if asset_entity: - task_name = legacy_io.Session["AVALON_TASK"] + task_name = context.data["task"] anatomy_data = get_template_data( project_entity, asset_entity, task_name, host_name, system_settings From db6bfcb1ee978a3aa77ed00b1dbc2462715a187b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Jul 2022 03:59:40 +0000 Subject: [PATCH 266/371] Bump terser from 5.10.0 to 5.14.2 in /website Bumps [terser](https://github.com/terser/terser) from 5.10.0 to 5.14.2. - [Release notes](https://github.com/terser/terser/releases) - [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md) - [Commits](https://github.com/terser/terser/commits) --- updated-dependencies: - dependency-name: terser dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/yarn.lock | 64 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index 7af15e9145..177a4a3802 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -1543,15 +1543,37 @@ dependencies: "@hapi/hoek" "^9.0.0" +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/resolve-uri@^3.0.3": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c" - integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew== + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" "@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.11" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" - integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== "@jridgewell/trace-mapping@^0.3.0": version "0.3.4" @@ -1561,6 +1583,14 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.14" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" + integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@mdx-js/mdx@1.6.22", "@mdx-js/mdx@^1.6.21": version "1.6.22" resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba" @@ -2140,10 +2170,10 @@ acorn@^6.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== -acorn@^8.0.4, acorn@^8.4.1: - version "8.7.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" - integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== +acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== address@^1.0.1, address@^1.1.2: version "1.1.2" @@ -6843,11 +6873,6 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@~0.7.2: - version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - sourcemap-codec@^1.4.4: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" @@ -7053,12 +7078,13 @@ terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.4: terser "^5.7.2" terser@^5.10.0, terser@^5.7.2: - version "5.10.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.10.0.tgz#b86390809c0389105eb0a0b62397563096ddafcc" - integrity sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA== + version "5.14.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10" + integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA== dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" commander "^2.20.0" - source-map "~0.7.2" source-map-support "~0.5.20" text-table@^0.2.0: From ff760342c7719238e9ae06f9bb23c8747cabb615 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 14 Nov 2022 18:27:11 +0100 Subject: [PATCH 267/371] ignore case sensitivity of extension in files widget --- openpype/lib/attribute_definitions.py | 7 +++++++ openpype/tools/attribute_defs/files_widget.py | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 589a4ef9ab..6baeaec045 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -541,6 +541,13 @@ class FileDefItem(object): return ext return None + @property + def lower_ext(self): + ext = self.ext + if ext is not None: + return ext.lower() + return ext + @property def is_dir(self): if self.is_empty: diff --git a/openpype/tools/attribute_defs/files_widget.py b/openpype/tools/attribute_defs/files_widget.py index 3f1e6a34e1..738e50ba07 100644 --- a/openpype/tools/attribute_defs/files_widget.py +++ b/openpype/tools/attribute_defs/files_widget.py @@ -349,7 +349,7 @@ class FilesModel(QtGui.QStandardItemModel): 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.lower_ext, EXT_ROLE) item.setData(file_item.is_dir, IS_DIR_ROLE) item.setData(file_item.is_sequence, IS_SEQUENCE_ROLE) @@ -463,7 +463,7 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel): for filepath in filepaths: if os.path.isfile(filepath): _, ext = os.path.splitext(filepath) - if ext in self._allowed_extensions: + if ext.lower() in self._allowed_extensions: return True elif self._allow_folders: @@ -475,7 +475,7 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel): for filepath in filepaths: if os.path.isfile(filepath): _, ext = os.path.splitext(filepath) - if ext in self._allowed_extensions: + if ext.lower() in self._allowed_extensions: filtered_paths.append(filepath) elif self._allow_folders: From 213c78b9a019ac3a8956718e19564b9d5bdfa067 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 14 Sep 2022 00:16:06 +0200 Subject: [PATCH 268/371] Avoid name conflict where `group_name != group_node` due to maya auto renaming new node --- openpype/hosts/maya/plugins/load/load_yeti_cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_yeti_cache.py b/openpype/hosts/maya/plugins/load/load_yeti_cache.py index 090047e22d..5ba381050a 100644 --- a/openpype/hosts/maya/plugins/load/load_yeti_cache.py +++ b/openpype/hosts/maya/plugins/load/load_yeti_cache.py @@ -73,8 +73,8 @@ class YetiCacheLoader(load.LoaderPlugin): c = colors.get(family) if c is not None: - cmds.setAttr(group_name + ".useOutlinerColor", 1) - cmds.setAttr(group_name + ".outlinerColor", + cmds.setAttr(group_node + ".useOutlinerColor", 1) + cmds.setAttr(group_node + ".outlinerColor", (float(c[0])/255), (float(c[1])/255), (float(c[2])/255) From d662b34ca7a70ddb797b4aef4d570028c23a5031 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 15 Nov 2022 13:26:22 +0100 Subject: [PATCH 269/371] added settings for validate frame range in tray publisher --- .../project_settings/traypublisher.json | 7 +++++ .../schema_project_traypublisher.json | 18 +++++++++++++ .../schemas/template_validate_plugin.json | 26 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/template_validate_plugin.json diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 5db2a79772..e99b96b8c4 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -303,5 +303,12 @@ "extensions": [ ".mov" ] + }, + "publish": { + "ValidateFrameRange": { + "enabled": true, + "optional": true, + "active": true + } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 7c61aeed50..faa5033d2a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -311,6 +311,24 @@ "object_type": "text" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "publish", + "label": "Publish plugins", + "children": [ + { + "type": "schema_template", + "name": "template_validate_plugin", + "template_data": [ + { + "key": "ValidateFrameRange", + "label": "Validate frame range" + } + ] + } + ] } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_validate_plugin.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_validate_plugin.json new file mode 100644 index 0000000000..b57cad6719 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_validate_plugin.json @@ -0,0 +1,26 @@ +[ + { + "type": "dict", + "collapsible": true, + "key": "{key}", + "label": "{label}", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + } + ] + } +] From 125d0bbeed7b07640bc34dd877dac2e4c814895f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Mon, 31 Oct 2022 13:28:50 +0100 Subject: [PATCH 270/371] Feature: Auto download last published workfile as first workfile --- .../hooks/pre_copy_last_published_workfile.py | 124 ++++++++++++++++++ openpype/modules/sync_server/sync_server.py | 9 +- 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 openpype/hooks/pre_copy_last_published_workfile.py diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py new file mode 100644 index 0000000000..004f9d25e7 --- /dev/null +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -0,0 +1,124 @@ +import gc +import os +import shutil +from openpype.client.entities import ( + get_last_version_by_subset_id, + get_representations, + get_subsets, +) +from openpype.lib import PreLaunchHook +from openpype.modules.base import ModulesManager +from openpype.pipeline.load.utils import get_representation_path + + +class CopyLastPublishedWorkfile(PreLaunchHook): + """Copy last published workfile as first workfile. + + Prelaunch hook works only if last workfile leads to not existing file. + - That is possible only if it's first version. + """ + + # Before `AddLastWorkfileToLaunchArgs` + order = -1 + app_groups = ["blender", "photoshop", "tvpaint", "aftereffects"] + + def execute(self): + """Check if local workfile doesn't exist, else copy it. + + 1- Check if setting for this feature is enabled + 2- Check if workfile in work area doesn't exist + 3- Check if published workfile exists and is copied locally in publish + + Returns: + None: This is a void method. + """ + # TODO setting + self.log.info("Trying to fetch last published workfile...") + + last_workfile = self.data.get("last_workfile_path") + if os.path.exists(last_workfile): + self.log.debug( + "Last workfile exists. Skipping {} process.".format( + self.__class__.__name__ + ) + ) + return + + project_name = self.data["project_name"] + task_name = self.data["task_name"] + + project_doc = self.data.get("project_doc") + asset_doc = self.data.get("asset_doc") + anatomy = self.data.get("anatomy") + if project_doc and asset_doc: + # Get subset id + subset_id = next( + ( + subset["_id"] + for subset in get_subsets( + project_name, + asset_ids=[asset_doc["_id"]], + fields=["_id", "data.family"], + ) + if subset["data"]["family"] == "workfile" + ), + None, + ) + if not subset_id: + return + + # Get workfile representation + workfile_representation = next( + ( + representation + for representation in get_representations( + project_name, + version_ids=[ + get_last_version_by_subset_id( + project_name, subset_id, fields=["_id"] + )["_id"] + ], + ) + if representation["context"]["task"]["name"] == task_name + ), + None, + ) + + if workfile_representation: # TODO add setting + # Get sync server from Tray, which handles the asynchronous thread instance + sync_server = next( + ( + t["sync_server"] + for t in [ + obj + for obj in gc.get_objects() + if isinstance(obj, ModulesManager) + ] + if t["sync_server"].sync_server_thread + ), + None, + ) + + # Add site and reset timer + active_site = sync_server.get_active_site(project_name) + sync_server.add_site( + project_name, + workfile_representation["_id"], + active_site, + force=True, + ) + sync_server.reset_timer() + + # Wait for the download loop to end + sync_server.sync_server_thread.files_processed.wait() + + # Get paths + published_workfile_path = get_representation_path( + workfile_representation, root=anatomy.roots + ) + local_workfile_dir = os.path.dirname(last_workfile) + + # Copy file and substitute path + self.data["last_workfile_path"] = shutil.copy( + published_workfile_path, local_workfile_dir + ) diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index 8b11055e65..def9e6cfd8 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -236,7 +236,11 @@ class SyncServerThread(threading.Thread): """ def __init__(self, module): self.log = Logger.get_logger(self.__class__.__name__) - super(SyncServerThread, self).__init__() + + # Event to trigger files have been processed + self.files_processed = threading.Event() + + super(SyncServerThread, self).__init__(args=(self.files_processed,)) self.module = module self.loop = None self.is_running = False @@ -396,6 +400,8 @@ class SyncServerThread(threading.Thread): representation, site, error) + # Trigger files are processed + self.files_processed.set() duration = time.time() - start_time self.log.debug("One loop took {:.2f}s".format(duration)) @@ -454,6 +460,7 @@ class SyncServerThread(threading.Thread): async def run_timer(self, delay): """Wait for 'delay' seconds to start next loop""" + self.files_processed.clear() await asyncio.sleep(delay) def reset_timer(self): From af15b0d9415d1bfd2bff978ad81d370484d36bdb Mon Sep 17 00:00:00 2001 From: Felix David Date: Tue, 1 Nov 2022 10:00:26 +0100 Subject: [PATCH 271/371] Project setting --- .../hooks/pre_copy_last_published_workfile.py | 119 ++++++++++++------ .../defaults/project_settings/global.json | 3 +- .../schemas/schema_global_tools.json | 5 + 3 files changed, 88 insertions(+), 39 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 004f9d25e7..312548d2db 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -7,8 +7,10 @@ from openpype.client.entities import ( get_subsets, ) from openpype.lib import PreLaunchHook +from openpype.lib.profiles_filtering import filter_profiles from openpype.modules.base import ModulesManager from openpype.pipeline.load.utils import get_representation_path +from openpype.settings.lib import get_project_settings class CopyLastPublishedWorkfile(PreLaunchHook): @@ -32,9 +34,45 @@ class CopyLastPublishedWorkfile(PreLaunchHook): Returns: None: This is a void method. """ - # TODO setting + project_name = self.data["project_name"] + task_name = self.data["task_name"] + task_type = self.data["task_type"] + host_name = self.application.host_name + + # Check settings has enabled it + project_settings = get_project_settings(project_name) + profiles = project_settings["global"]["tools"]["Workfiles"][ + "last_workfile_on_startup" + ] + filter_data = { + "tasks": task_name, + "task_types": task_type, + "hosts": host_name, + } + last_workfile_settings = filter_profiles(profiles, filter_data) + use_last_published_workfile = last_workfile_settings.get( + "use_last_published_workfile" + ) + if use_last_published_workfile is None: + self.log.info( + ( + "Seems like old version of settings is used." + ' Can\'t access custom templates in host "{}".' + ).format(host_name) + ) + return + elif use_last_published_workfile is False: + self.log.info( + ( + 'Project "{}" has turned off to use last published workfile' + ' as first workfile for host "{}"' + ).format(project_name, host_name) + ) + return + self.log.info("Trying to fetch last published workfile...") + # Check there is no workfile available last_workfile = self.data.get("last_workfile_path") if os.path.exists(last_workfile): self.log.debug( @@ -44,9 +82,6 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ) return - project_name = self.data["project_name"] - task_name = self.data["task_name"] - project_doc = self.data.get("project_doc") asset_doc = self.data.get("asset_doc") anatomy = self.data.get("anatomy") @@ -65,6 +100,9 @@ class CopyLastPublishedWorkfile(PreLaunchHook): None, ) if not subset_id: + self.log.debug('No any workfile for asset "{}".').format( + asset_doc["name"] + ) return # Get workfile representation @@ -84,41 +122,46 @@ class CopyLastPublishedWorkfile(PreLaunchHook): None, ) - if workfile_representation: # TODO add setting - # Get sync server from Tray, which handles the asynchronous thread instance - sync_server = next( - ( - t["sync_server"] - for t in [ - obj - for obj in gc.get_objects() - if isinstance(obj, ModulesManager) - ] - if t["sync_server"].sync_server_thread - ), - None, - ) + if not workfile_representation: + self.log.debug( + 'No published workfile for task "{}" and host "{}".' + ).format(task_name, host_name) + return - # Add site and reset timer - active_site = sync_server.get_active_site(project_name) - sync_server.add_site( - project_name, - workfile_representation["_id"], - active_site, - force=True, - ) - sync_server.reset_timer() + # Get sync server from Tray, which handles the asynchronous thread instance + sync_server = next( + ( + t["sync_server"] + for t in [ + obj + for obj in gc.get_objects() + if isinstance(obj, ModulesManager) + ] + if t["sync_server"].sync_server_thread + ), + None, + ) - # Wait for the download loop to end - sync_server.sync_server_thread.files_processed.wait() + # Add site and reset timer + active_site = sync_server.get_active_site(project_name) + sync_server.add_site( + project_name, + workfile_representation["_id"], + active_site, + force=True, + ) + sync_server.reset_timer() - # Get paths - published_workfile_path = get_representation_path( - workfile_representation, root=anatomy.roots - ) - local_workfile_dir = os.path.dirname(last_workfile) + # Wait for the download loop to end + sync_server.sync_server_thread.files_processed.wait() - # Copy file and substitute path - self.data["last_workfile_path"] = shutil.copy( - published_workfile_path, local_workfile_dir - ) + # Get paths + published_workfile_path = get_representation_path( + workfile_representation, root=anatomy.roots + ) + local_workfile_dir = os.path.dirname(last_workfile) + + # Copy file and substitute path + self.data["last_workfile_path"] = shutil.copy( + published_workfile_path, local_workfile_dir + ) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 9c3f2f1e1b..7daa4afa79 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -458,7 +458,8 @@ "hosts": [], "task_types": [], "tasks": [], - "enabled": true + "enabled": true, + "use_last_published_workfile": false } ], "open_workfile_tool_on_startup": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index ba446135e2..962008d476 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -149,6 +149,11 @@ "type": "boolean", "key": "enabled", "label": "Enabled" + }, + { + "type": "boolean", + "key": "use_last_published_workfile", + "label": "Use last published workfile" } ] } From 7a7c91c418f1084dacd25e6aa453e0c70caf9fcd Mon Sep 17 00:00:00 2001 From: Felix David Date: Tue, 1 Nov 2022 10:10:12 +0100 Subject: [PATCH 272/371] docstring --- openpype/hooks/pre_copy_last_published_workfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 312548d2db..b1b2fe2366 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -30,6 +30,7 @@ class CopyLastPublishedWorkfile(PreLaunchHook): 1- Check if setting for this feature is enabled 2- Check if workfile in work area doesn't exist 3- Check if published workfile exists and is copied locally in publish + 4- Substitute copied published workfile as first workfile Returns: None: This is a void method. From e24489c463af8ce3a83807df69af984357363bfb Mon Sep 17 00:00:00 2001 From: Felix David Date: Tue, 1 Nov 2022 10:14:58 +0100 Subject: [PATCH 273/371] comment length --- openpype/hooks/pre_copy_last_published_workfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index b1b2fe2366..d342151823 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -129,7 +129,8 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ).format(task_name, host_name) return - # Get sync server from Tray, which handles the asynchronous thread instance + # Get sync server from Tray, + # which handles the asynchronous thread instance sync_server = next( ( t["sync_server"] From 17853d0b3b55658310bef044eb65bed19d533bed Mon Sep 17 00:00:00 2001 From: Felix David Date: Tue, 1 Nov 2022 10:50:30 +0100 Subject: [PATCH 274/371] lint --- openpype/hooks/pre_copy_last_published_workfile.py | 4 ++-- openpype/modules/sync_server/sync_server.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index d342151823..cf4edeac9b 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -65,8 +65,8 @@ class CopyLastPublishedWorkfile(PreLaunchHook): elif use_last_published_workfile is False: self.log.info( ( - 'Project "{}" has turned off to use last published workfile' - ' as first workfile for host "{}"' + 'Project "{}" has turned off to use last published' + ' workfile as first workfile for host "{}"' ).format(project_name, host_name) ) return diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index def9e6cfd8..353b39c4e1 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -239,7 +239,7 @@ class SyncServerThread(threading.Thread): # Event to trigger files have been processed self.files_processed = threading.Event() - + super(SyncServerThread, self).__init__(args=(self.files_processed,)) self.module = module self.loop = None From 881bcebd1dbec626d1b1e48ebf079746ad567b0c Mon Sep 17 00:00:00 2001 From: Felix David Date: Thu, 3 Nov 2022 11:41:59 +0100 Subject: [PATCH 275/371] requested cosmetic changes --- .../hooks/pre_copy_last_published_workfile.py | 172 +++++++++--------- 1 file changed, 90 insertions(+), 82 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index cf4edeac9b..7a835507f7 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -35,6 +35,17 @@ class CopyLastPublishedWorkfile(PreLaunchHook): Returns: None: This is a void method. """ + # Check there is no workfile available + last_workfile = self.data.get("last_workfile_path") + if os.path.exists(last_workfile): + self.log.debug( + "Last workfile exists. Skipping {} process.".format( + self.__class__.__name__ + ) + ) + return + + # Get data project_name = self.data["project_name"] task_name = self.data["task_name"] task_type = self.data["task_type"] @@ -73,97 +84,94 @@ class CopyLastPublishedWorkfile(PreLaunchHook): self.log.info("Trying to fetch last published workfile...") - # Check there is no workfile available - last_workfile = self.data.get("last_workfile_path") - if os.path.exists(last_workfile): - self.log.debug( - "Last workfile exists. Skipping {} process.".format( - self.__class__.__name__ - ) - ) - return - project_doc = self.data.get("project_doc") asset_doc = self.data.get("asset_doc") anatomy = self.data.get("anatomy") - if project_doc and asset_doc: - # Get subset id - subset_id = next( - ( - subset["_id"] - for subset in get_subsets( - project_name, - asset_ids=[asset_doc["_id"]], - fields=["_id", "data.family"], - ) - if subset["data"]["family"] == "workfile" - ), - None, - ) - if not subset_id: - self.log.debug('No any workfile for asset "{}".').format( - asset_doc["name"] - ) - return - # Get workfile representation - workfile_representation = next( - ( - representation - for representation in get_representations( - project_name, - version_ids=[ + # Check it can proceed + if not project_doc and not asset_doc: + return + + # Get subset id + subset_id = next( + ( + subset["_id"] + for subset in get_subsets( + project_name, + asset_ids=[asset_doc["_id"]], + fields=["_id", "data.family"], + ) + if subset["data"]["family"] == "workfile" + ), + None, + ) + if not subset_id: + self.log.debug('No any workfile for asset "{}".').format( + asset_doc["name"] + ) + return + + # Get workfile representation + workfile_representation = next( + ( + representation + for representation in get_representations( + project_name, + version_ids=[ + ( get_last_version_by_subset_id( project_name, subset_id, fields=["_id"] - )["_id"] - ], - ) - if representation["context"]["task"]["name"] == task_name - ), - None, - ) + ) + or {} + ).get("_id") + ], + ) + if representation["context"]["task"]["name"] == task_name + ), + None, + ) - if not workfile_representation: - self.log.debug( - 'No published workfile for task "{}" and host "{}".' - ).format(task_name, host_name) - return + if not workfile_representation: + self.log.debug( + 'No published workfile for task "{}" and host "{}".' + ).format(task_name, host_name) + return - # Get sync server from Tray, - # which handles the asynchronous thread instance - sync_server = next( - ( - t["sync_server"] - for t in [ - obj - for obj in gc.get_objects() - if isinstance(obj, ModulesManager) - ] - if t["sync_server"].sync_server_thread - ), - None, - ) + # Get sync server from Tray, + # which handles the asynchronous thread instance + sync_server = next( + ( + t["sync_server"] + for t in [ + obj + for obj in gc.get_objects() + if isinstance(obj, ModulesManager) + ] + if t["sync_server"].sync_server_thread + ), + None, + ) - # Add site and reset timer - active_site = sync_server.get_active_site(project_name) - sync_server.add_site( - project_name, - workfile_representation["_id"], - active_site, - force=True, - ) - sync_server.reset_timer() + # Add site and reset timer + active_site = sync_server.get_active_site(project_name) + sync_server.add_site( + project_name, + workfile_representation["_id"], + active_site, + force=True, + ) + sync_server.reset_timer() - # Wait for the download loop to end - sync_server.sync_server_thread.files_processed.wait() + # Wait for the download loop to end + sync_server.sync_server_thread.files_processed.wait() - # Get paths - published_workfile_path = get_representation_path( - workfile_representation, root=anatomy.roots - ) - local_workfile_dir = os.path.dirname(last_workfile) + # Get paths + published_workfile_path = get_representation_path( + workfile_representation, root=anatomy.roots + ) + local_workfile_dir = os.path.dirname(last_workfile) - # Copy file and substitute path - self.data["last_workfile_path"] = shutil.copy( - published_workfile_path, local_workfile_dir - ) + # Copy file and substitute path + self.data["last_workfile_path"] = shutil.copy( + published_workfile_path, local_workfile_dir + ) From 9e01c5deaa1615316b82d6123df8ffa1101a15ec Mon Sep 17 00:00:00 2001 From: Felix David Date: Thu, 3 Nov 2022 18:33:54 +0100 Subject: [PATCH 276/371] Change to REST API using web server --- .../hooks/pre_copy_last_published_workfile.py | 54 ++++++++------- openpype/modules/sync_server/rest_api.py | 68 +++++++++++++++++++ openpype/modules/sync_server/sync_server.py | 12 ++-- .../modules/sync_server/sync_server_module.py | 9 +++ openpype/modules/timers_manager/rest_api.py | 2 +- 5 files changed, 112 insertions(+), 33 deletions(-) create mode 100644 openpype/modules/sync_server/rest_api.py diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 7a835507f7..cefc7e5d40 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -1,14 +1,14 @@ -import gc import os import shutil +from time import sleep from openpype.client.entities import ( get_last_version_by_subset_id, get_representations, get_subsets, ) from openpype.lib import PreLaunchHook +from openpype.lib.local_settings import get_local_site_id from openpype.lib.profiles_filtering import filter_profiles -from openpype.modules.base import ModulesManager from openpype.pipeline.load.utils import get_representation_path from openpype.settings.lib import get_project_settings @@ -137,33 +137,37 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ).format(task_name, host_name) return - # Get sync server from Tray, - # which handles the asynchronous thread instance - sync_server = next( - ( - t["sync_server"] - for t in [ - obj - for obj in gc.get_objects() - if isinstance(obj, ModulesManager) - ] - if t["sync_server"].sync_server_thread - ), - None, - ) + # POST to webserver sites to add to representations + webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") + if not webserver_url: + self.log.warning("Couldn't find webserver url") + return - # Add site and reset timer - active_site = sync_server.get_active_site(project_name) - sync_server.add_site( - project_name, - workfile_representation["_id"], - active_site, - force=True, + entry_point_url = "{}/sync_server".format(webserver_url) + rest_api_url = "{}/add_sites_to_representations".format( + entry_point_url + ) + try: + import requests + except Exception: + self.log.warning( + "Couldn't add sites to representations ('requests' is not available)" + ) + return + + requests.post( + rest_api_url, + json={ + "project_name": project_name, + "sites": [get_local_site_id()], + "representations": [str(workfile_representation["_id"])], + }, ) - sync_server.reset_timer() # Wait for the download loop to end - sync_server.sync_server_thread.files_processed.wait() + rest_api_url = "{}/files_are_processed".format(entry_point_url) + while requests.get(rest_api_url).content: + sleep(5) # Get paths published_workfile_path = get_representation_path( diff --git a/openpype/modules/sync_server/rest_api.py b/openpype/modules/sync_server/rest_api.py new file mode 100644 index 0000000000..b7c5d26d15 --- /dev/null +++ b/openpype/modules/sync_server/rest_api.py @@ -0,0 +1,68 @@ +from aiohttp.web_response import Response +from openpype.lib import Logger + + +class SyncServerModuleRestApi: + """ + REST API endpoint used for calling from hosts when context change + happens in Workfile app. + """ + + def __init__(self, user_module, server_manager): + self._log = None + self.module = user_module + self.server_manager = server_manager + + self.prefix = "/sync_server" + + self.register() + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + + def register(self): + self.server_manager.add_route( + "POST", + self.prefix + "/add_sites_to_representations", + self.add_sites_to_representations, + ) + self.server_manager.add_route( + "GET", + self.prefix + "/files_are_processed", + self.files_are_processed, + ) + + async def add_sites_to_representations(self, request): + # Extract data from request + data = await request.json() + try: + project_name = data["project_name"] + sites = data["sites"] + representations = data["representations"] + except KeyError: + msg = ( + "Payload must contain fields 'project_name," + " 'sites' (list of names) and 'representations' (list of IDs)" + ) + self.log.error(msg) + return Response(status=400, message=msg) + + # Add all sites to each representation + for representation_id in representations: + for site in sites: + self.module.add_site( + project_name, representation_id, site, force=True + ) + + # Force timer to run immediately + self.module.reset_timer() + + return Response(status=200) + + async def files_are_processed(self, _request): + return Response( + body=bytes(self.module.sync_server_thread.files_are_processed) + ) diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index 353b39c4e1..7fd2311c2d 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -237,15 +237,13 @@ class SyncServerThread(threading.Thread): def __init__(self, module): self.log = Logger.get_logger(self.__class__.__name__) - # Event to trigger files have been processed - self.files_processed = threading.Event() - - super(SyncServerThread, self).__init__(args=(self.files_processed,)) + super(SyncServerThread, self).__init__() self.module = module self.loop = None self.is_running = False self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=3) self.timer = None + self.files_are_processed = False def run(self): self.is_running = True @@ -400,8 +398,8 @@ class SyncServerThread(threading.Thread): representation, site, error) - # Trigger files are processed - self.files_processed.set() + # Trigger files process finished + self.files_are_processed = False duration = time.time() - start_time self.log.debug("One loop took {:.2f}s".format(duration)) @@ -460,7 +458,6 @@ class SyncServerThread(threading.Thread): async def run_timer(self, delay): """Wait for 'delay' seconds to start next loop""" - self.files_processed.clear() await asyncio.sleep(delay) def reset_timer(self): @@ -469,6 +466,7 @@ class SyncServerThread(threading.Thread): if self.timer: self.timer.cancel() self.timer = None + self.files_are_processed = True def _working_sites(self, project_name): if self.module.is_project_paused(project_name): diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index e84c333a58..bff999723b 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -2089,6 +2089,15 @@ class SyncServerModule(OpenPypeModule, ITrayModule): def cli(self, click_group): click_group.add_command(cli_main) + # Webserver module implementation + def webserver_initialization(self, server_manager): + """Add routes for syncs.""" + if self.tray_initialized: + from .rest_api import SyncServerModuleRestApi + self.rest_api_obj = SyncServerModuleRestApi( + self, server_manager + ) + @click.group(SyncServerModule.name, help="SyncServer module related commands.") def cli_main(): diff --git a/openpype/modules/timers_manager/rest_api.py b/openpype/modules/timers_manager/rest_api.py index 4a2e9e6575..979db9075b 100644 --- a/openpype/modules/timers_manager/rest_api.py +++ b/openpype/modules/timers_manager/rest_api.py @@ -21,7 +21,7 @@ class TimersManagerModuleRestApi: @property def log(self): if self._log is None: - self._log = Logger.get_logger(self.__ckass__.__name__) + self._log = Logger.get_logger(self.__class__.__name__) return self._log def register(self): From 8d9542bb45088fec5c800fe7b7d9b76f5ca3c14c Mon Sep 17 00:00:00 2001 From: Felix David Date: Thu, 3 Nov 2022 18:37:13 +0100 Subject: [PATCH 277/371] lint --- openpype/hooks/pre_copy_last_published_workfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index cefc7e5d40..6bec4f7d2c 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -151,7 +151,8 @@ class CopyLastPublishedWorkfile(PreLaunchHook): import requests except Exception: self.log.warning( - "Couldn't add sites to representations ('requests' is not available)" + "Couldn't add sites to representations " + "('requests' is not available)" ) return From 5e02d7d2d71796b6826e320fd8cfbc3e77980d93 Mon Sep 17 00:00:00 2001 From: Felix David Date: Fri, 4 Nov 2022 10:06:59 +0100 Subject: [PATCH 278/371] legacy compatibility --- openpype/hooks/pre_copy_last_published_workfile.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 6bec4f7d2c..f3293fa511 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -99,9 +99,11 @@ class CopyLastPublishedWorkfile(PreLaunchHook): for subset in get_subsets( project_name, asset_ids=[asset_doc["_id"]], - fields=["_id", "data.family"], + fields=["_id", "data.family", "data.families"], ) - if subset["data"]["family"] == "workfile" + if subset["data"].get("family") == "workfile" + # Legacy compatibility + or "workfile" in subset["data"].get("families", {}) ), None, ) From 1c48c0936290cbd7013df50d83392f53a68d51dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Nov 2022 16:41:19 +0100 Subject: [PATCH 279/371] use 'created_dt' of representation --- openpype/client/entities.py | 28 +++++++++++++++++++ .../hooks/pre_copy_last_published_workfile.py | 19 +++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 43afccf2f1..43c2874f57 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -6,6 +6,7 @@ that has project name as a context (e.g. on 'ProjectEntity'?). + We will need more specific functions doing wery specific queires really fast. """ +from datetime import datetime import re import collections @@ -1367,6 +1368,33 @@ def get_representation_parents(project_name, representation): return parents_by_repre_id[repre_id] +def get_representation_last_created_time_on_site( + representation: dict, site_name: str +) -> datetime: + """Get `created_dt` value for representation on site. + + Args: + representation (dict): Representation to get creation date of + site_name (str): Site from which to get the creation date + + Returns: + datetime: Created time of representation on site + """ + created_time = next( + ( + site.get("created_dt") + for site in representation["files"][0].get("sites", []) + if site["name"] == site_name + ), + None, + ) + if created_time: + return created_time + else: + # Use epoch as 'zero' time + return datetime.utcfromtimestamp(0) + + def get_thumbnail_id_from_source(project_name, src_type, src_id): """Receive thumbnail id from source entity. diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index f3293fa511..4eb66f6f85 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -3,6 +3,8 @@ import shutil from time import sleep from openpype.client.entities import ( get_last_version_by_subset_id, + get_representation_by_id, + get_representation_last_created_time_on_site, get_representations, get_subsets, ) @@ -158,18 +160,29 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ) return + local_site_id = get_local_site_id() requests.post( rest_api_url, json={ "project_name": project_name, - "sites": [get_local_site_id()], + "sites": [local_site_id], "representations": [str(workfile_representation["_id"])], }, ) # Wait for the download loop to end - rest_api_url = "{}/files_are_processed".format(entry_point_url) - while requests.get(rest_api_url).content: + last_created_time = get_representation_last_created_time_on_site( + workfile_representation, local_site_id + ) + while ( + last_created_time + >= get_representation_last_created_time_on_site( + get_representation_by_id( + project_name, workfile_representation["_id"] + ), + local_site_id, + ) + ): sleep(5) # Get paths From a614f0f805acd6d73c57dc68bc00a9d7834714cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Nov 2022 18:57:02 +0100 Subject: [PATCH 280/371] add priority to add_site --- openpype/modules/sync_server/rest_api.py | 17 ++++++----------- openpype/modules/sync_server/sync_server.py | 4 ---- .../modules/sync_server/sync_server_module.py | 13 ++++++++++--- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/openpype/modules/sync_server/rest_api.py b/openpype/modules/sync_server/rest_api.py index b7c5d26d15..e92ddc8eee 100644 --- a/openpype/modules/sync_server/rest_api.py +++ b/openpype/modules/sync_server/rest_api.py @@ -1,4 +1,5 @@ from aiohttp.web_response import Response +from openpype.client.entities import get_representation_by_id from openpype.lib import Logger @@ -29,11 +30,6 @@ class SyncServerModuleRestApi: self.prefix + "/add_sites_to_representations", self.add_sites_to_representations, ) - self.server_manager.add_route( - "GET", - self.prefix + "/files_are_processed", - self.files_are_processed, - ) async def add_sites_to_representations(self, request): # Extract data from request @@ -54,15 +50,14 @@ class SyncServerModuleRestApi: for representation_id in representations: for site in sites: self.module.add_site( - project_name, representation_id, site, force=True + project_name, + representation_id, + site, + force=True, + priority=99, ) # Force timer to run immediately self.module.reset_timer() return Response(status=200) - - async def files_are_processed(self, _request): - return Response( - body=bytes(self.module.sync_server_thread.files_are_processed) - ) diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index 7fd2311c2d..d0a40a60ff 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -243,7 +243,6 @@ class SyncServerThread(threading.Thread): self.is_running = False self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=3) self.timer = None - self.files_are_processed = False def run(self): self.is_running = True @@ -398,8 +397,6 @@ class SyncServerThread(threading.Thread): representation, site, error) - # Trigger files process finished - self.files_are_processed = False duration = time.time() - start_time self.log.debug("One loop took {:.2f}s".format(duration)) @@ -466,7 +463,6 @@ class SyncServerThread(threading.Thread): if self.timer: self.timer.cancel() self.timer = None - self.files_are_processed = True def _working_sites(self, project_name): if self.module.is_project_paused(project_name): diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index bff999723b..6a1fc9a1c5 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -136,7 +136,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """ Start of Public API """ def add_site(self, project_name, representation_id, site_name=None, - force=False): + force=False, priority=None): """ Adds new site to representation to be synced. @@ -152,6 +152,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): representation_id (string): MongoDB _id value site_name (string): name of configured and active site force (bool): reset site if exists + priority (int): set priority Throws: SiteAlreadyPresentError - if adding already existing site and @@ -167,7 +168,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): self.reset_site_on_representation(project_name, representation_id, site_name=site_name, - force=force) + force=force, + priority=priority) def remove_site(self, project_name, representation_id, site_name, remove_local_files=False): @@ -1655,7 +1657,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): def reset_site_on_representation(self, project_name, representation_id, side=None, file_id=None, site_name=None, - remove=False, pause=None, force=False): + remove=False, pause=None, force=False, priority=None): """ Reset information about synchronization for particular 'file_id' and provider. @@ -1678,6 +1680,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): remove (bool): if True remove site altogether pause (bool or None): if True - pause, False - unpause force (bool): hard reset - currently only for add_site + priority (int): set priority Raises: SiteAlreadyPresentError - if adding already existing site and @@ -1705,6 +1708,10 @@ class SyncServerModule(OpenPypeModule, ITrayModule): elem = {"name": site_name} + # Add priority + if priority: + elem["priority"] = priority + if file_id: # reset site for particular file self._reset_site_for_file(project_name, representation_id, elem, file_id, site_name) From b6365d85404b88dddcb218969f0a7e30e4668e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Nov 2022 18:58:19 +0100 Subject: [PATCH 281/371] clean --- openpype/modules/sync_server/rest_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/sync_server/rest_api.py b/openpype/modules/sync_server/rest_api.py index e92ddc8eee..0c3b914833 100644 --- a/openpype/modules/sync_server/rest_api.py +++ b/openpype/modules/sync_server/rest_api.py @@ -1,5 +1,4 @@ from aiohttp.web_response import Response -from openpype.client.entities import get_representation_by_id from openpype.lib import Logger From 138051f2f4c9d0c705eeb1cd299166d0ca249850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Nov 2022 18:59:03 +0100 Subject: [PATCH 282/371] clean --- openpype/modules/sync_server/sync_server_module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 6a1fc9a1c5..951cb116fc 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -1657,7 +1657,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): def reset_site_on_representation(self, project_name, representation_id, side=None, file_id=None, site_name=None, - remove=False, pause=None, force=False, priority=None): + remove=False, pause=None, force=False, + priority=None): """ Reset information about synchronization for particular 'file_id' and provider. From 3a5ebc6ea29fd4ec34b0fc80c27f5cc187ace8e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Nov 2022 19:02:29 +0100 Subject: [PATCH 283/371] sort fields --- openpype/hooks/pre_copy_last_published_workfile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 4eb66f6f85..acbc9ec1c7 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -178,7 +178,9 @@ class CopyLastPublishedWorkfile(PreLaunchHook): last_created_time >= get_representation_last_created_time_on_site( get_representation_by_id( - project_name, workfile_representation["_id"] + project_name, + workfile_representation["_id"], + fields=["files"], ), local_site_id, ) From 29f0dee272c9b0b27c4a6e4098caab2ed11a1d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Nov 2022 19:04:03 +0100 Subject: [PATCH 284/371] clean --- openpype/modules/sync_server/sync_server_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 951cb116fc..1292bed9af 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -1657,7 +1657,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): def reset_site_on_representation(self, project_name, representation_id, side=None, file_id=None, site_name=None, - remove=False, pause=None, force=False, + remove=False, pause=None, force=False, priority=None): """ Reset information about synchronization for particular 'file_id' From a600cf4dcad5f8caa08187ca2d449bbf9986623a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:18:23 +0100 Subject: [PATCH 285/371] fix last version check --- .../hooks/pre_copy_last_published_workfile.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index acbc9ec1c7..96b5ccadb2 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -116,19 +116,19 @@ class CopyLastPublishedWorkfile(PreLaunchHook): return # Get workfile representation + last_version_doc = get_last_version_by_subset_id( + project_name, subset_id, fields=["_id"] + ) + if not last_version_doc: + self.log.debug("Subset does not have any versions") + return + workfile_representation = next( ( representation for representation in get_representations( project_name, - version_ids=[ - ( - get_last_version_by_subset_id( - project_name, subset_id, fields=["_id"] - ) - or {} - ).get("_id") - ], + version_ids=[last_version_doc["_id"]] ) if representation["context"]["task"]["name"] == task_name ), From 7596610c160cf83b5dccb00c1638756312a54cf1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:19:03 +0100 Subject: [PATCH 286/371] replaced 'add_sites_to_representations' with 'reset_timer' in rest api --- openpype/modules/sync_server/rest_api.py | 31 +++--------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/openpype/modules/sync_server/rest_api.py b/openpype/modules/sync_server/rest_api.py index 0c3b914833..51769cd4fb 100644 --- a/openpype/modules/sync_server/rest_api.py +++ b/openpype/modules/sync_server/rest_api.py @@ -26,36 +26,11 @@ class SyncServerModuleRestApi: def register(self): self.server_manager.add_route( "POST", - self.prefix + "/add_sites_to_representations", - self.add_sites_to_representations, + self.prefix + "/reset_timer", + self.reset_timer, ) - async def add_sites_to_representations(self, request): - # Extract data from request - data = await request.json() - try: - project_name = data["project_name"] - sites = data["sites"] - representations = data["representations"] - except KeyError: - msg = ( - "Payload must contain fields 'project_name," - " 'sites' (list of names) and 'representations' (list of IDs)" - ) - self.log.error(msg) - return Response(status=400, message=msg) - - # Add all sites to each representation - for representation_id in representations: - for site in sites: - self.module.add_site( - project_name, - representation_id, - site, - force=True, - priority=99, - ) - + async def reset_timer(self, request): # Force timer to run immediately self.module.reset_timer() From 2052afc76a72d5845570e4900e527cabe1d1ecb1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:19:15 +0100 Subject: [PATCH 287/371] added ability to rese timer from add_site --- openpype/modules/sync_server/sync_server_module.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 1292bed9af..5e19a6fce0 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -136,7 +136,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """ Start of Public API """ def add_site(self, project_name, representation_id, site_name=None, - force=False, priority=None): + force=False, priority=None, reset_timer=False): """ Adds new site to representation to be synced. @@ -171,6 +171,9 @@ class SyncServerModule(OpenPypeModule, ITrayModule): force=force, priority=priority) + if reset_timer: + self.reset_timer() + def remove_site(self, project_name, representation_id, site_name, remove_local_files=False): """ From f7c1fa01ae1b358b8af45ab777ebda4a6ba81bfc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:19:32 +0100 Subject: [PATCH 288/371] 'reset_timer' can reset timer via rest api endpoint --- .../modules/sync_server/sync_server_module.py | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 5e19a6fce0..b505e25d2f 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -916,7 +916,42 @@ class SyncServerModule(OpenPypeModule, ITrayModule): In case of user's involvement (reset site), start that right away. """ - self.sync_server_thread.reset_timer() + + if not self.enabled: + return + + if self.sync_server_thread is None: + self._reset_timer_with_rest_api() + else: + self.sync_server_thread.reset_timer() + + def is_representaion_on_site( + self, project_name, representation_id, site_id + ): + # TODO implement + return False + + def _reset_timer_with_rest_api(self): + # POST to webserver sites to add to representations + webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") + if not webserver_url: + self.log.warning("Couldn't find webserver url") + return + + rest_api_url = "{}/sync_server/reset_timer".format( + webserver_url + ) + + try: + import requests + except Exception: + self.log.warning( + "Couldn't add sites to representations " + "('requests' is not available)" + ) + return + + requests.post(rest_api_url) def get_enabled_projects(self): """Returns list of projects which have SyncServer enabled.""" From 1d028d22ba4b796b6e8fe700b70f2f2e87217edb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:20:21 +0100 Subject: [PATCH 289/371] updated prelaunch hook with new abilities of sync server --- .../hooks/pre_copy_last_published_workfile.py | 52 +++++-------------- 1 file changed, 12 insertions(+), 40 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 96b5ccadb2..6fd50a64d6 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -141,49 +141,21 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ).format(task_name, host_name) return - # POST to webserver sites to add to representations - webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL") - if not webserver_url: - self.log.warning("Couldn't find webserver url") - return - - entry_point_url = "{}/sync_server".format(webserver_url) - rest_api_url = "{}/add_sites_to_representations".format( - entry_point_url - ) - try: - import requests - except Exception: - self.log.warning( - "Couldn't add sites to representations " - "('requests' is not available)" - ) - return - local_site_id = get_local_site_id() - requests.post( - rest_api_url, - json={ - "project_name": project_name, - "sites": [local_site_id], - "representations": [str(workfile_representation["_id"])], - }, + sync_server = self.modules_manager.get("sync_server") + sync_server.add_site( + project_name, + workfile_representation["_id"], + local_site_id, + force=True, + priority=99, + reset_timer=True ) - # Wait for the download loop to end - last_created_time = get_representation_last_created_time_on_site( - workfile_representation, local_site_id - ) - while ( - last_created_time - >= get_representation_last_created_time_on_site( - get_representation_by_id( - project_name, - workfile_representation["_id"], - fields=["files"], - ), - local_site_id, - ) + while not sync_server.is_representaion_on_site( + project_name, + workfile_representation["_id"], + local_site_id ): sleep(5) From 5db743080ccb22adc89f76fc86f4ca26020503fd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:22:56 +0100 Subject: [PATCH 290/371] check if is sync server enabled --- openpype/hooks/pre_copy_last_published_workfile.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 6fd50a64d6..69e3d6efe4 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -37,6 +37,12 @@ class CopyLastPublishedWorkfile(PreLaunchHook): Returns: None: This is a void method. """ + + sync_server = self.modules_manager.get("sync_server") + if not sync_server or not sync_server.enabled: + self.log.deubg("Sync server module is not enabled or available") + return + # Check there is no workfile available last_workfile = self.data.get("last_workfile_path") if os.path.exists(last_workfile): @@ -142,7 +148,6 @@ class CopyLastPublishedWorkfile(PreLaunchHook): return local_site_id = get_local_site_id() - sync_server = self.modules_manager.get("sync_server") sync_server.add_site( project_name, workfile_representation["_id"], From 75e12954ee51d09916032224f4c72be84c12bacf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:36:03 +0100 Subject: [PATCH 291/371] removed 'get_representation_last_created_time_on_site' function --- openpype/client/entities.py | 27 ------------------- .../hooks/pre_copy_last_published_workfile.py | 2 -- 2 files changed, 29 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 43c2874f57..91d4b499b0 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1368,33 +1368,6 @@ def get_representation_parents(project_name, representation): return parents_by_repre_id[repre_id] -def get_representation_last_created_time_on_site( - representation: dict, site_name: str -) -> datetime: - """Get `created_dt` value for representation on site. - - Args: - representation (dict): Representation to get creation date of - site_name (str): Site from which to get the creation date - - Returns: - datetime: Created time of representation on site - """ - created_time = next( - ( - site.get("created_dt") - for site in representation["files"][0].get("sites", []) - if site["name"] == site_name - ), - None, - ) - if created_time: - return created_time - else: - # Use epoch as 'zero' time - return datetime.utcfromtimestamp(0) - - def get_thumbnail_id_from_source(project_name, src_type, src_id): """Receive thumbnail id from source entity. diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 69e3d6efe4..884b0f54b6 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -3,8 +3,6 @@ import shutil from time import sleep from openpype.client.entities import ( get_last_version_by_subset_id, - get_representation_by_id, - get_representation_last_created_time_on_site, get_representations, get_subsets, ) From 99bebd82a7a3d9288799d4771a242a56dd58c40a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Nov 2022 11:51:00 +0100 Subject: [PATCH 292/371] fix typo --- openpype/hooks/pre_copy_last_published_workfile.py | 2 +- openpype/modules/sync_server/sync_server_module.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 884b0f54b6..0e561334e1 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -155,7 +155,7 @@ class CopyLastPublishedWorkfile(PreLaunchHook): reset_timer=True ) - while not sync_server.is_representaion_on_site( + while not sync_server.is_representation_on_site( project_name, workfile_representation["_id"], local_site_id diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index b505e25d2f..1f65ea9bda 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -925,7 +925,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): else: self.sync_server_thread.reset_timer() - def is_representaion_on_site( + def is_representation_on_site( self, project_name, representation_id, site_id ): # TODO implement From 44cfbf9f2922c80c21875f6f25658e6041c6b677 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Nov 2022 12:27:07 +0100 Subject: [PATCH 293/371] added method to check if representation has all files on site --- .../modules/sync_server/sync_server_module.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 1f65ea9bda..6250146523 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -926,10 +926,27 @@ class SyncServerModule(OpenPypeModule, ITrayModule): self.sync_server_thread.reset_timer() def is_representation_on_site( - self, project_name, representation_id, site_id + self, project_name, representation_id, site_name ): - # TODO implement - return False + """Checks if 'representation_id' has all files avail. on 'site_name'""" + representation = get_representation_by_id(project_name, + representation_id, + fields=["_id", "files"]) + if not representation: + return False + + on_site = False + for file_info in representation.get("files", []): + for site in file_info.get("sites", []): + if site["name"] != site_name: + continue + + if (site.get("progress") or site.get("error") or + not site.get("created_dt")): + return False + on_site = True + + return on_site def _reset_timer_with_rest_api(self): # POST to webserver sites to add to representations From 2e6f850b5d9a99d7063d0693414459834a6ba373 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Nov 2022 12:48:52 +0100 Subject: [PATCH 294/371] small updates to docstrings --- openpype/modules/sync_server/sync_server_module.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 6250146523..653ee50541 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -143,7 +143,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): 'project_name' must have synchronization enabled (globally or project only) - Used as a API endpoint from outside applications (Loader etc). + Used as an API endpoint from outside applications (Loader etc). Use 'force' to reset existing site. @@ -153,6 +153,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): site_name (string): name of configured and active site force (bool): reset site if exists priority (int): set priority + reset_timer (bool): if delay timer should be reset, eg. user mark + some representation to be synced manually Throws: SiteAlreadyPresentError - if adding already existing site and @@ -1601,12 +1603,12 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Args: project_name (string): name of project - force to db connection as each file might come from different collection - new_file_id (string): + new_file_id (string): only present if file synced successfully file (dictionary): info about processed file (pulled from DB) representation (dictionary): parent repr of file (from DB) site (string): label ('gdrive', 'S3') error (string): exception message - progress (float): 0-1 of progress of upload/download + progress (float): 0-0.99 of progress of upload/download priority (int): 0-100 set priority Returns: From 5838f0f6097d57201402d9d5d360755f5c54b93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 9 Nov 2022 17:09:38 +0100 Subject: [PATCH 295/371] clean --- openpype/client/entities.py | 1 - openpype/modules/sync_server/rest_api.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 91d4b499b0..43afccf2f1 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -6,7 +6,6 @@ that has project name as a context (e.g. on 'ProjectEntity'?). + We will need more specific functions doing wery specific queires really fast. """ -from datetime import datetime import re import collections diff --git a/openpype/modules/sync_server/rest_api.py b/openpype/modules/sync_server/rest_api.py index 51769cd4fb..a7d9dd80b7 100644 --- a/openpype/modules/sync_server/rest_api.py +++ b/openpype/modules/sync_server/rest_api.py @@ -30,8 +30,8 @@ class SyncServerModuleRestApi: self.reset_timer, ) - async def reset_timer(self, request): - # Force timer to run immediately + async def reset_timer(self, _request): + """Force timer to run immediately.""" self.module.reset_timer() return Response(status=200) From c096279cfcd9bdf5124a2e444da2830ebf300d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 15 Nov 2022 09:06:09 +0100 Subject: [PATCH 296/371] logging format --- .../hooks/pre_copy_last_published_workfile.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 0e561334e1..44144e5fff 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -75,16 +75,20 @@ class CopyLastPublishedWorkfile(PreLaunchHook): self.log.info( ( "Seems like old version of settings is used." - ' Can\'t access custom templates in host "{}".' - ).format(host_name) + ' Can\'t access custom templates in host "{}".'.format( + host_name + ) + ) ) return elif use_last_published_workfile is False: self.log.info( ( 'Project "{}" has turned off to use last published' - ' workfile as first workfile for host "{}"' - ).format(project_name, host_name) + ' workfile as first workfile for host "{}"'.format( + project_name, host_name + ) + ) ) return @@ -114,8 +118,8 @@ class CopyLastPublishedWorkfile(PreLaunchHook): None, ) if not subset_id: - self.log.debug('No any workfile for asset "{}".').format( - asset_doc["name"] + self.log.debug( + 'No any workfile for asset "{}".'.format(asset_doc["name"]) ) return @@ -131,8 +135,7 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ( representation for representation in get_representations( - project_name, - version_ids=[last_version_doc["_id"]] + project_name, version_ids=[last_version_doc["_id"]] ) if representation["context"]["task"]["name"] == task_name ), @@ -141,8 +144,10 @@ class CopyLastPublishedWorkfile(PreLaunchHook): if not workfile_representation: self.log.debug( - 'No published workfile for task "{}" and host "{}".' - ).format(task_name, host_name) + 'No published workfile for task "{}" and host "{}".'.format( + task_name, host_name + ) + ) return local_site_id = get_local_site_id() @@ -152,13 +157,11 @@ class CopyLastPublishedWorkfile(PreLaunchHook): local_site_id, force=True, priority=99, - reset_timer=True + reset_timer=True, ) while not sync_server.is_representation_on_site( - project_name, - workfile_representation["_id"], - local_site_id + project_name, workfile_representation["_id"], local_site_id ): sleep(5) From 460adc767e6b81e62fdab5d2699f0c10c1023e9a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 31 Oct 2022 19:36:44 +0800 Subject: [PATCH 297/371] Alembic Loader as Arnold Standin --- .../maya/plugins/load/load_abc_to_standin.py | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 openpype/hosts/maya/plugins/load/load_abc_to_standin.py diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py new file mode 100644 index 0000000000..defed4bd73 --- /dev/null +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -0,0 +1,115 @@ +import os +import clique + +from openpype.pipeline import ( + load, + get_representation_path +) +from openpype.settings import get_project_settings + + +class AlembicStandinLoader(load.LoaderPlugin): + """Load Alembic as Arnold Standin""" + + families = ["model", "pointcache"] + representations = ["abc"] + + label = "Import Alembic as Standin" + order = -5 + icon = "code-fork" + color = "orange" + + def load(self, context, name, namespace, options): + + import maya.cmds as cmds + import pymel.core as pm + import mtoa.ui.arnoldmenu + from openpype.hosts.maya.api.pipeline import containerise + from openpype.hosts.maya.api.lib import unique_namespace + + version = context["version"] + version_data = version.get("data", {}) + + self.log.info("version_data: {}\n".format(version_data)) + + frameStart = version_data.get("frameStart", None) + + asset = context["asset"]["name"] + namespace = namespace or unique_namespace( + asset + "_", + prefix="_" if asset[0].isdigit() else "", + suffix="_", + ) + + #Root group + label = "{}:{}".format(namespace, name) + root = pm.group(name=label, empty=True) + + settings = get_project_settings(os.environ['AVALON_PROJECT']) + colors = settings["maya"]["load"]["colors"] + + c = colors.get('ass') + if c is not None: + cmds.setAttr(root + ".useOutlinerColor", 1) + cmds.setAttr(root + ".outlinerColor", + c[0], c[1], c[2]) + + transform_name = label + "_ABC" + + standinShape = pm.PyNode(mtoa.ui.arnoldmenu.createStandIn()) + standin = standinShape.getParent() + standin.rename(transform_name) + + pm.parent(standin, root) + + # Set the standin filepath + standinShape.dso.set(self.fname) + if frameStart is not None: + standinShape.useFrameExtension.set(1) + + nodes = [root, standin] + self[:] = nodes + + return containerise( + name=name, + namespace=namespace, + nodes=nodes, + context=context, + loader=self.__class__.__name__) + + def update(self, container, representation): + + import pymel.core as pm + + path = get_representation_path(representation) + + # Update the standin + standins = list() + members = pm.sets(container['objectName'], query=True) + for member in members: + shape = member.getShape() + if (shape and shape.type() == "aiStandIn"): + standins.append(shape) + + for standin in standins: + standin.dso.set(path) + standin.useFrameExtension.set(1) + + container = pm.PyNode(container["objectName"]) + container.representation.set(str(representation["_id"])) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + import maya.cmds as cmds + members = cmds.sets(container['objectName'], query=True) + cmds.lockNode(members, lock=False) + cmds.delete([container['objectName']] + members) + + # Clean up the namespace + try: + cmds.namespace(removeNamespace=container['namespace'], + deleteNamespaceContent=True) + except RuntimeError: + pass From ecbe06bdc8aeee163072f6173b96ba2886b0ebb1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 31 Oct 2022 19:56:10 +0800 Subject: [PATCH 298/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index defed4bd73..f39aa56650 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -1,5 +1,4 @@ import os -import clique from openpype.pipeline import ( load, @@ -41,7 +40,7 @@ class AlembicStandinLoader(load.LoaderPlugin): suffix="_", ) - #Root group + # Root group label = "{}:{}".format(namespace, name) root = pm.group(name=label, empty=True) From 2e6974b0640d0ad43bbacb2163a1cf85a7933522 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 1 Nov 2022 20:36:58 +0800 Subject: [PATCH 299/371] Alembic Loader as Arnold Standin --- .../maya/plugins/load/load_abc_to_standin.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index f39aa56650..68aeb24069 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -13,7 +13,7 @@ class AlembicStandinLoader(load.LoaderPlugin): families = ["model", "pointcache"] representations = ["abc"] - label = "Import Alembic as Standin" + label = "Import Alembic as Arnold Standin" order = -5 icon = "code-fork" color = "orange" @@ -21,7 +21,6 @@ class AlembicStandinLoader(load.LoaderPlugin): def load(self, context, name, namespace, options): import maya.cmds as cmds - import pymel.core as pm import mtoa.ui.arnoldmenu from openpype.hosts.maya.api.pipeline import containerise from openpype.hosts.maya.api.lib import unique_namespace @@ -42,7 +41,7 @@ class AlembicStandinLoader(load.LoaderPlugin): # Root group label = "{}:{}".format(namespace, name) - root = pm.group(name=label, empty=True) + root = cmds.group(name=label, empty=True) settings = get_project_settings(os.environ['AVALON_PROJECT']) colors = settings["maya"]["load"]["colors"] @@ -55,16 +54,17 @@ class AlembicStandinLoader(load.LoaderPlugin): transform_name = label + "_ABC" - standinShape = pm.PyNode(mtoa.ui.arnoldmenu.createStandIn()) - standin = standinShape.getParent() - standin.rename(transform_name) + standinShape = cmds.ls(mtoa.ui.arnoldmenu.createStandIn())[0] + standin = cmds.listRelatives(standinShape, parent=True, typ="transform") + standin = cmds.rename(standin, transform_name) + standinShape = cmds.listRelatives(standin, children=True)[0] - pm.parent(standin, root) + cmds.parent(standin, root) # Set the standin filepath - standinShape.dso.set(self.fname) + cmds.setAttr(standinShape + ".dso", self.fname, type="string") if frameStart is not None: - standinShape.useFrameExtension.set(1) + cmds.setAttr(standinShape + ".useFrameExtension", 1) nodes = [root, standin] self[:] = nodes From ce5d4c02fa7f31c7731a04f5580e52f042933353 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 1 Nov 2022 20:38:51 +0800 Subject: [PATCH 300/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index 68aeb24069..5d6c52eac9 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -55,7 +55,8 @@ class AlembicStandinLoader(load.LoaderPlugin): transform_name = label + "_ABC" standinShape = cmds.ls(mtoa.ui.arnoldmenu.createStandIn())[0] - standin = cmds.listRelatives(standinShape, parent=True, typ="transform") + standin = cmds.listRelatives(standinShape, parent=True, + typ="transform") standin = cmds.rename(standin, transform_name) standinShape = cmds.listRelatives(standin, children=True)[0] From c58ef40f15c405c615cb5d4ec8a566a00de5c2eb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 9 Nov 2022 00:32:22 +0800 Subject: [PATCH 301/371] Alembic Loader as Arnold Standin --- .../hosts/maya/plugins/load/load_abc_to_standin.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index 5d6c52eac9..94bb974917 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -10,7 +10,7 @@ from openpype.settings import get_project_settings class AlembicStandinLoader(load.LoaderPlugin): """Load Alembic as Arnold Standin""" - families = ["model", "pointcache"] + families = ["animation", "model", "pointcache"] representations = ["abc"] label = "Import Alembic as Arnold Standin" @@ -31,6 +31,7 @@ class AlembicStandinLoader(load.LoaderPlugin): self.log.info("version_data: {}\n".format(version_data)) frameStart = version_data.get("frameStart", None) + frameEnd = version_data.get("frameEnd", None) asset = context["asset"]["name"] namespace = namespace or unique_namespace( @@ -64,7 +65,13 @@ class AlembicStandinLoader(load.LoaderPlugin): # Set the standin filepath cmds.setAttr(standinShape + ".dso", self.fname, type="string") - if frameStart is not None: + cmds.setAttr(standinShape + ".abcFPS", 25) + + if frameStart is None: + cmds.setAttr(standinShape + ".useFrameExtension", 0) + elif frameStart == 1 and frameEnd == 1: + cmds.setAttr(standinShape + ".useFrameExtension", 0) + else: cmds.setAttr(standinShape + ".useFrameExtension", 1) nodes = [root, standin] @@ -93,7 +100,8 @@ class AlembicStandinLoader(load.LoaderPlugin): for standin in standins: standin.dso.set(path) - standin.useFrameExtension.set(1) + standin.useFrameExtension.set(0) + standin.abcFPS.set(25) container = pm.PyNode(container["objectName"]) container.representation.set(str(representation["_id"])) From 031465779bc4096a2848a545b52c37a44a010128 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 9 Nov 2022 08:39:52 +0800 Subject: [PATCH 302/371] Alembic Loader as Arnold Standin --- .../hosts/maya/plugins/load/load_abc_to_standin.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index 94bb974917..19e60d33da 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -1,6 +1,7 @@ import os from openpype.pipeline import ( + legacy_io, load, get_representation_path ) @@ -46,6 +47,7 @@ class AlembicStandinLoader(load.LoaderPlugin): settings = get_project_settings(os.environ['AVALON_PROJECT']) colors = settings["maya"]["load"]["colors"] + fps = legacy_io.Session["AVALON_FPS"] c = colors.get('ass') if c is not None: @@ -65,12 +67,14 @@ class AlembicStandinLoader(load.LoaderPlugin): # Set the standin filepath cmds.setAttr(standinShape + ".dso", self.fname, type="string") - cmds.setAttr(standinShape + ".abcFPS", 25) + cmds.setAttr(standinShape + ".abcFPS", float(fps)) if frameStart is None: cmds.setAttr(standinShape + ".useFrameExtension", 0) + elif frameStart == 1 and frameEnd == 1: cmds.setAttr(standinShape + ".useFrameExtension", 0) + else: cmds.setAttr(standinShape + ".useFrameExtension", 1) @@ -89,7 +93,7 @@ class AlembicStandinLoader(load.LoaderPlugin): import pymel.core as pm path = get_representation_path(representation) - + fps = legacy_io.Session["AVALON_FPS"] # Update the standin standins = list() members = pm.sets(container['objectName'], query=True) @@ -101,7 +105,7 @@ class AlembicStandinLoader(load.LoaderPlugin): for standin in standins: standin.dso.set(path) standin.useFrameExtension.set(0) - standin.abcFPS.set(25) + standin.abcFPS.set(float(fps)) container = pm.PyNode(container["objectName"]) container.representation.set(str(representation["_id"])) From 6e94a81393884cab2c3b2798e2c765c08617c4d1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 19:09:13 +0800 Subject: [PATCH 303/371] Alembic Loader as Arnold Standin --- .../maya/plugins/load/load_abc_to_standin.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index 19e60d33da..a192d9c357 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -28,11 +28,10 @@ class AlembicStandinLoader(load.LoaderPlugin): version = context["version"] version_data = version.get("data", {}) - + family = version["data"]["families"] self.log.info("version_data: {}\n".format(version_data)) - + self.log.info("family: {}\n".format(family)) frameStart = version_data.get("frameStart", None) - frameEnd = version_data.get("frameEnd", None) asset = context["asset"]["name"] namespace = namespace or unique_namespace( @@ -48,12 +47,14 @@ class AlembicStandinLoader(load.LoaderPlugin): settings = get_project_settings(os.environ['AVALON_PROJECT']) colors = settings["maya"]["load"]["colors"] fps = legacy_io.Session["AVALON_FPS"] - - c = colors.get('ass') + c = colors.get(family[0]) if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - c[0], c[1], c[2]) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) transform_name = label + "_ABC" @@ -72,7 +73,7 @@ class AlembicStandinLoader(load.LoaderPlugin): if frameStart is None: cmds.setAttr(standinShape + ".useFrameExtension", 0) - elif frameStart == 1 and frameEnd == 1: + elif "model" in family: cmds.setAttr(standinShape + ".useFrameExtension", 0) else: From 66608300969101ee75cf68a8c7a86dfd7b7710d4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 19:10:41 +0800 Subject: [PATCH 304/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index a192d9c357..8ce1aee3ac 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -30,7 +30,6 @@ class AlembicStandinLoader(load.LoaderPlugin): version_data = version.get("data", {}) family = version["data"]["families"] self.log.info("version_data: {}\n".format(version_data)) - self.log.info("family: {}\n".format(family)) frameStart = version_data.get("frameStart", None) asset = context["asset"]["name"] @@ -51,10 +50,10 @@ class AlembicStandinLoader(load.LoaderPlugin): if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - (float(c[0])/255), - (float(c[1])/255), - (float(c[2])/255) - ) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) transform_name = label + "_ABC" From 7c3f625fe324328ed7e7a93e4adff4da7c1d6e8e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 19:11:56 +0800 Subject: [PATCH 305/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index 8ce1aee3ac..d93c85f8a4 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -50,10 +50,10 @@ class AlembicStandinLoader(load.LoaderPlugin): if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - (float(c[0])/255), - (float(c[1])/255), - (float(c[2])/255) - ) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) transform_name = label + "_ABC" From bee7b17ff2b612fab87c95cdfa4143659453d049 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 19:13:04 +0800 Subject: [PATCH 306/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index d93c85f8a4..dafe999d9d 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -50,9 +50,9 @@ class AlembicStandinLoader(load.LoaderPlugin): if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - (float(c[0])/255), - (float(c[1])/255), - (float(c[2])/255) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) ) transform_name = label + "_ABC" From 3649ee7e4e163f45472c13f5f4bb74a65175e979 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 19:15:11 +0800 Subject: [PATCH 307/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index dafe999d9d..d93c85f8a4 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -50,9 +50,9 @@ class AlembicStandinLoader(load.LoaderPlugin): if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - (float(c[0])/255), - (float(c[1])/255), - (float(c[2])/255) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) ) transform_name = label + "_ABC" From cd27df0e8d35ccb94a4cfde09f399cbb6319a1c4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 19:16:10 +0800 Subject: [PATCH 308/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index d93c85f8a4..8ce1aee3ac 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -50,10 +50,10 @@ class AlembicStandinLoader(load.LoaderPlugin): if c is not None: cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - (float(c[0])/255), - (float(c[1])/255), - (float(c[2])/255) - ) + (float(c[0])/255), + (float(c[1])/255), + (float(c[2])/255) + ) transform_name = label + "_ABC" From 3186acc83e967f726ceec6c975fc74d6ea6cd8a2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 10 Nov 2022 19:19:11 +0800 Subject: [PATCH 309/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index 8ce1aee3ac..9583063c7e 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -30,6 +30,7 @@ class AlembicStandinLoader(load.LoaderPlugin): version_data = version.get("data", {}) family = version["data"]["families"] self.log.info("version_data: {}\n".format(version_data)) + self.log.info("family: {}\n".format(family)) frameStart = version_data.get("frameStart", None) asset = context["asset"]["name"] @@ -48,12 +49,12 @@ class AlembicStandinLoader(load.LoaderPlugin): fps = legacy_io.Session["AVALON_FPS"] c = colors.get(family[0]) if c is not None: + r = (float(c[0]) / 255) + g = (float(c[1]) / 255) + b = (float(c[2]) / 255) cmds.setAttr(root + ".useOutlinerColor", 1) cmds.setAttr(root + ".outlinerColor", - (float(c[0])/255), - (float(c[1])/255), - (float(c[2])/255) - ) + r, g, b) transform_name = label + "_ABC" From cf8bd8eb59590df3b2a196d68bbb47e29fcd862f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 14 Nov 2022 17:57:32 +0800 Subject: [PATCH 310/371] Alembic Loader as Arnold Standin --- openpype/hosts/maya/plugins/load/load_abc_to_standin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py index 9583063c7e..605a492e4d 100644 --- a/openpype/hosts/maya/plugins/load/load_abc_to_standin.py +++ b/openpype/hosts/maya/plugins/load/load_abc_to_standin.py @@ -98,6 +98,7 @@ class AlembicStandinLoader(load.LoaderPlugin): # Update the standin standins = list() members = pm.sets(container['objectName'], query=True) + self.log.info("container:{}".format(container)) for member in members: shape = member.getShape() if (shape and shape.type() == "aiStandIn"): @@ -105,8 +106,11 @@ class AlembicStandinLoader(load.LoaderPlugin): for standin in standins: standin.dso.set(path) - standin.useFrameExtension.set(0) standin.abcFPS.set(float(fps)) + if "modelMain" in container['objectName']: + standin.useFrameExtension.set(0) + else: + standin.useFrameExtension.set(1) container = pm.PyNode(container["objectName"]) container.representation.set(str(representation["_id"])) From 93bff0c038c262290c5d8e0b5e28847c3a210777 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 16 Nov 2022 03:40:10 +0000 Subject: [PATCH 311/371] [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 1953d0d6a5..268f33083a 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.4" +__version__ = "3.14.7-nightly.5" From 35b43f34ebbea13407154445369a4f8cdb15cf78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Nov 2022 23:23:26 +0000 Subject: [PATCH 312/371] Bump loader-utils from 1.4.1 to 1.4.2 in /website Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.1 to 1.4.2. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v1.4.1...v1.4.2) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index 177a4a3802..220a489dfa 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -4812,9 +4812,9 @@ loader-runner@^4.2.0: integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== loader-utils@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.1.tgz#278ad7006660bccc4d2c0c1578e17c5c78d5c0e0" - integrity sha512-1Qo97Y2oKaU+Ro2xnDMR26g1BwMT29jNbem1EvcujW2jqt+j5COXyscjM7bLQkM9HaxI7pkWeW7gnI072yMI9Q== + version "1.4.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" + integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== dependencies: big.js "^5.2.2" emojis-list "^3.0.0" From ece1e8b9137d5a95d412b42b0e2b2fc5b4a9176a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 16 Nov 2022 14:51:10 +0100 Subject: [PATCH 313/371] OP-4394 - Hound --- .../webpublisher/plugins/publish/collect_published_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 181f8b4ab7..79ed499a20 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -253,7 +253,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): if ext: ext = ext.lower() if ext.startswith("."): - ext = ext[1:] + ext = ext[1:] lower_extensions.add(ext) # all extensions setting From 4b95ad68168b138070171c862e0afaf4c08fb9f0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 16 Nov 2022 15:57:30 +0100 Subject: [PATCH 314/371] OP-4394 - use lowercased extension in ExtractReview There might be uppercased extension sent in by accident (.PNG), which would make all checks against set of extension not work. --- openpype/plugins/publish/extract_review.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 1f9b30fba3..982bd9dc24 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -152,7 +152,7 @@ class ExtractReview(pyblish.api.InstancePlugin): if input_ext.startswith("."): input_ext = input_ext[1:] - if input_ext not in self.supported_exts: + if input_ext.lower() not in self.supported_exts: self.log.info( "Representation has unsupported extension \"{}\"".format( input_ext @@ -179,7 +179,7 @@ class ExtractReview(pyblish.api.InstancePlugin): single_frame_image = False if len(input_filepaths) == 1: ext = os.path.splitext(input_filepaths[0])[-1] - single_frame_image = ext in IMAGE_EXTENSIONS + single_frame_image = ext.lower() in IMAGE_EXTENSIONS filtered_defs = [] for output_def in output_defs: @@ -501,7 +501,7 @@ class ExtractReview(pyblish.api.InstancePlugin): first_sequence_frame += handle_start ext = os.path.splitext(repre["files"][0])[1].replace(".", "") - if ext in self.alpha_exts: + if ext.lower() in self.alpha_exts: input_allow_bg = True return { @@ -934,6 +934,8 @@ class ExtractReview(pyblish.api.InstancePlugin): if output_ext.startswith("."): output_ext = output_ext[1:] + output_ext = output_ext.lower() + # Store extension to representation new_repre["ext"] = output_ext From c028bb2a9446f5a7891a7a42427a62aa0f3a0886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Wed, 16 Nov 2022 18:37:24 +0100 Subject: [PATCH 315/371] Update openpype/client/entities.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/client/entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index bbef8dc65e..38d6369d09 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -389,7 +389,7 @@ def get_subset_by_name(project_name, subset_name, asset_id, fields=None): returned if 'None' is passed. Returns: - Union[str, Dict]: None if subset with specified filters was not found. + Union[None, Dict[str, Any]]: None if subset with specified filters was not found. or dict subset document which can be reduced to specified 'fields'. From 24da47332bbd0951acd621fbb54274153a8a1e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 16 Nov 2022 18:56:18 +0100 Subject: [PATCH 316/371] :bug: fix representation creation --- .../traypublisher/plugins/publish/collect_online_file.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_online_file.py b/openpype/hosts/traypublisher/plugins/publish/collect_online_file.py index 459ee463aa..82c4870fe4 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_online_file.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_online_file.py @@ -13,12 +13,11 @@ class CollectOnlineFile(pyblish.api.InstancePlugin): def process(self, instance): file = Path(instance.data["creator_attributes"]["path"]) - if not instance.data.get("representations"): - instance.data["representations"] = [ - { + instance.data["representations"].append( + { "name": file.suffix.lstrip("."), "ext": file.suffix.lstrip("."), "files": file.name, "stagingDir": file.parent.as_posix() - } - ] + } + ) From 45c6a9ab93a8c5ae0b830190eaabd559d8c369b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 16 Nov 2022 18:56:36 +0100 Subject: [PATCH 317/371] :recycle: refactor code --- .../plugins/create/create_online.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_online.py b/openpype/hosts/traypublisher/plugins/create/create_online.py index 5a6373730d..19f956a50e 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_online.py +++ b/openpype/hosts/traypublisher/plugins/create/create_online.py @@ -38,23 +38,31 @@ class OnlineCreator(TrayPublishCreator): return "fa.file" def create(self, subset_name, instance_data, pre_create_data): - if not pre_create_data.get("representation_file")["filenames"]: + repr_file = pre_create_data.get("representation_file") + if not repr_file: raise CreatorError("No files specified") - asset = get_asset_by_name(self.project_name, instance_data["asset"]) - origin_basename = Path(pre_create_data.get( - "representation_file")["filenames"][0]).stem + files = repr_file.get("filenames") + if not files: + # this should never happen + raise CreatorError("Missing files from representation") + origin_basename = Path(files[0]).stem + + asset = get_asset_by_name( + self.project_name, instance_data["asset"], fields=["_id"]) if get_subset_by_name( - self.project_name, origin_basename, asset["_id"]): + self.project_name, origin_basename, asset["_id"], + fields=["_id"]): raise CreatorError(f"subset with {origin_basename} already " "exists in selected asset") instance_data["originalBasename"] = origin_basename subset_name = origin_basename - path = (Path(pre_create_data.get("representation_file")["directory"]) / pre_create_data.get("representation_file")["filenames"][0]).as_posix() # noqa - instance_data["creator_attributes"] = {"path": path} + instance_data["creator_attributes"] = { + "path": (Path(repr_file["directory"]) / files[0]).as_posix() + } # Create new instance new_instance = CreatedInstance(self.family, subset_name, From 3357392e71c5a1b53747d56c3430897f68a8995b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 16 Nov 2022 18:59:21 +0100 Subject: [PATCH 318/371] :rotating_light: fix :dog: --- openpype/client/entities.py | 4 ++-- .../traypublisher/plugins/publish/collect_online_file.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 38d6369d09..c415be8816 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -389,8 +389,8 @@ def get_subset_by_name(project_name, subset_name, asset_id, fields=None): returned if 'None' is passed. Returns: - Union[None, Dict[str, Any]]: None if subset with specified filters was not found. - or dict subset document which can be reduced to + Union[None, Dict[str, Any]]: None if subset with specified filters was + not found or dict subset document which can be reduced to specified 'fields'. """ diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_online_file.py b/openpype/hosts/traypublisher/plugins/publish/collect_online_file.py index 82c4870fe4..a3f86afa13 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_online_file.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_online_file.py @@ -15,9 +15,9 @@ class CollectOnlineFile(pyblish.api.InstancePlugin): instance.data["representations"].append( { - "name": file.suffix.lstrip("."), - "ext": file.suffix.lstrip("."), - "files": file.name, - "stagingDir": file.parent.as_posix() + "name": file.suffix.lstrip("."), + "ext": file.suffix.lstrip("."), + "files": file.name, + "stagingDir": file.parent.as_posix() } ) From 64e5af230a3509ef16d8c0ee0fc826284960232b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 18 Nov 2022 12:54:50 +0100 Subject: [PATCH 319/371] OP-4394 - removed explicit lower from repre ext to not shadow upper case issue Using lower here would hide possibly broken representation, as we would expect both repre["ext"] and repre["name"] be lowercased. In case the aren't review won't get created >> someone will notice and fix issues on source representation. --- openpype/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 982bd9dc24..f299d1c6e9 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -152,7 +152,7 @@ class ExtractReview(pyblish.api.InstancePlugin): if input_ext.startswith("."): input_ext = input_ext[1:] - if input_ext.lower() not in self.supported_exts: + if input_ext not in self.supported_exts: self.log.info( "Representation has unsupported extension \"{}\"".format( input_ext From 69ddc20e3c4003db2285d2095f45c0e585cae001 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 18 Nov 2022 14:34:42 +0100 Subject: [PATCH 320/371] include secrets module to python_2 vendor --- .../vendor/python/python_2/secrets/LICENSE | 21 +++ .../python/python_2/secrets/__init__.py | 16 +++ .../vendor/python/python_2/secrets/secrets.py | 132 ++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 openpype/vendor/python/python_2/secrets/LICENSE create mode 100644 openpype/vendor/python/python_2/secrets/__init__.py create mode 100644 openpype/vendor/python/python_2/secrets/secrets.py diff --git a/openpype/vendor/python/python_2/secrets/LICENSE b/openpype/vendor/python/python_2/secrets/LICENSE new file mode 100644 index 0000000000..d3211e4d9f --- /dev/null +++ b/openpype/vendor/python/python_2/secrets/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Scaleway + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/openpype/vendor/python/python_2/secrets/__init__.py b/openpype/vendor/python/python_2/secrets/__init__.py new file mode 100644 index 0000000000..c29ee61be1 --- /dev/null +++ b/openpype/vendor/python/python_2/secrets/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + + +__version__ = "1.0.6" + +# Emulates __all__ for Python2 +from .secrets import ( + choice, + randbelow, + randbits, + SystemRandom, + token_bytes, + token_hex, + token_urlsafe, + compare_digest +) diff --git a/openpype/vendor/python/python_2/secrets/secrets.py b/openpype/vendor/python/python_2/secrets/secrets.py new file mode 100644 index 0000000000..967d2862d9 --- /dev/null +++ b/openpype/vendor/python/python_2/secrets/secrets.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +"""Generate cryptographically strong pseudo-random numbers suitable for + +managing secrets such as account authentication, tokens, and similar. + + +See PEP 506 for more information. + +https://www.python.org/dev/peps/pep-0506/ + + +""" + + +__all__ = ['choice', 'randbelow', 'randbits', 'SystemRandom', + + 'token_bytes', 'token_hex', 'token_urlsafe', + + 'compare_digest', + + ] + +import os +import sys +from random import SystemRandom + +import base64 + +import binascii + + +# hmac.compare_digest did appear in python 2.7.7 +if sys.version_info >= (2, 7, 7): + from hmac import compare_digest +else: + # If we use an older python version, we will define an equivalent method + def compare_digest(a, b): + """Compatibility compare_digest method for python < 2.7. + This method is NOT cryptographically secure and may be subject to + timing attacks, see https://docs.python.org/2/library/hmac.html + """ + return a == b + + +_sysrand = SystemRandom() + + +randbits = _sysrand.getrandbits + +choice = _sysrand.choice + + +def randbelow(exclusive_upper_bound): + + """Return a random int in the range [0, n).""" + + if exclusive_upper_bound <= 0: + + raise ValueError("Upper bound must be positive.") + + return _sysrand._randbelow(exclusive_upper_bound) + + +DEFAULT_ENTROPY = 32 # number of bytes to return by default + + +def token_bytes(nbytes=None): + + """Return a random byte string containing *nbytes* bytes. + + + If *nbytes* is ``None`` or not supplied, a reasonable + + default is used. + + + >>> token_bytes(16) #doctest:+SKIP + + b'\\xebr\\x17D*t\\xae\\xd4\\xe3S\\xb6\\xe2\\xebP1\\x8b' + + + """ + + if nbytes is None: + + nbytes = DEFAULT_ENTROPY + + return os.urandom(nbytes) + + +def token_hex(nbytes=None): + + """Return a random text string, in hexadecimal. + + + The string has *nbytes* random bytes, each byte converted to two + + hex digits. If *nbytes* is ``None`` or not supplied, a reasonable + + default is used. + + + >>> token_hex(16) #doctest:+SKIP + + 'f9bf78b9a18ce6d46a0cd2b0b86df9da' + + + """ + + return binascii.hexlify(token_bytes(nbytes)).decode('ascii') + + +def token_urlsafe(nbytes=None): + + """Return a random URL-safe text string, in Base64 encoding. + + + The string has *nbytes* random bytes. If *nbytes* is ``None`` + + or not supplied, a reasonable default is used. + + + >>> token_urlsafe(16) #doctest:+SKIP + + 'Drmhze6EPcv0fN_81Bj-nA' + + + """ + + tok = token_bytes(nbytes) + + return base64.urlsafe_b64encode(tok).rstrip(b'=').decode('ascii') From 2db4cc43aae80fa8fb203ba775fff6fbe19a23c0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 18 Nov 2022 17:52:57 +0100 Subject: [PATCH 321/371] Fix - typo --- openpype/hooks/pre_copy_last_published_workfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/hooks/pre_copy_last_published_workfile.py index 44144e5fff..26b43c39cb 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/hooks/pre_copy_last_published_workfile.py @@ -38,7 +38,7 @@ class CopyLastPublishedWorkfile(PreLaunchHook): sync_server = self.modules_manager.get("sync_server") if not sync_server or not sync_server.enabled: - self.log.deubg("Sync server module is not enabled or available") + self.log.debug("Sync server module is not enabled or available") return # Check there is no workfile available From d076de0d077197bf3afc64edf9ab08837f2db549 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 18 Nov 2022 18:47:37 +0100 Subject: [PATCH 322/371] add more information about where ftrack service is storing versions or where is looking for versions --- openpype/modules/ftrack/scripts/sub_event_status.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/modules/ftrack/scripts/sub_event_status.py b/openpype/modules/ftrack/scripts/sub_event_status.py index 6c7ecb8351..eb3f63c04b 100644 --- a/openpype/modules/ftrack/scripts/sub_event_status.py +++ b/openpype/modules/ftrack/scripts/sub_event_status.py @@ -7,6 +7,8 @@ import signal import socket import datetime +import appdirs + import ftrack_api from openpype_modules.ftrack.ftrack_server.ftrack_server import FtrackServer from openpype_modules.ftrack.ftrack_server.lib import ( @@ -253,6 +255,15 @@ class StatusFactory: ) }) + items.append({ + "type": "label", + "value": ( + "Local versions dir: {}
Version repository path: {}" + ).format( + appdirs.user_data_dir("openpype", "pypeclub"), + os.environ.get("OPENPYPE_PATH") + ) + }) items.append({"type": "label", "value": "---"}) return items From 996cf3dcf95cd5042b2433780406ec5e74f1ae30 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 18 Nov 2022 20:52:55 +0100 Subject: [PATCH 323/371] Nuke: load image first frame --- openpype/hosts/nuke/plugins/load/load_image.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index 3e81ef999b..3c5d4a7fc1 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -62,7 +62,9 @@ class LoadImage(load.LoaderPlugin): def load(self, context, name, namespace, options): self.log.info("__ options: `{}`".format(options)) - frame_number = options.get("frame_number", 1) + frame_number = options.get( + "frame_number", int(nuke.root()["first_frame"].getValue()) + ) version = context['version'] version_data = version.get("data", {}) From 8d1e720a889ebabc985505f0165ec11c4d6f7342 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 18 Nov 2022 21:19:23 +0100 Subject: [PATCH 324/371] Nuke: reset tab to first native tab --- openpype/hosts/nuke/api/pipeline.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index c343c635fa..fb707ca44c 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -364,6 +364,9 @@ def containerise(node, set_avalon_knob_data(node, data) + # set tab to first native + node.setTab(0) + return node From c06f6891e8b021ab6e67f70a080202161059d8e8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 18 Nov 2022 21:19:46 +0100 Subject: [PATCH 325/371] nuke: close property panel after node creation --- openpype/hosts/nuke/plugins/load/load_camera_abc.py | 3 +++ openpype/hosts/nuke/plugins/load/load_clip.py | 3 +++ openpype/hosts/nuke/plugins/load/load_effects.py | 3 +++ openpype/hosts/nuke/plugins/load/load_effects_ip.py | 3 +++ openpype/hosts/nuke/plugins/load/load_image.py | 4 ++++ openpype/hosts/nuke/plugins/load/load_model.py | 4 ++++ openpype/hosts/nuke/plugins/load/load_script_precomp.py | 3 +++ 7 files changed, 23 insertions(+) diff --git a/openpype/hosts/nuke/plugins/load/load_camera_abc.py b/openpype/hosts/nuke/plugins/load/load_camera_abc.py index f5dfc8c0ab..9fef7424c8 100644 --- a/openpype/hosts/nuke/plugins/load/load_camera_abc.py +++ b/openpype/hosts/nuke/plugins/load/load_camera_abc.py @@ -65,6 +65,9 @@ class AlembicCameraLoader(load.LoaderPlugin): object_name, file), inpanel=False ) + # hide property panel + camera_node.hideControlPanel() + camera_node.forceValidate() camera_node["frame_rate"].setValue(float(fps)) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index b17356c5c7..565d777811 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -145,6 +145,9 @@ class LoadClip(plugin.NukeLoader): "Read", "name {}".format(read_name)) + # hide property panel + read_node.hideControlPanel() + # to avoid multiple undo steps for rest of process # we will switch off undo-ing with viewer_update_and_undo_stop(): diff --git a/openpype/hosts/nuke/plugins/load/load_effects.py b/openpype/hosts/nuke/plugins/load/load_effects.py index d164e0604c..cef4b0a5fc 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects.py +++ b/openpype/hosts/nuke/plugins/load/load_effects.py @@ -89,6 +89,9 @@ class LoadEffects(load.LoaderPlugin): "Group", "name {}_1".format(object_name)) + # hide property panel + GN.hideControlPanel() + # adding content to the group node with GN: pre_node = nuke.createNode("Input") diff --git a/openpype/hosts/nuke/plugins/load/load_effects_ip.py b/openpype/hosts/nuke/plugins/load/load_effects_ip.py index 44565c139d..9bd40be816 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_effects_ip.py @@ -90,6 +90,9 @@ class LoadEffectsInputProcess(load.LoaderPlugin): "Group", "name {}_1".format(object_name)) + # hide property panel + GN.hideControlPanel() + # adding content to the group node with GN: pre_node = nuke.createNode("Input") diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index 3e81ef999b..f7ce20eee9 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -112,6 +112,10 @@ class LoadImage(load.LoaderPlugin): r = nuke.createNode( "Read", "name {}".format(read_name)) + + # hide property panel + r.hideControlPanel() + r["file"].setValue(file) # Set colorspace defined in version data diff --git a/openpype/hosts/nuke/plugins/load/load_model.py b/openpype/hosts/nuke/plugins/load/load_model.py index 151401bad3..ad985e83c6 100644 --- a/openpype/hosts/nuke/plugins/load/load_model.py +++ b/openpype/hosts/nuke/plugins/load/load_model.py @@ -63,6 +63,10 @@ class AlembicModelLoader(load.LoaderPlugin): object_name, file), inpanel=False ) + + # hide property panel + model_node.hideControlPanel() + model_node.forceValidate() # Ensure all items are imported and selected. diff --git a/openpype/hosts/nuke/plugins/load/load_script_precomp.py b/openpype/hosts/nuke/plugins/load/load_script_precomp.py index 21e384b538..f0972f85d2 100644 --- a/openpype/hosts/nuke/plugins/load/load_script_precomp.py +++ b/openpype/hosts/nuke/plugins/load/load_script_precomp.py @@ -71,6 +71,9 @@ class LinkAsGroup(load.LoaderPlugin): "Precomp", "file {}".format(file)) + # hide property panel + P.hideControlPanel() + # Set colorspace defined in version data colorspace = context["version"]["data"].get("colorspace", None) self.log.info("colorspace: {}\n".format(colorspace)) From 554b3b256c4bfb368bb808376088e3315df54127 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 19 Nov 2022 03:37:23 +0000 Subject: [PATCH 326/371] [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 268f33083a..0116b49f4d 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.5" +__version__ = "3.14.7-nightly.6" From 33974c39d4aac0bb28ebd87e007acc166b8cd003 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 19 Nov 2022 17:13:50 +0800 Subject: [PATCH 327/371] aov Filtering --- openpype/hosts/maya/api/lib_renderproducts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 58fcd2d281..6fde0df162 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1110,7 +1110,7 @@ class RenderProductsRedshift(ARenderProducts): if light_groups_enabled: return products - beauty_name = "Beauty_other" if has_beauty_aov else "" + beauty_name = "BeautyAux" if has_beauty_aov else "" for camera in cameras: products.insert(0, RenderProduct(productName=beauty_name, From 94939e431a18cc45472276f01f96c71e5187dfc8 Mon Sep 17 00:00:00 2001 From: clement hector Date: Mon, 21 Nov 2022 16:18:26 +0100 Subject: [PATCH 328/371] rename families_to_upload to families_to_review + define it as class attribute --- openpype/hosts/tvpaint/plugins/publish/extract_sequence.py | 3 ++- openpype/settings/defaults/project_settings/tvpaint.json | 5 ++--- .../schemas/projects_schema/schema_project_tvpaint.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index d8aef1ab6b..7d2e9c6f25 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -19,6 +19,7 @@ class ExtractSequence(pyblish.api.Extractor): label = "Extract Sequence" hosts = ["tvpaint"] families = ["review", "renderPass", "renderLayer", "renderScene"] + families_to_review = ["review"] # Modifiable with settings review_bg = [255, 255, 255, 255] @@ -129,7 +130,7 @@ class ExtractSequence(pyblish.api.Extractor): # Fill tags and new families from project settings tags = [] - if family_lowered in self.families_to_upload: + if family_lowered in self.families_to_review: tags.append("review") # Sequence of one frame diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 2e413f50cd..9ccc318d70 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -12,11 +12,10 @@ 255, 255 ], - "families_to_upload": [ + "families_to_review": [ "review", "renderpass", - "renderlayer", - "renderscene" + "renderlayer" ] }, "ValidateProjectSettings": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 0392c9089b..61342ef738 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -59,8 +59,8 @@ }, { "type": "enum", - "key": "families_to_upload", - "label": "Families to upload", + "key": "families_to_review", + "label": "Families to review", "multiselection": true, "enum_items": [ {"review": "review"}, From cdb91c03795db7bc9b249e69dd605769562c11bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 21 Nov 2022 17:56:02 +0100 Subject: [PATCH 329/371] Added helper class for version resolving and sorting --- .../custom/plugins/GlobalJobPreLoad.py | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 9b35c9502d..6c3dd092fe 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -14,6 +14,137 @@ from Deadline.Scripting import ( ProcessUtils, ) +VERSION_REGEX = re.compile( + r"(?P0|[1-9]\d*)" + r"\.(?P0|[1-9]\d*)" + r"\.(?P0|[1-9]\d*)" + r"(?:-(?P[a-zA-Z\d\-.]*))?" + r"(?:\+(?P[a-zA-Z\d\-.]*))?" +) + + +class OpenPypeVersion: + """Fake semver version class for OpenPype version purposes. + + The version + """ + def __init__(self, major, minor, patch, prerelease, origin=None): + self.major = major + self.minor = minor + self.patch = patch + self.prerelease = prerelease + + is_valid = True + if not major or not minor or not patch: + is_valid = False + self.is_valid = is_valid + + if origin is None: + base = "{}.{}.{}".format(str(major), str(minor), str(patch)) + if not prerelease: + origin = base + else: + origin = "{}-{}".format(base, str(prerelease)) + + self.origin = origin + + @classmethod + def from_string(cls, version): + """Create an object of version from string. + + Args: + version (str): Version as a string. + + Returns: + Union[OpenPypeVersion, None]: Version object if input is nonempty + string otherwise None. + """ + + if not version: + return None + valid_parts = VERSION_REGEX.findall(version) + if len(valid_parts) != 1: + # Return invalid version with filled 'origin' attribute + return cls(None, None, None, None, origin=str(version)) + + # Unpack found version + major, minor, patch, pre, post = valid_parts[0] + prerelease = pre + # Post release is not important anymore and should be considered as + # part of prerelease + # - comparison is implemented to find suitable build and builds should + # never contain prerelease part so "not proper" parsing is + # acceptable for this use case. + if post: + prerelease = "{}+{}".format(pre, post) + + return cls( + int(major), int(minor), int(patch), prerelease, origin=version + ) + + def has_compatible_release(self, other): + """Version has compatible release as other version. + + Both major and minor versions must be exactly the same. In that case + a build can be considered as release compatible with any version. + + Args: + other (OpenPypeVersion): Other version. + + Returns: + bool: Version is release compatible with other version. + """ + + if self.is_valid and other.is_valid: + return self.major == other.major and self.minor == other.minor + return False + + def __bool__(self): + return self.is_valid + + def __repr__(self): + return "<{} {}>".format(self.__class__.__name__, self.origin) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return self.origin == other + return self.origin == other.origin + + def __lt__(self, other): + if not isinstance(other, self.__class__): + return None + + if not self.is_valid: + return True + + if not other.is_valid: + return False + + if self.origin == other.origin: + return None + + same_major = self.major == other.major + if not same_major: + return self.major < other.major + + same_minor = self.minor == other.minor + if not same_minor: + return self.minor < other.minor + + same_patch = self.patch == other.patch + if not same_patch: + return self.patch < other.patch + + if not self.prerelease: + return False + + if not other.prerelease: + return True + + pres = [self.prerelease, other.prerelease] + pres.sort() + return pres[0] == self.prerelease + def get_openpype_version_from_path(path, build=True): """Get OpenPype version from provided path. From b1e899d8ee2a79cd673bdf14bf4adf2134443dca Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 21 Nov 2022 17:57:10 +0100 Subject: [PATCH 330/371] Use full version for resolving and use specific build if matches requested version --- .../custom/plugins/GlobalJobPreLoad.py | 197 ++++++++++-------- 1 file changed, 110 insertions(+), 87 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 6c3dd092fe..375cf48b8f 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -152,9 +152,9 @@ def get_openpype_version_from_path(path, build=True): build (bool, optional): Get only builds, not sources Returns: - str or None: version of OpenPype if found. - + Union[OpenPypeVersion, None]: version of OpenPype if found. """ + # fix path for application bundle on macos if platform.system().lower() == "darwin": path = os.path.join(path, "Contents", "MacOS", "lib", "Python") @@ -177,8 +177,10 @@ def get_openpype_version_from_path(path, build=True): with open(version_file, "r") as vf: exec(vf.read(), version) - version_match = re.search(r"(\d+\.\d+.\d+).*", version["__version__"]) - return version_match[1] + version_str = version.get("__version__") + if version_str: + return OpenPypeVersion.from_string(version_str) + return None def get_openpype_executable(): @@ -190,6 +192,91 @@ def get_openpype_executable(): return exe_list, dir_list +def get_openpype_versions(exe_list, dir_list): + print(">>> Getting OpenPype executable ...") + openpype_versions = [] + + install_dir = DirectoryUtils.SearchDirectoryList(dir_list) + if install_dir: + print("--- Looking for OpenPype at: {}".format(install_dir)) + sub_dirs = [ + f.path for f in os.scandir(install_dir) + if f.is_dir() + ] + for subdir in sub_dirs: + version = get_openpype_version_from_path(subdir) + if not version: + continue + print(" - found: {} - {}".format(version, subdir)) + openpype_versions.append((version, subdir)) + return openpype_versions + + +def get_requested_openpype_executable( + exe, dir_list, requested_version +): + requested_version_obj = OpenPypeVersion.from_string(requested_version) + if not requested_version_obj: + print(( + ">>> Requested version does not match version regex \"{}\"" + ).format(VERSION_REGEX)) + return None + + print(( + ">>> Scanning for compatible requested version {}" + ).format(requested_version)) + openpype_versions = get_openpype_versions(dir_list) + if not openpype_versions: + return None + + # if looking for requested compatible version, + # add the implicitly specified to the list too. + if exe: + exe_dir = os.path.dirname(exe) + print("Looking for OpenPype at: {}".format(exe_dir)) + version = get_openpype_version_from_path(exe_dir) + if version: + print(" - found: {} - {}".format(version, exe_dir)) + openpype_versions.append((version, exe_dir)) + + matching_item = None + compatible_versions = [] + for version_item in openpype_versions: + version, version_dir = version_item + if requested_version_obj.has_compatible_release(version): + compatible_versions.append(version_item) + if version == requested_version_obj: + # Store version item if version match exactly + # - break if is found matching version + matching_item = version_item + break + + if not compatible_versions: + return None + + compatible_versions.sort(key=lambda item: item[0]) + if matching_item: + version, version_dir = matching_item + print(( + "*** Found exact match build version {} in {}" + ).format(version_dir, version)) + + else: + version, version_dir = compatible_versions[-1] + + print(( + "*** Latest compatible version found is {} in {}" + ).format(version_dir, version)) + + # create list of executables for different platform and let + # Deadline decide. + exe_list = [ + os.path.join(version_dir, "openpype_console.exe"), + os.path.join(version_dir, "openpype_console") + ] + return FileUtils.SearchFileList(";".join(exe_list)) + + def inject_openpype_environment(deadlinePlugin): """ Pull env vars from OpenPype and push them to rendering process. @@ -199,93 +286,29 @@ def inject_openpype_environment(deadlinePlugin): print(">>> Injecting OpenPype environments ...") try: - print(">>> Getting OpenPype executable ...") exe_list, dir_list = get_openpype_executable() - openpype_versions = [] - # if the job requires specific OpenPype version, - # lets go over all available and find compatible build. + exe = FileUtils.SearchFileList(exe_list) + requested_version = job.GetJobEnvironmentKeyValue("OPENPYPE_VERSION") if requested_version: - print(( - ">>> Scanning for compatible requested version {}" - ).format(requested_version)) - install_dir = DirectoryUtils.SearchDirectoryList(dir_list) - if install_dir: - print("--- Looking for OpenPype at: {}".format(install_dir)) - sub_dirs = [ - f.path for f in os.scandir(install_dir) - if f.is_dir() - ] - for subdir in sub_dirs: - version = get_openpype_version_from_path(subdir) - if not version: - continue - print(" - found: {} - {}".format(version, subdir)) - openpype_versions.append((version, subdir)) + exe = get_requested_openpype_executable( + exe, dir_list, requested_version + ) + if exe is None: + raise RuntimeError(( + "Cannot find compatible version available for version {}" + " requested by the job. Please add it through plugin" + " configuration in Deadline or install it to configured" + " directory." + ).format(requested_version)) - exe = FileUtils.SearchFileList(exe_list) - if openpype_versions: - # if looking for requested compatible version, - # add the implicitly specified to the list too. - print("Looking for OpenPype at: {}".format(os.path.dirname(exe))) - version = get_openpype_version_from_path( - os.path.dirname(exe)) - if version: - print(" - found: {} - {}".format( - version, os.path.dirname(exe) - )) - openpype_versions.append((version, os.path.dirname(exe))) - - if requested_version: - # sort detected versions - if openpype_versions: - # use natural sorting - openpype_versions.sort( - key=lambda ver: [ - int(t) if t.isdigit() else t.lower() - for t in re.split(r"(\d+)", ver[0]) - ]) - print(( - "*** Latest available version found is {}" - ).format(openpype_versions[-1][0])) - requested_major, requested_minor, _ = requested_version.split(".")[:3] # noqa: E501 - compatible_versions = [] - for version in openpype_versions: - v = version[0].split(".")[:3] - if v[0] == requested_major and v[1] == requested_minor: - compatible_versions.append(version) - if not compatible_versions: - raise RuntimeError( - ("Cannot find compatible version available " - "for version {} requested by the job. " - "Please add it through plugin configuration " - "in Deadline or install it to configured " - "directory.").format(requested_version)) - # sort compatible versions nad pick the last one - compatible_versions.sort( - key=lambda ver: [ - int(t) if t.isdigit() else t.lower() - for t in re.split(r"(\d+)", ver[0]) - ]) - print(( - "*** Latest compatible version found is {}" - ).format(compatible_versions[-1][0])) - # create list of executables for different platform and let - # Deadline decide. - exe_list = [ - os.path.join( - compatible_versions[-1][1], "openpype_console.exe"), - os.path.join( - compatible_versions[-1][1], "openpype_console") - ] - exe = FileUtils.SearchFileList(";".join(exe_list)) - if exe == "": - raise RuntimeError( - "OpenPype executable was not found " + - "in the semicolon separated list " + - "\"" + ";".join(exe_list) + "\". " + - "The path to the render executable can be configured " + - "from the Plugin Configuration in the Deadline Monitor.") + if not exe: + raise RuntimeError(( + "OpenPype executable was not found in the semicolon " + "separated list \"{}\"." + "The path to the render executable can be configured" + " from the Plugin Configuration in the Deadline Monitor." + ).format(";".join(exe_list))) print("--- OpenPype executable: {}".format(exe)) From dbc72502b4cbf9859493d43ce90141f84ecc9420 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 21 Nov 2022 17:57:37 +0100 Subject: [PATCH 331/371] few formatting changes --- .../custom/plugins/GlobalJobPreLoad.py | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 375cf48b8f..78e1371eee 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -326,22 +326,22 @@ def inject_openpype_environment(deadlinePlugin): export_url ] - add_args = {} - add_args['project'] = \ - job.GetJobEnvironmentKeyValue('AVALON_PROJECT') - add_args['asset'] = job.GetJobEnvironmentKeyValue('AVALON_ASSET') - add_args['task'] = job.GetJobEnvironmentKeyValue('AVALON_TASK') - add_args['app'] = job.GetJobEnvironmentKeyValue('AVALON_APP_NAME') - add_args["envgroup"] = "farm" + add_kwargs = { + "project": job.GetJobEnvironmentKeyValue("AVALON_PROJECT"), + "asset": job.GetJobEnvironmentKeyValue("AVALON_ASSET"), + "task": job.GetJobEnvironmentKeyValue("AVALON_TASK"), + "app": job.GetJobEnvironmentKeyValue("AVALON_APP_NAME"), + "envgroup": "farm" + } + if all(add_kwargs.values()): + for key, value in add_kwargs.items(): + args.extend(["--{}".format(key), value]) - if all(add_args.values()): - for key, value in add_args.items(): - args.append("--{}".format(key)) - args.append(value) else: - msg = "Required env vars: AVALON_PROJECT, AVALON_ASSET, " + \ - "AVALON_TASK, AVALON_APP_NAME" - raise RuntimeError(msg) + raise RuntimeError(( + "Missing required env vars: AVALON_PROJECT, AVALON_ASSET," + " AVALON_TASK, AVALON_APP_NAME" + )) if not os.environ.get("OPENPYPE_MONGO"): print(">>> Missing OPENPYPE_MONGO env var, process won't work") @@ -362,12 +362,12 @@ def inject_openpype_environment(deadlinePlugin): print(">>> Loading file ...") with open(export_url) as fp: contents = json.load(fp) - for key, value in contents.items(): - deadlinePlugin.SetProcessEnvironmentVariable(key, value) + + for key, value in contents.items(): + deadlinePlugin.SetProcessEnvironmentVariable(key, value) script_url = job.GetJobPluginInfoKeyValue("ScriptFilename") if script_url: - script_url = script_url.format(**contents).replace("\\", "/") print(">>> Setting script path {}".format(script_url)) job.SetJobPluginInfoKeyValue("ScriptFilename", script_url) From 61e5dc3fc9c326a90601e774722ae30b419ef390 Mon Sep 17 00:00:00 2001 From: Thomas Fricard <51854004+friquette@users.noreply.github.com> Date: Mon, 21 Nov 2022 18:21:04 +0100 Subject: [PATCH 332/371] change order of default value Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/settings/defaults/project_settings/tvpaint.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 9ccc318d70..e03ce32030 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -14,8 +14,8 @@ ], "families_to_review": [ "review", - "renderpass", - "renderlayer" + "renderlayer", + "renderscene" ] }, "ValidateProjectSettings": { From e24c2f853b5b976e1c441470cf6e7f435e2c0815 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 21 Nov 2022 19:15:34 +0100 Subject: [PATCH 333/371] attribute definitions can be hidden and disabled --- openpype/lib/attribute_definitions.py | 32 +++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 6baeaec045..ed151bbe4e 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -105,11 +105,14 @@ class AbtractAttrDef(object): How to force to set `key` attribute? Args: - key(str): Under which key will be attribute value stored. - label(str): Attribute label. - tooltip(str): Attribute tooltip. - is_label_horizontal(bool): UI specific argument. Specify if label is + key (str): Under which key will be attribute value stored. + default (Any): Default value of an attribute. + label (str): Attribute label. + tooltip (str): Attribute tooltip. + is_label_horizontal (bool): UI specific argument. Specify if label is next to value input or ahead. + hidden (bool): Will be item hidden (for UI purposes). + disabled (bool): Item will be visible but disabled (for UI purposes). """ type_attributes = [] @@ -117,16 +120,29 @@ class AbtractAttrDef(object): is_value_def = True def __init__( - self, key, default, label=None, tooltip=None, is_label_horizontal=None + self, + key, + default, + label=None, + tooltip=None, + is_label_horizontal=None, + hidden=False, + disabled=False ): if is_label_horizontal is None: is_label_horizontal = True + + if hidden is None: + hidden = False + self.key = key self.label = label self.tooltip = tooltip self.default = default self.is_label_horizontal = is_label_horizontal - self._id = uuid.uuid4() + self.hidden = hidden + self.disabled = disabled + self._id = uuid.uuid4().hex self.__init__class__ = AbtractAttrDef @@ -173,7 +189,9 @@ class AbtractAttrDef(object): "label": self.label, "tooltip": self.tooltip, "default": self.default, - "is_label_horizontal": self.is_label_horizontal + "is_label_horizontal": self.is_label_horizontal, + "hidden": self.hidden, + "disabled": self.disabled } for attr in self.type_attributes: data[attr] = getattr(self, attr) From 6abfa14e01d67eae20e7bb66c219feab99d70a37 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 21 Nov 2022 19:16:02 +0100 Subject: [PATCH 334/371] added special definition for hidden values --- openpype/lib/attribute_definitions.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index ed151bbe4e..0df7b16e64 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -253,6 +253,26 @@ class UnknownDef(AbtractAttrDef): return value +class HiddenDef(AbtractAttrDef): + """Hidden value of Any type. + + This attribute can be used for UI purposes to pass values related + to other attributes (e.g. in multi-page UIs). + + Keep in mind the value should be possible to parse by json parser. + """ + + type = "hidden" + + def __init__(self, key, default=None, **kwargs): + kwargs["default"] = default + kwargs["hidden"] = True + super(UnknownDef, self).__init__(key, **kwargs) + + def convert_value(self, value): + return value + + class NumberDef(AbtractAttrDef): """Number definition. From fe392aa5db267ef09e0152d867eb02e45fee065e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 21 Nov 2022 19:16:41 +0100 Subject: [PATCH 335/371] implemented hidden widget --- openpype/tools/attribute_defs/widgets.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py index dc697b08a6..7f7c20009e 100644 --- a/openpype/tools/attribute_defs/widgets.py +++ b/openpype/tools/attribute_defs/widgets.py @@ -6,6 +6,7 @@ from Qt import QtWidgets, QtCore from openpype.lib.attribute_definitions import ( AbtractAttrDef, UnknownDef, + HiddenDef, NumberDef, TextDef, EnumDef, @@ -459,6 +460,29 @@ class UnknownAttrWidget(_BaseAttrDefWidget): self._input_widget.setText(str_value) +class HiddenAttrWidget(_BaseAttrDefWidget): + def _ui_init(self): + self.setVisible(False) + self._value = None + self._multivalue = False + + def setVisible(self, visible): + if visible: + visible = False + super(HiddenAttrWidget, self).setVisible(visible) + + def current_value(self): + if self._multivalue: + raise ValueError( + "{} can't output for multivalue.".format(self.__class__.__name__) + ) + return self._value + + def set_value(self, value, multivalue=False): + self._value = copy.deepcopy(value) + self._multivalue = multivalue + + class FileAttrWidget(_BaseAttrDefWidget): def _ui_init(self): input_widget = FilesWidget( From 068ec3f89809eca1fcff32d81e36158f88dc248a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 21 Nov 2022 19:17:09 +0100 Subject: [PATCH 336/371] enhanced attribute definitons widget --- openpype/tools/attribute_defs/widgets.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py index 7f7c20009e..6db6da58e1 100644 --- a/openpype/tools/attribute_defs/widgets.py +++ b/openpype/tools/attribute_defs/widgets.py @@ -23,6 +23,16 @@ from .files_widget import FilesWidget def create_widget_for_attr_def(attr_def, parent=None): + widget = _create_widget_for_attr_def(attr_def, parent) + if attr_def.hidden: + widget.setVisible(False) + + if attr_def.disabled: + widget.setEnabled(False) + return widget + + +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 @@ -43,6 +53,9 @@ def create_widget_for_attr_def(attr_def, parent=None): if isinstance(attr_def, UnknownDef): return UnknownAttrWidget(attr_def, parent) + if isinstance(attr_def, HiddenDef): + return HiddenAttrWidget(attr_def, parent) + if isinstance(attr_def, FileDef): return FileAttrWidget(attr_def, parent) @@ -116,6 +129,10 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): self._current_keys.add(attr_def.key) widget = create_widget_for_attr_def(attr_def, self) + self._widgets.append(widget) + + if attr_def.hidden: + continue expand_cols = 2 if attr_def.is_value_def and attr_def.is_label_horizontal: @@ -134,7 +151,6 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): layout.addWidget( widget, row, col_num, 1, expand_cols ) - self._widgets.append(widget) row += 1 def set_value(self, value): From a606de5b76b63a6051731f292daa7b0420bfbbde Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 21 Nov 2022 19:17:56 +0100 Subject: [PATCH 337/371] don't add hidden widgets to publisher widgets --- openpype/tools/publisher/widgets/widgets.py | 26 +++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index ce3d91ce63..a0d97245ba 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -9,6 +9,7 @@ import collections from Qt import QtWidgets, QtCore, QtGui import qtawesome +from openpype.lib.attribute_definitions import UnknownDef from openpype.tools.attribute_defs import create_widget_for_attr_def from openpype.tools import resources from openpype.tools.flickcharm import FlickCharm @@ -1303,6 +1304,13 @@ class CreatorAttrsWidget(QtWidgets.QWidget): else: widget.set_value(values, True) + widget.value_changed.connect(self._input_value_changed) + self._attr_def_id_to_instances[attr_def.id] = attr_instances + self._attr_def_id_to_attr_def[attr_def.id] = attr_def + + if attr_def.hidden: + continue + expand_cols = 2 if attr_def.is_value_def and attr_def.is_label_horizontal: expand_cols = 1 @@ -1321,13 +1329,8 @@ class CreatorAttrsWidget(QtWidgets.QWidget): content_layout.addWidget( widget, row, col_num, 1, expand_cols ) - row += 1 - widget.value_changed.connect(self._input_value_changed) - self._attr_def_id_to_instances[attr_def.id] = attr_instances - self._attr_def_id_to_attr_def[attr_def.id] = attr_def - self._scroll_area.setWidget(content_widget) self._content_widget = content_widget @@ -1421,8 +1424,17 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): widget = create_widget_for_attr_def( attr_def, content_widget ) - label = attr_def.label or attr_def.key - content_layout.addRow(label, widget) + hidden_widget = attr_def.hidden + # Hide unknown values of publish plugins + # - The keys in most of cases does not represent what would + # label represent + if isinstance(attr_def, UnknownDef): + widget.setVisible(False) + hidden_widget = True + + if not hidden_widget: + label = attr_def.label or attr_def.key + content_layout.addRow(label, widget) widget.value_changed.connect(self._input_value_changed) From 29cc9bdce61ea3ce1dc01d17bb05c3a2db3afffe Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 21 Nov 2022 19:39:10 +0100 Subject: [PATCH 338/371] Fix line length --- openpype/tools/attribute_defs/widgets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py index 6db6da58e1..1ffb3d3799 100644 --- a/openpype/tools/attribute_defs/widgets.py +++ b/openpype/tools/attribute_defs/widgets.py @@ -489,9 +489,9 @@ class HiddenAttrWidget(_BaseAttrDefWidget): def current_value(self): if self._multivalue: - raise ValueError( - "{} can't output for multivalue.".format(self.__class__.__name__) - ) + raise ValueError("{} can't output for multivalue.".format( + self.__class__.__name__ + )) return self._value def set_value(self, value, multivalue=False): From 7bf1d0bc9b2efb05e9904a4784ded5ea2da5b717 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 22 Nov 2022 18:56:20 +0800 Subject: [PATCH 339/371] aov filtering --- openpype/hosts/maya/api/lib_renderproducts.py | 1 - .../modules/deadline/plugins/publish/submit_publish_job.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 6fde0df162..c54e3ab3e0 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1054,7 +1054,6 @@ class RenderProductsRedshift(ARenderProducts): # Any AOVs that still get processed, like Cryptomatte # by themselves are not multipart files. - # aov_multipart = not multipart # Redshift skips rendering of masterlayer without AOV suffix # when a Beauty AOV is rendered. It overrides the main layer. diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index c1e9dd4015..6362b4ca65 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -500,7 +500,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if instance_data.get("multipartExr"): preview = True - self.log.info("preview:{}".format(preview)) + self.log.debug("preview:{}".format(preview)) new_instance = deepcopy(instance_data) new_instance["subset"] = subset_name new_instance["subsetGroup"] = group_name @@ -543,7 +543,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if new_instance.get("extendFrames", False): self._copy_extend_frames(new_instance, rep) instances.append(new_instance) - self.log.info("instances:{}".format(instances)) + self.log.debug("instances:{}".format(instances)) return instances def _get_representations(self, instance, exp_files): From 996bd4897b80ae72a06a8bbc81c1b69d471485ac Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 22 Nov 2022 12:04:47 +0100 Subject: [PATCH 340/371] tabs widget can set current tab by index --- openpype/tools/publisher/widgets/tabs_widget.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/tools/publisher/widgets/tabs_widget.py b/openpype/tools/publisher/widgets/tabs_widget.py index 84638a002c..eb3eda8c19 100644 --- a/openpype/tools/publisher/widgets/tabs_widget.py +++ b/openpype/tools/publisher/widgets/tabs_widget.py @@ -68,7 +68,16 @@ class PublisherTabsWidget(QtWidgets.QFrame): self.set_current_tab(identifier) return button + def get_tab_by_index(self, index): + if index < 0 or index > self._btns_layout.count(): + return None + item = self._btns_layout.itemAt(index) + return item.widget() + def set_current_tab(self, identifier): + if isinstance(identifier, int): + identifier = self.get_tab_by_index(identifier) + if isinstance(identifier, PublisherTabBtn): identifier = identifier.identifier From 430f30c05e3dc53184277ed121efd0fdcd003b3a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 22 Nov 2022 12:05:22 +0100 Subject: [PATCH 341/371] added helper methods to know on which tab we are --- openpype/tools/publisher/window.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index f107c0e505..3879e37ad7 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -539,6 +539,18 @@ class PublisherWindow(QtWidgets.QDialog): def _go_to_report_tab(self): self._set_current_tab("report") + def _is_on_create_tab(self): + self._is_current_tab("create") + + def _is_on_publish_tab(self): + self._is_current_tab("publish") + + def _is_on_details_tab(self): + self._is_current_tab("details") + + def _is_on_report_tab(self): + self._is_current_tab("report") + def _set_publish_overlay_visibility(self, visible): if visible: widget = self._publish_overlay From ac9b9b208e055c856c32313a87d20bf1dbf403c3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 22 Nov 2022 12:54:28 +0100 Subject: [PATCH 342/371] OP-4196 - safer getter for published_path published_path might be missing in case of thumbnail not getting published. This implementation takes from staging if published_path not present --- .../slack/plugins/publish/integrate_slack_api.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 643e55915b..f40a13db9f 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -142,13 +142,19 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): def _get_thumbnail_path(self, instance): """Returns abs url for thumbnail if present in instance repres""" - published_path = None + thumbnail_path = None for repre in instance.data.get("representations", []): if repre.get('thumbnail') or "thumbnail" in repre.get('tags', []): - if os.path.exists(repre["published_path"]): - published_path = repre["published_path"] + self.log.info(repre) + repre_thumbnail_path = ( + repre.get("published_path") or + os.path.join(repre["stagingDir"], repre["files"]) + ) + if os.path.exists(repre_thumbnail_path): + self.log.info("exists") + thumbnail_path = repre_thumbnail_path break - return published_path + return thumbnail_path def _get_review_path(self, instance): """Returns abs url for review if present in instance repres""" From c61098b782492728f7dbbe667b2540b2805b35ba Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 22 Nov 2022 13:00:34 +0100 Subject: [PATCH 343/371] OP-4196 - fix when task_data is not dict In legacy cases task might be only string with its name, not structure with additional metadata (type etc.). This implementation handles that. --- .../modules/slack/plugins/publish/integrate_slack_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index f40a13db9f..6138671180 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -121,10 +121,13 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): ): fill_pairs.append(("task", task_data["name"])) - else: + elif isinstance(task_data, dict): for key, value in task_data.items(): fill_key = "task[{}]".format(key) fill_pairs.append((fill_key, value)) + else: + # fallback for legacy - if task_data is only task name + fill_pairs.append(("task", task_data)) self.log.debug("fill_pairs ::{}".format(fill_pairs)) multiple_case_variants = prepare_template_data(fill_pairs) From f993842c4ec7a4e91b5a42cbd61ddba0f9387a35 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 22 Nov 2022 13:01:27 +0100 Subject: [PATCH 344/371] OP-4196 - remove unnecessary logging --- openpype/modules/slack/plugins/publish/integrate_slack_api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 6138671180..e43b07b228 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -148,13 +148,11 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): thumbnail_path = None for repre in instance.data.get("representations", []): if repre.get('thumbnail') or "thumbnail" in repre.get('tags', []): - self.log.info(repre) repre_thumbnail_path = ( repre.get("published_path") or os.path.join(repre["stagingDir"], repre["files"]) ) if os.path.exists(repre_thumbnail_path): - self.log.info("exists") thumbnail_path = repre_thumbnail_path break return thumbnail_path From ab17acddc7c192dab58727e87fe87b51e242a3df Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 22 Nov 2022 13:39:24 +0100 Subject: [PATCH 345/371] OP-4196 - better handling of data It should take task from instance anatomyData, then from context and handle non dict items. --- .../slack/plugins/publish/integrate_slack_api.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index e43b07b228..2c6f3d21bd 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -112,7 +112,13 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): if review_path: fill_pairs.append(("review_filepath", review_path)) - task_data = fill_data.get("task") + task_data = ( + copy.deepcopy(instance.data.get("anatomyData", [])).get("task") + or fill_data.get("task") + ) + if not isinstance(task_data, dict): + # fallback for legacy - if task_data is only task name + task_data["name"] = task_data if task_data: if ( "{task}" in message_templ @@ -121,13 +127,10 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): ): fill_pairs.append(("task", task_data["name"])) - elif isinstance(task_data, dict): + else: for key, value in task_data.items(): fill_key = "task[{}]".format(key) fill_pairs.append((fill_key, value)) - else: - # fallback for legacy - if task_data is only task name - fill_pairs.append(("task", task_data)) self.log.debug("fill_pairs ::{}".format(fill_pairs)) multiple_case_variants = prepare_template_data(fill_pairs) From 3cd241d2dbfa56a43ae2199fb1c38bd236497cd1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 22 Nov 2022 14:01:10 +0100 Subject: [PATCH 346/371] OP-4196 - fix wrong return type --- openpype/modules/slack/plugins/publish/integrate_slack_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 2c6f3d21bd..9539d03306 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -113,7 +113,7 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): fill_pairs.append(("review_filepath", review_path)) task_data = ( - copy.deepcopy(instance.data.get("anatomyData", [])).get("task") + copy.deepcopy(instance.data.get("anatomyData", {})).get("task") or fill_data.get("task") ) if not isinstance(task_data, dict): From 8a121bc0ff43e86bbe42d660a29e1d1fed13e08c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 22 Nov 2022 15:11:58 +0100 Subject: [PATCH 347/371] move default settings from 'project_settings/global/tools/publish/template_name_profiles' to legacy place --- .../defaults/project_settings/global.json | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index b8995de99e..46b8b1b0c8 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -288,6 +288,17 @@ "task_types": [], "tasks": [], "template_name": "maya2unreal" + }, + { + "families": [ + "online" + ], + "hosts": [ + "traypublisher" + ], + "task_types": [], + "tasks": [], + "template_name": "online" } ] }, @@ -484,19 +495,7 @@ ] }, "publish": { - "template_name_profiles": [ - { - "families": [ - "online" - ], - "hosts": [ - "traypublisher" - ], - "task_types": [], - "task_names": [], - "template_name": "online" - } - ], + "template_name_profiles": [], "hero_template_name_profiles": [] } }, From 788ed6478006c17644460945c9a60cc8207a036c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 22 Nov 2022 15:50:46 +0100 Subject: [PATCH 348/371] fix typo --- openpype/tools/publisher/widgets/card_view_widgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 9fd2bf0824..72644c09db 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -674,9 +674,9 @@ class InstanceCardView(AbstractInstanceView): instances_by_group[group_name] ) - self._update_ordered_group_nameS() + self._update_ordered_group_names() - def _update_ordered_group_nameS(self): + def _update_ordered_group_names(self): ordered_group_names = [CONTEXT_GROUP] for idx in range(self._content_layout.count()): if idx > 0: From 3ba5f8e0e99798c62ec295ea2a3706f3da8aac37 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 22 Nov 2022 15:51:05 +0100 Subject: [PATCH 349/371] fix tas combobox sizes --- 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 ce3d91ce63..332e231653 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -578,6 +578,11 @@ class TasksCombobox(QtWidgets.QComboBox): self._text = None + # Make sure combobox is extended horizontally + size_policy = self.sizePolicy() + size_policy.setHorizontalPolicy(size_policy.MinimumExpanding) + self.setSizePolicy(size_policy) + def set_invalid_empty_task(self, invalid=True): self._proxy_model.set_filter_empty(invalid) if invalid: From b2065acd7a43724ecd522a9f14531f3a45df38ce Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 22 Nov 2022 15:51:44 +0100 Subject: [PATCH 350/371] added ability to know if views have any items --- .../publisher/widgets/card_view_widgets.py | 7 +++++++ .../publisher/widgets/list_view_widgets.py | 7 +++++++ .../tools/publisher/widgets/overview_widget.py | 4 ++++ openpype/tools/publisher/widgets/widgets.py | 17 ++++++++++++++++- 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 72644c09db..09635d1a15 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -676,6 +676,13 @@ class InstanceCardView(AbstractInstanceView): self._update_ordered_group_names() + def has_items(self): + if self._convertor_items_group is not None: + return True + if self._widgets_by_group: + return True + return False + def _update_ordered_group_names(self): ordered_group_names = [CONTEXT_GROUP] for idx in range(self._content_layout.count()): diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index 32d84862f0..1cdb4cdcdb 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -912,6 +912,13 @@ class InstanceListView(AbstractInstanceView): if not self._instance_view.isExpanded(proxy_index): self._instance_view.expand(proxy_index) + def has_items(self): + if self._convertor_group_widget is not None: + return True + if self._group_items: + return True + return False + def get_selected_items(self): """Get selected instance ids and context selection. diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 1c924d1631..b1aeda9cd4 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -205,6 +205,10 @@ class OverviewWidget(QtWidgets.QFrame): self._subset_views_widget.height() ) + def has_items(self): + view = self._subset_views_layout.currentWidget() + return view.has_items() + def _on_create_clicked(self): """Pass signal to parent widget which should care about changing state. diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 332e231653..d6c6f8673c 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -305,6 +305,20 @@ class AbstractInstanceView(QtWidgets.QWidget): "{} Method 'refresh' is not implemented." ).format(self.__class__.__name__)) + def has_items(self): + """View has at least one item. + + This is more a question for controller but is called from widget + which should probably should not use controller. + + Returns: + bool: There is at least one instance or conversion item. + """ + + raise NotImplementedError(( + "{} Method 'has_items' is not implemented." + ).format(self.__class__.__name__)) + def get_selected_items(self): """Selected instances required for callbacks. @@ -1185,7 +1199,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): """Set currently selected instances. Args: - instances(list): List of selected instances. + instances(List[CreatedInstance]): List of selected instances. Empty instances tells that nothing or context is selected. """ self._set_btns_visible(False) @@ -1619,6 +1633,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget): instances(List[CreatedInstance]): List of currently selected instances. context_selected(bool): Is context selected. + convertor_identifiers(List[str]): Identifiers of convert items. """ all_valid = True From d87e8fe99c68c23a5fdf1ce19fc0debe654eee97 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 22 Nov 2022 15:52:06 +0100 Subject: [PATCH 351/371] tabs widget can accept tab indexes --- openpype/tools/publisher/widgets/tabs_widget.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openpype/tools/publisher/widgets/tabs_widget.py b/openpype/tools/publisher/widgets/tabs_widget.py index eb3eda8c19..d8ad19cfc0 100644 --- a/openpype/tools/publisher/widgets/tabs_widget.py +++ b/openpype/tools/publisher/widgets/tabs_widget.py @@ -54,6 +54,9 @@ class PublisherTabsWidget(QtWidgets.QFrame): self._buttons_by_identifier = {} def is_current_tab(self, identifier): + if isinstance(identifier, int): + identifier = self.get_tab_by_index(identifier) + if isinstance(identifier, PublisherTabBtn): identifier = identifier.identifier return self._current_identifier == identifier @@ -69,10 +72,10 @@ class PublisherTabsWidget(QtWidgets.QFrame): return button def get_tab_by_index(self, index): - if index < 0 or index > self._btns_layout.count(): - return None - item = self._btns_layout.itemAt(index) - return item.widget() + if 0 >= index < self._btns_layout.count(): + item = self._btns_layout.itemAt(index) + return item.widget() + return None def set_current_tab(self, identifier): if isinstance(identifier, int): From dd50c6723e1ec892478205f72de2e0bf57940d35 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 22 Nov 2022 15:55:39 +0100 Subject: [PATCH 352/371] small teaks and fixes --- openpype/tools/publisher/window.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 3879e37ad7..59dd2e6ec9 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -432,7 +432,7 @@ class PublisherWindow(QtWidgets.QDialog): self._update_create_overlay_size() self._update_create_overlay_visibility() - if self._is_current_tab("create"): + if self._is_on_create_tab(): self._install_app_event_listener() # Reset if requested @@ -450,7 +450,7 @@ class PublisherWindow(QtWidgets.QDialog): self._context_label.setText(label) def _update_publish_details_widget(self, force=False): - if not force and not self._is_current_tab("details"): + if not force and not self._is_on_details_tab(): return report_data = self.controller.get_publish_report() @@ -540,16 +540,16 @@ class PublisherWindow(QtWidgets.QDialog): self._set_current_tab("report") def _is_on_create_tab(self): - self._is_current_tab("create") + return self._is_current_tab("create") def _is_on_publish_tab(self): - self._is_current_tab("publish") + return self._is_current_tab("publish") def _is_on_details_tab(self): - self._is_current_tab("details") + return self._is_current_tab("details") def _is_on_report_tab(self): - self._is_current_tab("report") + return self._is_current_tab("report") def _set_publish_overlay_visibility(self, visible): if visible: @@ -601,11 +601,8 @@ class PublisherWindow(QtWidgets.QDialog): self._set_publish_visibility(False) self._set_footer_enabled(False) self._update_publish_details_widget() - if ( - not self._is_current_tab("create") - and not self._is_current_tab("publish") ): - self._set_current_tab("publish") + self._go_to_publish_tab() def _on_publish_start(self): self._create_tab.setEnabled(False) @@ -621,8 +618,8 @@ class PublisherWindow(QtWidgets.QDialog): self._publish_details_widget.close_details_popup() - if self._is_current_tab(self._create_tab): - self._set_current_tab("publish") + if self._is_on_create_tab(): + self._go_to_publish_tab() def _on_publish_validated_change(self, event): if event["value"]: @@ -635,7 +632,7 @@ class PublisherWindow(QtWidgets.QDialog): publish_has_crashed = self._controller.publish_has_crashed validate_enabled = not publish_has_crashed publish_enabled = not publish_has_crashed - if self._is_current_tab("publish"): + if self._is_on_publish_tab(): self._go_to_report_tab() if validate_enabled: From caf94fb68f789d528e69cf6b423b29b20fe16369 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 22 Nov 2022 15:57:17 +0100 Subject: [PATCH 353/371] show publisher can accept tab to switch to --- openpype/tools/publisher/window.py | 62 +++++++++++++++++++++++++++++- openpype/tools/utils/host_tools.py | 16 ++++---- 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 59dd2e6ec9..0f7fd2c7e3 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -156,7 +156,7 @@ class PublisherWindow(QtWidgets.QDialog): footer_layout.addWidget(footer_bottom_widget, 0) # Content - # - wrap stacked widget under one more widget to be able propagate + # - wrap stacked widget under one more widget to be able to propagate # margins (QStackedLayout can't have margins) content_widget = QtWidgets.QWidget(under_publish_widget) @@ -267,6 +267,9 @@ class PublisherWindow(QtWidgets.QDialog): controller.event_system.add_callback( "publish.reset.finished", self._on_publish_reset ) + controller.event_system.add_callback( + "controller.reset.finished", self._on_controller_reset + ) controller.event_system.add_callback( "publish.process.started", self._on_publish_start ) @@ -337,11 +340,13 @@ class PublisherWindow(QtWidgets.QDialog): self._controller = controller self._first_show = True + self._first_reset = True # This is a little bit confusing but 'reset_on_first_show' is too long - # forin init + # for init self._reset_on_first_show = reset_on_show self._reset_on_show = True self._publish_frame_visible = None + self._tab_on_reset = None self._error_messages_to_show = collections.deque() self._errors_dialog_message_timer = errors_dialog_message_timer @@ -353,12 +358,21 @@ class PublisherWindow(QtWidgets.QDialog): self._show_timer = show_timer self._show_counter = 0 + self._window_is_visible = False @property def controller(self): return self._controller + def make_sure_is_visible(self): + if self._window_is_visible: + self.setWindowState(QtCore.Qt.ActiveWindow) + + else: + self.show() + def showEvent(self, event): + self._window_is_visible = True super(PublisherWindow, self).showEvent(event) if self._first_show: self._first_show = False @@ -372,6 +386,7 @@ class PublisherWindow(QtWidgets.QDialog): self._update_create_overlay_size() def closeEvent(self, event): + self._window_is_visible = False self._uninstall_app_event_listener() self.save_changes() self._reset_on_show = True @@ -449,6 +464,19 @@ class PublisherWindow(QtWidgets.QDialog): def set_context_label(self, label): self._context_label.setText(label) + def set_tab_on_reset(self, tab): + """Define tab that will be selected on window show. + + This is single use method, when publisher window is showed the value is + unset and not used on next show. + + Args: + tab (Union[int, Literal[create, publish, details, report]]: Index + or name of tab which will be selected on show (after reset). + """ + + self._tab_on_reset = tab + def _update_publish_details_widget(self, force=False): if not force and not self._is_on_details_tab(): return @@ -524,6 +552,11 @@ class PublisherWindow(QtWidgets.QDialog): def _set_current_tab(self, identifier): self._tabs_widget.set_current_tab(identifier) + def set_current_tab(self, tab): + self._set_current_tab(tab) + if not self._window_is_visible: + self.set_tab_on_reset(tab) + def _is_current_tab(self, identifier): return self._tabs_widget.is_current_tab(identifier) @@ -601,7 +634,32 @@ class PublisherWindow(QtWidgets.QDialog): self._set_publish_visibility(False) self._set_footer_enabled(False) self._update_publish_details_widget() + + def _on_controller_reset(self): + self._first_reset, first_reset = False, self._first_reset + if self._tab_on_reset is not None: + self._tab_on_reset, new_tab = None, self._tab_on_reset + self._set_current_tab(new_tab) + return + + # On first reset change tab based on available items + # - if there is at least one instance the tab is changed to 'publish' + # otherwise 'create' is used + # - this happens only on first show + if first_reset: + if self._overview_widget.has_items(): + self._go_to_publish_tab() + else: + self._go_to_create_tab() + + elif ( + not self._is_on_create_tab() + and not self._is_on_publish_tab() ): + # If current tab is not 'Create' or 'Publish' go to 'Publish' + # - this can happen when publishing started and was reset + # at that moment it doesn't make sense to stay at publish + # specific tabs. self._go_to_publish_tab() def _on_publish_start(self): diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 046dcbdf6a..e8593a8ae2 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -285,14 +285,12 @@ class HostToolsHelper: return self._publisher_tool - def show_publisher_tool(self, parent=None, controller=None): + def show_publisher_tool(self, parent=None, controller=None, tab=None): with qt_app_context(): - dialog = self.get_publisher_tool(parent, controller) - - dialog.show() - dialog.raise_() - dialog.activateWindow() - dialog.showNormal() + window = self.get_publisher_tool(parent, controller) + if tab: + window.set_current_tab(tab) + window.make_sure_is_visible() def get_tool_by_name(self, tool_name, parent=None, *args, **kwargs): """Show tool by it's name. @@ -446,8 +444,8 @@ def show_publish(parent=None): _SingletonPoint.show_tool_by_name("publish", parent) -def show_publisher(parent=None): - _SingletonPoint.show_tool_by_name("publisher", parent) +def show_publisher(parent=None, **kwargs): + _SingletonPoint.show_tool_by_name("publisher", parent, **kwargs) def show_experimental_tools_dialog(parent=None): From 6af4412591b45f2001a9f01e998a36e871666ec9 Mon Sep 17 00:00:00 2001 From: clement hector Date: Tue, 22 Nov 2022 16:08:03 +0100 Subject: [PATCH 354/371] set creator window as parent of pop up window --- .../hosts/photoshop/plugins/create/create_legacy_image.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py index 2792a775e0..7672458165 100644 --- a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py @@ -29,7 +29,8 @@ class CreateImage(create.LegacyCreator): if len(selection) > 1: # Ask user whether to create one image or image per selected # item. - msg_box = QtWidgets.QMessageBox() + active_window = QtWidgets.QApplication.activeWindow() + msg_box = QtWidgets.QMessageBox(parent=active_window) msg_box.setIcon(QtWidgets.QMessageBox.Warning) msg_box.setText( "Multiple layers selected." @@ -102,7 +103,7 @@ class CreateImage(create.LegacyCreator): if group.long_name: for directory in group.long_name[::-1]: name = directory.replace(stub.PUBLISH_ICON, '').\ - replace(stub.LOADED_ICON, '') + replace(stub.LOADED_ICON, '') long_names.append(name) self.data.update({"subset": subset_name}) From 861cdadc9bbcd171da0d8793de6595db8446efce Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 22 Nov 2022 16:48:02 +0100 Subject: [PATCH 355/371] fix formatting --- 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 d6c6f8673c..6bc09c55a3 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -316,7 +316,7 @@ class AbstractInstanceView(QtWidgets.QWidget): """ raise NotImplementedError(( - "{} Method 'has_items' is not implemented." + "{} Method 'has_items' is not implemented." ).format(self.__class__.__name__)) def get_selected_items(self): From 3b81c7f5731dfc5c018bff11e9758fc3e5e26450 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 22 Nov 2022 18:15:27 +0100 Subject: [PATCH 356/371] OP-4196 - better logging of file upload errors --- .../slack/plugins/publish/integrate_slack_api.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 9539d03306..0cd5ec9de8 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -188,10 +188,17 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): channel=channel, title=os.path.basename(p_file) ) - attachment_str += "\n<{}|{}>".format( - response["file"]["permalink"], - os.path.basename(p_file)) - file_ids.append(response["file"]["id"]) + if response.get("error"): + error_str = self._enrich_error( + str(response.get("error")), + channel) + self.log.warning( + "Error happened: {}".format(error_str)) + else: + attachment_str += "\n<{}|{}>".format( + response["file"]["permalink"], + os.path.basename(p_file)) + file_ids.append(response["file"]["id"]) if publish_files: message += attachment_str From 855e7d1c61c16093706b276435aed02fbb108e91 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 22 Nov 2022 18:28:01 +0100 Subject: [PATCH 357/371] OP-4196 - fix filtering profiles Task types didn't work. --- .../modules/slack/plugins/publish/collect_slack_family.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/slack/plugins/publish/collect_slack_family.py b/openpype/modules/slack/plugins/publish/collect_slack_family.py index 39b05937dc..27e899d59a 100644 --- a/openpype/modules/slack/plugins/publish/collect_slack_family.py +++ b/openpype/modules/slack/plugins/publish/collect_slack_family.py @@ -18,15 +18,15 @@ class CollectSlackFamilies(pyblish.api.InstancePlugin): profiles = None def process(self, instance): - task_name = legacy_io.Session.get("AVALON_TASK") + task_data = instance.data["anatomyData"].get("task", {}) family = self.main_family_from_instance(instance) key_values = { "families": family, - "tasks": task_name, + "tasks": task_data.get("name"), + "task_types": task_data.get("type"), "hosts": instance.data["anatomyData"]["app"], "subsets": instance.data["subset"] } - profile = filter_profiles(self.profiles, key_values, logger=self.log) From c3e5b7a169c670b35889a5fb0038ba4b50bf7841 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Nov 2022 21:21:42 +0100 Subject: [PATCH 358/371] update history.md --- HISTORY.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index f6cc74e114..7365696f96 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,40 @@ # Changelog +## [3.14.6](https://github.com/pypeclub/OpenPype/tree/3.14.6) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.5...3.14.6) + +### 📖 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) From e600cd1b3d2963a2a2e26dce79e07818bb4c5d28 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Nov 2022 21:24:11 +0100 Subject: [PATCH 359/371] updating to 3.14.7 --- CHANGELOG.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 707b61676f..c3cccf2d1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,63 @@ # Changelog -## [3.14.6](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.14.7](https://github.com/pypeclub/OpenPype/tree/3.14.7) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.5...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.6...3.14.7) + +**🆕 New features** + +- Hiero: loading effect family to timeline [\#4055](https://github.com/pypeclub/OpenPype/pull/4055) + +**🚀 Enhancements** + +- Ftrack: Event server status give more information about version locations [\#4112](https://github.com/pypeclub/OpenPype/pull/4112) +- General: Allow higher numbers in frames and clips [\#4101](https://github.com/pypeclub/OpenPype/pull/4101) +- Publisher: Settings for validate frame range [\#4097](https://github.com/pypeclub/OpenPype/pull/4097) +- Publisher: Ignore escape button [\#4090](https://github.com/pypeclub/OpenPype/pull/4090) +- Flame: Loading clip with native colorspace resolved from mapping [\#4079](https://github.com/pypeclub/OpenPype/pull/4079) +- General: Extract review single frame output [\#4064](https://github.com/pypeclub/OpenPype/pull/4064) +- Publisher: Prepared common function for instance data cache [\#4063](https://github.com/pypeclub/OpenPype/pull/4063) +- Publisher: Easy access to publish page from create page [\#4058](https://github.com/pypeclub/OpenPype/pull/4058) +- General/TVPaint: Attribute defs dialog [\#4052](https://github.com/pypeclub/OpenPype/pull/4052) +- Publisher: Better reset defer [\#4048](https://github.com/pypeclub/OpenPype/pull/4048) +- Publisher: Add thumbnail sources [\#4042](https://github.com/pypeclub/OpenPype/pull/4042) + +**🐛 Bug fixes** + +- General: Move default settings for template name [\#4119](https://github.com/pypeclub/OpenPype/pull/4119) +- Nuke: loaded nodes set to first tab [\#4114](https://github.com/pypeclub/OpenPype/pull/4114) +- Nuke: load image first frame [\#4113](https://github.com/pypeclub/OpenPype/pull/4113) +- Files Widget: Ignore case sensitivity of extensions [\#4096](https://github.com/pypeclub/OpenPype/pull/4096) +- Webpublisher: extension is lowercased in Setting and in uploaded files [\#4095](https://github.com/pypeclub/OpenPype/pull/4095) +- Publish Report Viewer: Fix small bugs [\#4086](https://github.com/pypeclub/OpenPype/pull/4086) +- Igniter: fix regex to match semver better [\#4085](https://github.com/pypeclub/OpenPype/pull/4085) +- Maya: aov filtering [\#4083](https://github.com/pypeclub/OpenPype/pull/4083) +- Flame/Flare: Loading to multiple batches [\#4080](https://github.com/pypeclub/OpenPype/pull/4080) +- hiero: creator from settings with set maximum [\#4077](https://github.com/pypeclub/OpenPype/pull/4077) +- Nuke: resolve hashes in file name only for frame token [\#4074](https://github.com/pypeclub/OpenPype/pull/4074) +- Publisher: Fix cache of asset docs [\#4070](https://github.com/pypeclub/OpenPype/pull/4070) +- Webpublisher: cleanup wp extract thumbnail [\#4067](https://github.com/pypeclub/OpenPype/pull/4067) +- Settings UI: Locked setting can't bypass lock [\#4066](https://github.com/pypeclub/OpenPype/pull/4066) +- Loader: Fix comparison of repre name [\#4053](https://github.com/pypeclub/OpenPype/pull/4053) +- Deadline: Extract environment subprocess failure [\#4050](https://github.com/pypeclub/OpenPype/pull/4050) + +**🔀 Refactored code** + +- General: Collect entities plugin minor changes [\#4089](https://github.com/pypeclub/OpenPype/pull/4089) +- General: Direct interfaces import [\#4065](https://github.com/pypeclub/OpenPype/pull/4065) + +**Merged pull requests:** + +- Bump loader-utils from 1.4.1 to 1.4.2 in /website [\#4100](https://github.com/pypeclub/OpenPype/pull/4100) +- Online family for Tray Publisher [\#4093](https://github.com/pypeclub/OpenPype/pull/4093) +- Bump loader-utils from 1.4.0 to 1.4.1 in /website [\#4081](https://github.com/pypeclub/OpenPype/pull/4081) +- remove underscore from subset name [\#4059](https://github.com/pypeclub/OpenPype/pull/4059) +- Alembic Loader as Arnold Standin [\#4047](https://github.com/pypeclub/OpenPype/pull/4047) + + +## [3.14.6](https://github.com/pypeclub/OpenPype/tree/3.14.6) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.5...3.14.6) ### 📖 Documentation From c63f468484b32628c6d87a35df993bf2303ecb83 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 23 Nov 2022 03:35:08 +0000 Subject: [PATCH 360/371] [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 0116b49f4d..a4af8b7a99 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.6" +__version__ = "3.14.7-nightly.7" From 0d88af8aec4c6112be2629865da7ffce4a7cce4d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Nov 2022 11:40:16 +0100 Subject: [PATCH 361/371] update latest 3.14.7 --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3cccf2d1e..0c5f2cf8b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ **🚀 Enhancements** +- Photoshop: bug with pop-up window on Instance Creator [\#4121](https://github.com/pypeclub/OpenPype/pull/4121) +- Publisher: Open on specific tab [\#4120](https://github.com/pypeclub/OpenPype/pull/4120) +- Publisher: Hide unknown publish values [\#4116](https://github.com/pypeclub/OpenPype/pull/4116) - Ftrack: Event server status give more information about version locations [\#4112](https://github.com/pypeclub/OpenPype/pull/4112) - General: Allow higher numbers in frames and clips [\#4101](https://github.com/pypeclub/OpenPype/pull/4101) - Publisher: Settings for validate frame range [\#4097](https://github.com/pypeclub/OpenPype/pull/4097) @@ -25,6 +28,7 @@ **🐛 Bug fixes** - General: Move default settings for template name [\#4119](https://github.com/pypeclub/OpenPype/pull/4119) +- Slack: notification fail in new tray publisher [\#4118](https://github.com/pypeclub/OpenPype/pull/4118) - Nuke: loaded nodes set to first tab [\#4114](https://github.com/pypeclub/OpenPype/pull/4114) - Nuke: load image first frame [\#4113](https://github.com/pypeclub/OpenPype/pull/4113) - Files Widget: Ignore case sensitivity of extensions [\#4096](https://github.com/pypeclub/OpenPype/pull/4096) @@ -54,7 +58,6 @@ - remove underscore from subset name [\#4059](https://github.com/pypeclub/OpenPype/pull/4059) - Alembic Loader as Arnold Standin [\#4047](https://github.com/pypeclub/OpenPype/pull/4047) - ## [3.14.6](https://github.com/pypeclub/OpenPype/tree/3.14.6) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.5...3.14.6) From 6725c1f6d8dc025f13bffbbb1c92a242c49b618f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Nov 2022 11:40:57 +0100 Subject: [PATCH 362/371] udpate history --- HISTORY.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 7365696f96..04a1073c07 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,64 @@ # Changelog + +## [3.14.7](https://github.com/pypeclub/OpenPype/tree/3.14.7) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.6...3.14.7) + +**🆕 New features** + +- Hiero: loading effect family to timeline [\#4055](https://github.com/pypeclub/OpenPype/pull/4055) + +**🚀 Enhancements** + +- Photoshop: bug with pop-up window on Instance Creator [\#4121](https://github.com/pypeclub/OpenPype/pull/4121) +- Publisher: Open on specific tab [\#4120](https://github.com/pypeclub/OpenPype/pull/4120) +- Publisher: Hide unknown publish values [\#4116](https://github.com/pypeclub/OpenPype/pull/4116) +- Ftrack: Event server status give more information about version locations [\#4112](https://github.com/pypeclub/OpenPype/pull/4112) +- General: Allow higher numbers in frames and clips [\#4101](https://github.com/pypeclub/OpenPype/pull/4101) +- Publisher: Settings for validate frame range [\#4097](https://github.com/pypeclub/OpenPype/pull/4097) +- Publisher: Ignore escape button [\#4090](https://github.com/pypeclub/OpenPype/pull/4090) +- Flame: Loading clip with native colorspace resolved from mapping [\#4079](https://github.com/pypeclub/OpenPype/pull/4079) +- General: Extract review single frame output [\#4064](https://github.com/pypeclub/OpenPype/pull/4064) +- Publisher: Prepared common function for instance data cache [\#4063](https://github.com/pypeclub/OpenPype/pull/4063) +- Publisher: Easy access to publish page from create page [\#4058](https://github.com/pypeclub/OpenPype/pull/4058) +- General/TVPaint: Attribute defs dialog [\#4052](https://github.com/pypeclub/OpenPype/pull/4052) +- Publisher: Better reset defer [\#4048](https://github.com/pypeclub/OpenPype/pull/4048) +- Publisher: Add thumbnail sources [\#4042](https://github.com/pypeclub/OpenPype/pull/4042) + +**🐛 Bug fixes** + +- General: Move default settings for template name [\#4119](https://github.com/pypeclub/OpenPype/pull/4119) +- Slack: notification fail in new tray publisher [\#4118](https://github.com/pypeclub/OpenPype/pull/4118) +- Nuke: loaded nodes set to first tab [\#4114](https://github.com/pypeclub/OpenPype/pull/4114) +- Nuke: load image first frame [\#4113](https://github.com/pypeclub/OpenPype/pull/4113) +- Files Widget: Ignore case sensitivity of extensions [\#4096](https://github.com/pypeclub/OpenPype/pull/4096) +- Webpublisher: extension is lowercased in Setting and in uploaded files [\#4095](https://github.com/pypeclub/OpenPype/pull/4095) +- Publish Report Viewer: Fix small bugs [\#4086](https://github.com/pypeclub/OpenPype/pull/4086) +- Igniter: fix regex to match semver better [\#4085](https://github.com/pypeclub/OpenPype/pull/4085) +- Maya: aov filtering [\#4083](https://github.com/pypeclub/OpenPype/pull/4083) +- Flame/Flare: Loading to multiple batches [\#4080](https://github.com/pypeclub/OpenPype/pull/4080) +- hiero: creator from settings with set maximum [\#4077](https://github.com/pypeclub/OpenPype/pull/4077) +- Nuke: resolve hashes in file name only for frame token [\#4074](https://github.com/pypeclub/OpenPype/pull/4074) +- Publisher: Fix cache of asset docs [\#4070](https://github.com/pypeclub/OpenPype/pull/4070) +- Webpublisher: cleanup wp extract thumbnail [\#4067](https://github.com/pypeclub/OpenPype/pull/4067) +- Settings UI: Locked setting can't bypass lock [\#4066](https://github.com/pypeclub/OpenPype/pull/4066) +- Loader: Fix comparison of repre name [\#4053](https://github.com/pypeclub/OpenPype/pull/4053) +- Deadline: Extract environment subprocess failure [\#4050](https://github.com/pypeclub/OpenPype/pull/4050) + +**🔀 Refactored code** + +- General: Collect entities plugin minor changes [\#4089](https://github.com/pypeclub/OpenPype/pull/4089) +- General: Direct interfaces import [\#4065](https://github.com/pypeclub/OpenPype/pull/4065) + +**Merged pull requests:** + +- Bump loader-utils from 1.4.1 to 1.4.2 in /website [\#4100](https://github.com/pypeclub/OpenPype/pull/4100) +- Online family for Tray Publisher [\#4093](https://github.com/pypeclub/OpenPype/pull/4093) +- Bump loader-utils from 1.4.0 to 1.4.1 in /website [\#4081](https://github.com/pypeclub/OpenPype/pull/4081) +- remove underscore from subset name [\#4059](https://github.com/pypeclub/OpenPype/pull/4059) +- Alembic Loader as Arnold Standin [\#4047](https://github.com/pypeclub/OpenPype/pull/4047) + ## [3.14.6](https://github.com/pypeclub/OpenPype/tree/3.14.6) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.5...3.14.6) From 2594bc2a0efa19331b7dbccb2624be41acf1032a Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 23 Nov 2022 10:45:17 +0000 Subject: [PATCH 363/371] [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 a4af8b7a99..a00c7de704 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.7" +__version__ = "3.14.7-nightly.8" From 8b1b09b33825dc9ff320b6c6e49597dedcf58f7f Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 23 Nov 2022 10:58:00 +0000 Subject: [PATCH 364/371] [Automated] Release --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index a00c7de704..ffabcf8025 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.8" +__version__ = "3.14.7" From 5779687a2b4467195a20bd9242d2fa782f7b27cd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 23 Nov 2022 11:59:53 +0100 Subject: [PATCH 365/371] Removed unused argument --- .../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 78e1371eee..40193bac71 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -192,7 +192,7 @@ def get_openpype_executable(): return exe_list, dir_list -def get_openpype_versions(exe_list, dir_list): +def get_openpype_versions(dir_list): print(">>> Getting OpenPype executable ...") openpype_versions = [] From c3b7e3269544d3471c02e193acd4054b5a08eb08 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Nov 2022 16:30:49 +0100 Subject: [PATCH 366/371] skip turning on/off of autosync --- .../publish/integrate_hierarchy_ftrack.py | 43 +------------------ 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index fa7a89050c..6bae922d94 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -8,9 +8,6 @@ import pyblish.api from openpype.client import get_asset_by_id from openpype.lib import filter_profiles - -# Copy of constant `openpype_modules.ftrack.lib.avalon_sync.CUST_ATTR_AUTO_SYNC` -CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" CUST_ATTR_GROUP = "openpype" @@ -97,18 +94,9 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.task_types = self.get_all_task_types(project) self.task_statuses = self.get_task_statuses(project) - # disable termporarily ftrack project's autosyncing - if auto_sync_state: - self.auto_sync_off(project) + # import ftrack hierarchy + self.import_to_ftrack(project_name, hierarchy_context) - try: - # import ftrack hierarchy - self.import_to_ftrack(project_name, hierarchy_context) - except Exception: - raise - finally: - if auto_sync_state: - self.auto_sync_on(project) def import_to_ftrack(self, project_name, input_data, parent=None): # Prequery hiearchical custom attributes @@ -381,33 +369,6 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): return entity - def auto_sync_off(self, project): - project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = False - - self.log.info("Ftrack autosync swithed off") - - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - def auto_sync_on(self, project): - - project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = True - - self.log.info("Ftrack autosync swithed on") - - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - def _get_active_assets(self, context): """ Returns only asset dictionary. Usually the last part of deep dictionary which From 635c662a8c357c5170aadfb9197081a16c27c3b2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Nov 2022 16:32:04 +0100 Subject: [PATCH 367/371] raise known publish error if project in ftrack was not found --- .../publish/integrate_hierarchy_ftrack.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 6bae922d94..8b0e4ab62d 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -7,6 +7,7 @@ import pyblish.api from openpype.client import get_asset_by_id from openpype.lib import filter_profiles +from openpype.pipeline import KnownPublishError CUST_ATTR_GROUP = "openpype" @@ -16,7 +17,6 @@ CUST_ATTR_GROUP = "openpype" def get_pype_attr(session, split_hierarchical=True): custom_attributes = [] hier_custom_attributes = [] - # TODO remove deprecated "avalon" group from query cust_attrs_query = ( "select id, entity_type, object_type_id, is_hierarchical, default" " from CustomAttributeConfiguration" @@ -76,19 +76,25 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): create_task_status_profiles = [] def process(self, context): - self.context = context - if "hierarchyContext" not in self.context.data: + if "hierarchyContext" not in context.data: return hierarchy_context = self._get_active_assets(context) self.log.debug("__ hierarchy_context: {}".format(hierarchy_context)) - session = self.context.data["ftrackSession"] - project_name = self.context.data["projectEntity"]["name"] - query = 'Project where full_name is "{}"'.format(project_name) - project = session.query(query).one() - auto_sync_state = project["custom_attributes"][CUST_ATTR_AUTO_SYNC] + session = context.data["ftrackSession"] + project_name = context.data["projectName"] + project = session.query( + 'select id, full_name from Project where full_name is "{}"'.format( + project_name + ) + ).first() + if not project: + raise KnownPublishError( + "Project \"{}\" was not found on ftrack.".format(project_name) + ) + self.context = context self.session = session self.ft_project = project self.task_types = self.get_all_task_types(project) From 5a0cc527325642c9871323a6aba8c263be72d194 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Nov 2022 16:34:02 +0100 Subject: [PATCH 368/371] implemented helper methods to query information we need from ftrack --- .../publish/integrate_hierarchy_ftrack.py | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 8b0e4ab62d..02946f813f 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -103,6 +103,129 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): # import ftrack hierarchy self.import_to_ftrack(project_name, hierarchy_context) + def query_ftrack_entitites(self, session, ft_project): + project_id = ft_project["id"] + entities = session.query(( + "select id, name, parent_id" + " from TypedContext where project_id is \"{}\"" + ).format(project_id)).all() + + entities_by_id = {} + entities_by_parent_id = collections.defaultdict(list) + for entity in entities: + entities_by_id[entity["id"]] = entity + parent_id = entity["parent_id"] + entities_by_parent_id[parent_id].append(entity) + + ftrack_hierarchy = [] + ftrack_id_queue = collections.deque() + ftrack_id_queue.append((project_id, ftrack_hierarchy)) + while ftrack_id_queue: + item = ftrack_id_queue.popleft() + ftrack_id, parent_list = item + if ftrack_id == project_id: + entity = ft_project + name = entity["full_name"] + else: + entity = entities_by_id[ftrack_id] + name = entity["name"] + + children = [] + parent_list.append({ + "name": name, + "low_name": name.lower(), + "entity": entity, + "children": children, + }) + for child in entities_by_parent_id[ftrack_id]: + ftrack_id_queue.append((child["id"], children)) + return ftrack_hierarchy + + def find_matching_ftrack_entities( + self, hierarchy_context, ftrack_hierarchy + ): + walk_queue = collections.deque() + for entity_name, entity_data in hierarchy_context.items(): + walk_queue.append( + (entity_name, entity_data, ftrack_hierarchy) + ) + + matching_ftrack_entities = [] + while walk_queue: + item = walk_queue.popleft() + entity_name, entity_data, ft_children = item + matching_ft_child = None + for ft_child in ft_children: + if ft_child["low_name"] == entity_name.lower(): + matching_ft_child = ft_child + break + + if matching_ft_child is None: + continue + + entity = matching_ft_child["entity"] + entity_data["ft_entity"] = entity + matching_ftrack_entities.append(entity) + + hierarchy_children = entity_data.get("childs") + if not hierarchy_children: + continue + + for child_name, child_data in hierarchy_children.items(): + walk_queue.append( + (child_name, child_data, matching_ft_child["children"]) + ) + return matching_ftrack_entities + + def query_custom_attribute_values(self, session, entities, hier_attrs): + attr_ids = { + attr["id"] + for attr in hier_attrs + } + entity_ids = { + entity["id"] + for entity in entities + } + output = { + entity_id: {} + for entity_id in entity_ids + } + if not attr_ids or not entity_ids: + return {} + + joined_attr_ids = ",".join( + ['"{}"'.format(attr_id) for attr_id in attr_ids] + ) + + # Query values in chunks + chunk_size = int(5000 / len(attr_ids)) + # Make sure entity_ids is `list` for chunk selection + entity_ids = list(entity_ids) + results = [] + for idx in range(0, len(entity_ids), chunk_size): + joined_entity_ids = ",".join([ + '"{}"'.format(entity_id) + for entity_id in entity_ids[idx:idx + chunk_size] + ]) + results.extend( + session.query( + ( + "select value, entity_id, configuration_id" + " from CustomAttributeValue" + " where entity_id in ({}) and configuration_id in ({})" + ).format( + joined_entity_ids, + joined_attr_ids + ) + ).all() + ) + + for result in results: + attr_id = result["configuration_id"] + entity_id = result["entity_id"] + output[entity_id][attr_id] = result["value"] + + return output def import_to_ftrack(self, project_name, input_data, parent=None): # Prequery hiearchical custom attributes From a78ef54e56e7a0a0300fdc140ec40fb1be4111e9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Nov 2022 16:35:02 +0100 Subject: [PATCH 369/371] query user at the start of import method instead of requerying it again --- .../publish/integrate_hierarchy_ftrack.py | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 02946f813f..5d30b9bf7b 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -234,6 +234,16 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): attr["key"]: attr for attr in hier_custom_attributes } + # Query user entity (for comments) + user = self.session.query( + "User where username is \"{}\"".format(self.session.api_user) + ).first() + if not user: + self.log.warning( + "Was not able to query current User {}".format( + self.session.api_user + ) + ) # Get ftrack api module (as they are different per python version) ftrack_api = self.context.data["ftrackPythonModule"] @@ -364,25 +374,18 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): six.reraise(tp, value, tb) # Create notes. - user = self.session.query( - "User where username is \"{}\"".format(self.session.api_user) - ).first() - if user: - for comment in entity_data.get("comments", []): + entity_comments = entity_data.get("comments") + if user and entity_comments: + for comment in entity_comments: entity.create_note(comment, user) - else: - self.log.warning( - "Was not able to query current User {}".format( - self.session.api_user - ) - ) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) + + try: + self.session.commit() + except Exception: + tp, value, tb = sys.exc_info() + self.session.rollback() + self.session._configure_locations() + six.reraise(tp, value, tb) # Import children. if 'childs' in entity_data: From 36afd8aa7c3a9a88001c20f6c0ae8c616a2bf51a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Nov 2022 16:36:21 +0100 Subject: [PATCH 370/371] import to ftrack is not recursion based but queue based method --- .../publish/integrate_hierarchy_ftrack.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 5d30b9bf7b..12e89a1884 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -227,7 +227,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): return output - def import_to_ftrack(self, project_name, input_data, parent=None): + def import_to_ftrack(self, project_name, hierarchy_context): # Prequery hiearchical custom attributes hier_custom_attributes = get_pype_attr(self.session)[1] hier_attr_by_key = { @@ -247,8 +247,17 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): # Get ftrack api module (as they are different per python version) ftrack_api = self.context.data["ftrackPythonModule"] - for entity_name in input_data: - entity_data = input_data[entity_name] + # Use queue of hierarchy items to process + import_queue = collections.deque() + for entity_name, entity_data in hierarchy_context.items(): + import_queue.append( + (entity_name, entity_data, None) + ) + + while import_queue: + item = import_queue.popleft() + entity_name, entity_data, parent = item + entity_type = entity_data['entity_type'] self.log.debug(entity_data) self.log.debug(entity_type) @@ -388,9 +397,14 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): six.reraise(tp, value, tb) # Import children. - if 'childs' in entity_data: - self.import_to_ftrack( - project_name, entity_data['childs'], entity) + children = entity_data.get("childs") + if not children: + continue + + for entity_name, entity_data in children.items(): + import_queue.append( + (entity_name, entity_data, entity) + ) def create_links(self, project_name, entity_data, entity): # Clear existing links. From 5de422dea2c294bcc1ff097c272180b272e89e8a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 23 Nov 2022 16:38:04 +0100 Subject: [PATCH 371/371] change how custom attributes are filled on entities and how entities are created --- .../publish/integrate_hierarchy_ftrack.py | 156 +++++++++--------- 1 file changed, 82 insertions(+), 74 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 12e89a1884..046dfd9ad8 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -229,10 +229,10 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): def import_to_ftrack(self, project_name, hierarchy_context): # Prequery hiearchical custom attributes - hier_custom_attributes = get_pype_attr(self.session)[1] + hier_attrs = get_pype_attr(self.session)[1] hier_attr_by_key = { attr["key"]: attr - for attr in hier_custom_attributes + for attr in hier_attrs } # Query user entity (for comments) user = self.session.query( @@ -244,6 +244,19 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.session.api_user ) ) + + # Query ftrack hierarchy with parenting + ftrack_hierarchy = self.query_ftrack_entitites( + self.session, self.ft_project) + + # Fill ftrack entities to hierarchy context + # - there is no need to query entities again + matching_entities = self.find_matching_ftrack_entities( + hierarchy_context, ftrack_hierarchy) + # Query custom attribute values of each entity + custom_attr_values_by_id = self.query_custom_attribute_values( + self.session, matching_entities, hier_attrs) + # Get ftrack api module (as they are different per python version) ftrack_api = self.context.data["ftrackPythonModule"] @@ -260,75 +273,87 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): entity_type = entity_data['entity_type'] self.log.debug(entity_data) - self.log.debug(entity_type) - if entity_type.lower() == 'project': - entity = self.ft_project - - elif self.ft_project is None or parent is None: + entity = entity_data.get("ft_entity") + if entity is None and entity_type.lower() == "project": raise AssertionError( "Collected items are not in right order!" ) - # try to find if entity already exists - else: - query = ( - 'TypedContext where name is "{0}" and ' - 'project_id is "{1}"' - ).format(entity_name, self.ft_project["id"]) - try: - entity = self.session.query(query).one() - except Exception: - entity = None - # Create entity if not exists if entity is None: - entity = self.create_entity( - name=entity_name, - type=entity_type, - parent=parent - ) + entity = self.session.create(entity_type, { + "name": entity_name, + "parent": parent + }) + entity_data["ft_entity"] = entity + # self.log.info('entity: {}'.format(dict(entity))) # CUSTOM ATTRIBUTES - custom_attributes = entity_data.get('custom_attributes', []) - instances = [ - instance - for instance in self.context - if instance.data.get("asset") == entity["name"] - ] + custom_attributes = entity_data.get('custom_attributes', {}) + instances = [] + for instance in self.context: + instance_asset_name = instance.data.get("asset") + if ( + instance_asset_name + and instance_asset_name.lower() == entity["name"].lower() + ): + instances.append(instance) for instance in instances: instance.data["ftrackEntity"] = entity - for key in custom_attributes: + for key, cust_attr_value in custom_attributes.items(): + if cust_attr_value is None: + continue + hier_attr = hier_attr_by_key.get(key) # Use simple method if key is not hierarchical if not hier_attr: - assert (key in entity['custom_attributes']), ( - 'Missing custom attribute key: `{0}` in attrs: ' - '`{1}`'.format(key, entity['custom_attributes'].keys()) + if key not in entity["custom_attributes"]: + raise KnownPublishError(( + "Missing custom attribute in ftrack with name '{}'" + ).format(key)) + + entity['custom_attributes'][key] = cust_attr_value + continue + + attr_id = hier_attr["id"] + entity_values = custom_attr_values_by_id.get(entity["id"], {}) + # New value is defined by having id in values + # - it can be set to 'None' (ftrack allows that using API) + is_new_value = attr_id not in entity_values + attr_value = entity_values.get(attr_id) + + # Use ftrack operations method to set hiearchical + # attribute value. + # - this is because there may be non hiearchical custom + # attributes with different properties + entity_key = collections.OrderedDict(( + ("configuration_id", hier_attr["id"]), + ("entity_id", entity["id"]) + )) + op = None + if is_new_value: + op = ftrack_api.operation.CreateEntityOperation( + "CustomAttributeValue", + entity_key, + {"value": cust_attr_value} ) - entity['custom_attributes'][key] = custom_attributes[key] - - else: - # Use ftrack operations method to set hiearchical - # attribute value. - # - this is because there may be non hiearchical custom - # attributes with different properties - entity_key = collections.OrderedDict() - entity_key["configuration_id"] = hier_attr["id"] - entity_key["entity_id"] = entity["id"] - self.session.recorded_operations.push( - ftrack_api.operation.UpdateEntityOperation( - "ContextCustomAttributeValue", - entity_key, - "value", - ftrack_api.symbol.NOT_SET, - custom_attributes[key] - ) + elif attr_value != cust_attr_value: + op = ftrack_api.operation.UpdateEntityOperation( + "CustomAttributeValue", + entity_key, + "value", + attr_value, + cust_attr_value ) + if op is not None: + self.session.recorded_operations.push(op) + + if self.session.recorded_operations: try: self.session.commit() except Exception: @@ -342,7 +367,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): for instance in instances: task_name = instance.data.get("task") if task_name: - instances_by_task_name[task_name].append(instance) + instances_by_task_name[task_name.lower()].append(instance) tasks = entity_data.get('tasks', []) existing_tasks = [] @@ -500,21 +525,6 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): return task - def create_entity(self, name, type, parent): - entity = self.session.create(type, { - 'name': name, - 'parent': parent - }) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) - - return entity - def _get_active_assets(self, context): """ Returns only asset dictionary. Usually the last part of deep dictionary which @@ -536,19 +546,17 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): hierarchy_context = context.data["hierarchyContext"] - active_assets = [] + active_assets = set() # filter only the active publishing insatnces for instance in context: if instance.data.get("publish") is False: continue - if not instance.data.get("asset"): - continue - - active_assets.append(instance.data["asset"]) + asset_name = instance.data.get("asset") + if asset_name: + active_assets.add(asset_name) # remove duplicity in list - active_assets = list(set(active_assets)) - self.log.debug("__ active_assets: {}".format(active_assets)) + self.log.debug("__ active_assets: {}".format(list(active_assets))) return get_pure_hierarchy_data(hierarchy_context)