From 8fc0452438ff76b57d59d4d0c36a6934894e0e51 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 13:04:04 +0200 Subject: [PATCH 001/180] implemented UndefinedApplicationExecutable which is used if application does not have "executables" key --- openpype/lib/applications.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index d82b7cd847..074c722a81 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -484,6 +484,27 @@ class ApplicationExecutable: return bool(self._realpath()) +class UndefinedApplicationExecutable(ApplicationExecutable): + """Some applications do not require executable path from settings. + + In that case this class is used to "fake" existing executable. + """ + def __init__(self): + pass + + def __str__(self): + return self.__class__.__name__ + + def __repr__(self): + return "<{}>".format(self.__class__.__name__) + + def as_args(self): + return [] + + def exists(self): + return True + + @six.add_metaclass(ABCMeta) class LaunchHook: """Abstract base class of launch hook.""" From f694275bc4a9cd7c1605fdacaa0f6aedcac38ccb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 13:04:37 +0200 Subject: [PATCH 002/180] local settings will skip applications without "executables" key --- openpype/settings/lib.py | 6 +++++- openpype/tools/settings/local_settings/apps_widget.py | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index f61166fa69..4a3e66de33 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -532,7 +532,11 @@ def apply_local_settings_on_system_settings(system_settings, local_settings): variants = system_settings["applications"][app_group_name]["variants"] for app_name, app_value in value.items(): - if not app_value or app_name not in variants: + if ( + not app_value + or app_name not in variants + or "executables" not in variants[app_name] + ): continue executable = app_value.get("executable") diff --git a/openpype/tools/settings/local_settings/apps_widget.py b/openpype/tools/settings/local_settings/apps_widget.py index 5f4e5dd1c5..f228ecc826 100644 --- a/openpype/tools/settings/local_settings/apps_widget.py +++ b/openpype/tools/settings/local_settings/apps_widget.py @@ -121,6 +121,9 @@ class AppGroupWidget(QtWidgets.QWidget): widgets_by_variant_name = {} for variant_name, variant_entity in valid_variants.items(): + if "executables" not in variant_entity: + continue + variant_widget = AppVariantWidget( group_label, variant_name, variant_entity, content_widget ) From 5b9e8bb495eafc759a9e403b1056747caf2155cf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 13:08:06 +0200 Subject: [PATCH 003/180] Application can use `UndefinedApplicationExecutable` --- openpype/lib/applications.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 074c722a81..bed57d7022 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -191,26 +191,32 @@ class Application: self.full_label = full_label self._environment = data.get("environment") or {} + arguments = data.get("arguments") + if isinstance(arguments, dict): + arguments = arguments.get(platform.system().lower()) + + if not arguments: + arguments = [] + self.arguments = arguments + + if "executables" not in data: + self.executables = [ + UndefinedApplicationExecutable() + ] + return + _executables = data["executables"] + if isinstance(_executables, dict): + _executables = _executables.get(platform.system().lower()) + if not _executables: _executables = [] - elif isinstance(_executables, dict): - _executables = _executables.get(platform.system().lower()) or [] - - _arguments = data["arguments"] - if not _arguments: - _arguments = [] - - elif isinstance(_arguments, dict): - _arguments = _arguments.get(platform.system().lower()) or [] - executables = [] for executable in _executables: executables.append(ApplicationExecutable(executable)) self.executables = executables - self.arguments = _arguments def __repr__(self): return "<{}> - {}".format(self.__class__.__name__, self.full_name) From 5432a264653866cb00a3309b2b8a8679aea836da Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 13:08:38 +0200 Subject: [PATCH 004/180] skip executables and arguments key for unreal application --- .../defaults/system_settings/applications.json | 10 ---------- .../system_schema/host_settings/schema_unreal.json | 6 +++++- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 020924db67..72cd010cf2 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1101,16 +1101,6 @@ "variants": { "4-26": { "use_python_2": false, - "executables": { - "windows": [], - "darwin": [], - "linux": [] - }, - "arguments": { - "windows": [], - "darwin": [], - "linux": [] - }, "environment": {} } } diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json index df5ec0e6fa..fa1b45601f 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json @@ -30,7 +30,11 @@ "children": [ { "type": "schema_template", - "name": "template_host_variant_items" + "name": "template_host_variant_items", + "skip_paths": [ + "executables", + "arguments" + ] } ] } From b342760446cb1dfc7b62ec5cba6b58370a0ba7ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 13:08:47 +0200 Subject: [PATCH 005/180] named separator so can be skipped --- .../schemas/system_schema/host_settings/schema_unreal.json | 1 + .../system_schema/host_settings/template_host_variant_items.json | 1 + 2 files changed, 2 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json index fa1b45601f..133d6c9eaf 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json @@ -33,6 +33,7 @@ "name": "template_host_variant_items", "skip_paths": [ "executables", + "separator", "arguments" ] } diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant_items.json b/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant_items.json index ab4d2374a3..409efb006e 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant_items.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant_items.json @@ -14,6 +14,7 @@ "placeholder": "Executable path" }, { + "key": "separator", "type":"separator" }, { From 5208583d8fc9026930f186306ebafb3332ad1081 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 12 Jun 2021 12:29:41 +0200 Subject: [PATCH 006/180] reversed logic of drawing in drawTrigon --- .../widgets/color_widgets/color_triangle.py | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/openpype/widgets/color_widgets/color_triangle.py b/openpype/widgets/color_widgets/color_triangle.py index d4db175d84..ed7fefa7e8 100644 --- a/openpype/widgets/color_widgets/color_triangle.py +++ b/openpype/widgets/color_widgets/color_triangle.py @@ -724,27 +724,29 @@ class QtColorTriangle(QtWidgets.QWidget): lx = leftX[y] rx = rightX[y] + # if the xdist is 0, don't draw anything. + xdist = rx - lx + if xdist == 0.0: + continue + lxi = int(floor(lx)) rxi = int(floor(rx)) rc = rightColors[y] lc = leftColors[y] - # if the xdist is 0, don't draw anything. - xdist = rx - lx - if xdist != 0.0: - r = lc.r - g = lc.g - b = lc.b - rdelta = (rc.r - r) / xdist - gdelta = (rc.g - g) / xdist - bdelta = (rc.b - b) / xdist + r = lc.r + g = lc.g + b = lc.b + rdelta = (rc.r - r) / xdist + gdelta = (rc.g - g) / xdist + bdelta = (rc.b - b) / xdist - # Inner loop 2. Draws the line from left to right. - for x in range(lxi, rxi + 1): - buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b))) - r += rdelta - g += gdelta - b += bdelta + # Inner loop 2. Draws the line from left to right. + for x in range(lxi, rxi): + buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b))) + r += rdelta + g += gdelta + b += bdelta def _radius_at(self, pos, rect): mousexdist = pos.x() - float(rect.center().x()) From fbea4e4f9d0504d4d4af8a7a3e91fbfc58143f0c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 12 Jun 2021 12:29:56 +0200 Subject: [PATCH 007/180] draw 2 more pixels on each side of trigon --- openpype/widgets/color_widgets/color_triangle.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/widgets/color_widgets/color_triangle.py b/openpype/widgets/color_widgets/color_triangle.py index ed7fefa7e8..a6ee0e864f 100644 --- a/openpype/widgets/color_widgets/color_triangle.py +++ b/openpype/widgets/color_widgets/color_triangle.py @@ -741,6 +741,10 @@ class QtColorTriangle(QtWidgets.QWidget): gdelta = (rc.g - g) / xdist bdelta = (rc.b - b) / xdist + # Draw 2 more pixels on left side for smoothing + for x in range(lxi - 2, lxi): + buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b))) + # Inner loop 2. Draws the line from left to right. for x in range(lxi, rxi): buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b))) @@ -748,6 +752,10 @@ class QtColorTriangle(QtWidgets.QWidget): g += gdelta b += bdelta + # Draw 2 more pixels on right side for smoothing + for x in range(rxi, rxi + 3): + buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b))) + def _radius_at(self, pos, rect): mousexdist = pos.x() - float(rect.center().x()) mouseydist = pos.y() - float(rect.center().y()) From c9b30f276850bf12189c01a7ffdb49a5fe957b29 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 12 Jun 2021 12:30:39 +0200 Subject: [PATCH 008/180] draw trigon to different image which is painter over background with clipping --- .../widgets/color_widgets/color_triangle.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/openpype/widgets/color_widgets/color_triangle.py b/openpype/widgets/color_widgets/color_triangle.py index a6ee0e864f..f4a86c4fa5 100644 --- a/openpype/widgets/color_widgets/color_triangle.py +++ b/openpype/widgets/color_widgets/color_triangle.py @@ -241,7 +241,11 @@ class QtColorTriangle(QtWidgets.QWidget): # Blit the static generated background with the hue gradient onto # the double buffer. - buf = QtGui.QImage(self.bg_image.copy()) + buf = QtGui.QImage( + self.bg_image.width(), + self.bg_image.height(), + QtGui.QImage.Format_RGB32 + ) # Draw the trigon # Find the color with only the hue, and max value and saturation @@ -254,9 +258,21 @@ class QtColorTriangle(QtWidgets.QWidget): ) # Slow step: convert the image to a pixmap - pix = QtGui.QPixmap.fromImage(buf) + pix = self.bg_image.copy() pix_painter = QtGui.QPainter(pix) - pix_painter.setRenderHint(QtGui.QPainter.Antialiasing) + + pix_painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) + + trigon_path = QtGui.QPainterPath() + trigon_path.moveTo(self.point_a) + trigon_path.lineTo(self.point_b) + trigon_path.lineTo(self.point_c) + trigon_path.closeSubpath() + pix_painter.setClipPath(trigon_path) + + pix_painter.drawImage(0, 0, buf) + + pix_painter.setClipping(False) # Draw an outline of the triangle pix_painter.setPen(self._triangle_outline_pen) From 1e502773fdaf9c66a27f5a251bf46768da67b4d9 Mon Sep 17 00:00:00 2001 From: jezscha Date: Mon, 14 Jun 2021 10:31:57 +0000 Subject: [PATCH 009/180] Create draft PR for #1698 From ac219c1535675f54c98850a3d73380b86489ba5f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 15:00:27 +0200 Subject: [PATCH 010/180] color entity can have defined if can use alpha --- openpype/settings/entities/color_entity.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openpype/settings/entities/color_entity.py b/openpype/settings/entities/color_entity.py index 7a1b1d9848..dfaa75e761 100644 --- a/openpype/settings/entities/color_entity.py +++ b/openpype/settings/entities/color_entity.py @@ -12,6 +12,17 @@ class ColorEntity(InputEntity): def _item_initalization(self): self.valid_value_types = (list, ) self.value_on_not_set = [0, 0, 0, 255] + self.use_alpha = self.schema_data.get("use_alpha", True) + + def set_override_state(self, *args, **kwargs): + super(ColorEntity, self).set_override_state(*args, **kwargs) + value = self._current_value + if ( + not self.use_alpha + and isinstance(value, list) + and len(value) == 4 + ): + value[3] = 255 def convert_to_valid_type(self, value): """Conversion to valid type. @@ -51,4 +62,8 @@ class ColorEntity(InputEntity): ).format(value) raise BaseInvalidValueType(reason, self.path) new_value.append(item) + + # Make sure + if not self.use_alpha: + new_value[3] = 255 return new_value From 0c99c730c775582c5813fffa3507dde53d737d0e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 15:01:11 +0200 Subject: [PATCH 011/180] color widget can show color dialog without alpha based on `use_alpha` attribute --- .../tools/settings/settings/color_widget.py | 8 ++- .../color_widgets/color_picker_widget.py | 55 ++++++++++++------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/openpype/tools/settings/settings/color_widget.py b/openpype/tools/settings/settings/color_widget.py index fa0cd2c989..b38b46f3cb 100644 --- a/openpype/tools/settings/settings/color_widget.py +++ b/openpype/tools/settings/settings/color_widget.py @@ -25,7 +25,9 @@ class ColorWidget(InputWidget): self._dialog.open() return - dialog = ColorDialog(self.input_field.color(), self) + dialog = ColorDialog( + self.input_field.color(), self.entity.use_alpha, self + ) self._dialog = dialog dialog.open() @@ -120,12 +122,12 @@ class ColorViewer(QtWidgets.QWidget): class ColorDialog(QtWidgets.QDialog): - def __init__(self, color=None, parent=None): + def __init__(self, color=None, use_alpha=True, parent=None): super(ColorDialog, self).__init__(parent) self.setWindowTitle("Color picker dialog") - picker_widget = ColorPickerWidget(color, self) + picker_widget = ColorPickerWidget(color, use_alpha, self) footer_widget = QtWidgets.QWidget(self) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 81ec1f87aa..228d35a77c 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -17,19 +17,12 @@ from .color_inputs import ( class ColorPickerWidget(QtWidgets.QWidget): color_changed = QtCore.Signal(QtGui.QColor) - def __init__(self, color=None, parent=None): + def __init__(self, color=None, use_alpha=True, parent=None): super(ColorPickerWidget, self).__init__(parent) # Color triangle color_triangle = QtColorTriangle(self) - alpha_slider_proxy = QtWidgets.QWidget(self) - alpha_slider = AlphaSlider(QtCore.Qt.Horizontal, alpha_slider_proxy) - - alpha_slider_layout = QtWidgets.QHBoxLayout(alpha_slider_proxy) - alpha_slider_layout.setContentsMargins(5, 5, 5, 5) - alpha_slider_layout.addWidget(alpha_slider, 1) - # Eye picked widget pick_widget = PickScreenColorWidget() pick_widget.setMaximumHeight(50) @@ -47,8 +40,6 @@ class ColorPickerWidget(QtWidgets.QWidget): color_view = ColorViewer(self) color_view.setMaximumHeight(50) - alpha_inputs = AlphaInputs(self) - color_inputs_color = QtGui.QColor() col_inputs_by_label = [ ("HEX", HEXInputs(color_inputs_color, self)), @@ -58,6 +49,7 @@ class ColorPickerWidget(QtWidgets.QWidget): ] layout = QtWidgets.QGridLayout(self) + empty_col = 1 label_col = empty_col + 1 input_col = label_col + 1 @@ -65,6 +57,9 @@ class ColorPickerWidget(QtWidgets.QWidget): empty_widget.setFixedWidth(10) layout.addWidget(empty_widget, 0, empty_col) + layout.setColumnStretch(0, 1) + layout.setColumnStretch(input_col, 1) + row = 0 layout.addWidget(btn_pick_color, row, label_col) layout.addWidget(color_view, row, input_col) @@ -84,20 +79,41 @@ class ColorPickerWidget(QtWidgets.QWidget): layout.setRowStretch(row, 1) row += 1 - layout.addWidget(alpha_slider_proxy, row, 0) + alpha_label = None + alpha_slider_proxy = None + alpha_slider = None + alpha_inputs = None + if not use_alpha: + color.setAlpha(255) + else: + alpha_inputs = AlphaInputs(self) + alpha_label = QtWidgets.QLabel("Alpha", self) + alpha_slider_proxy = QtWidgets.QWidget(self) + alpha_slider = AlphaSlider( + QtCore.Qt.Horizontal, alpha_slider_proxy + ) + + alpha_slider_layout = QtWidgets.QHBoxLayout(alpha_slider_proxy) + alpha_slider_layout.setContentsMargins(5, 5, 5, 5) + alpha_slider_layout.addWidget(alpha_slider, 1) + + layout.addWidget(alpha_slider_proxy, row, 0) + + layout.addWidget(alpha_label, row, label_col) + layout.addWidget(alpha_inputs, row, input_col) + + row += 1 - layout.addWidget(QtWidgets.QLabel("Alpha", self), row, label_col) - layout.addWidget(alpha_inputs, row, input_col) - row += 1 layout.setRowStretch(row, 1) color_view.set_color(color_triangle.cur_color) color_triangle.color_changed.connect(self.triangle_color_changed) - alpha_slider.valueChanged.connect(self._on_alpha_slider_change) pick_widget.color_selected.connect(self.on_color_change) - alpha_inputs.alpha_changed.connect(self._on_alpha_inputs_changed) btn_pick_color.released.connect(self.pick_color) + if alpha_slider: + alpha_slider.valueChanged.connect(self._on_alpha_slider_change) + alpha_inputs.alpha_changed.connect(self._on_alpha_inputs_changed) self.color_input_fields = color_input_fields self.color_inputs_color = color_inputs_color @@ -131,7 +147,8 @@ class ColorPickerWidget(QtWidgets.QWidget): return self.color_view.color() def set_color(self, color): - self.alpha_inputs.set_alpha(color.alpha()) + if self.alpha_inputs: + self.alpha_inputs.set_alpha(color.alpha()) self.on_color_change(color) def pick_color(self): @@ -163,10 +180,10 @@ class ColorPickerWidget(QtWidgets.QWidget): def alpha_changed(self, value): self.color_view.set_alpha(value) - if self.alpha_slider.value() != value: + if self.alpha_slider and self.alpha_slider.value() != value: self.alpha_slider.setValue(value) - if self.alpha_inputs.alpha_value != value: + if self.alpha_inputs and self.alpha_inputs.alpha_value != value: self.alpha_inputs.set_alpha(value) def _on_alpha_inputs_changed(self, value): From f67b496bba879e909407ba2d14536fd6bf7aa035 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 15:01:30 +0200 Subject: [PATCH 012/180] fix color view piece size --- openpype/widgets/color_widgets/color_view.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/widgets/color_widgets/color_view.py b/openpype/widgets/color_widgets/color_view.py index 8644281a1d..b5fce28894 100644 --- a/openpype/widgets/color_widgets/color_view.py +++ b/openpype/widgets/color_widgets/color_view.py @@ -5,6 +5,8 @@ def draw_checkerboard_tile(piece_size=None, color_1=None, color_2=None): if piece_size is None: piece_size = 7 + # Make sure piece size is not float + piece_size = int(piece_size) if color_1 is None: color_1 = QtGui.QColor(188, 188, 188) From c21165643a92e52f2545acb02902d6881b5368f8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 15:02:55 +0200 Subject: [PATCH 013/180] added overscan_color to extract review settings --- .../settings/defaults/project_settings/global.json | 6 ++++++ .../projects_schema/schemas/schema_global_publish.json | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 4351f18a60..a86b3c712a 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -58,6 +58,12 @@ "overscan_crop": "", "width": 0, "height": 0, + "overscan_color": [ + 0, + 0, + 0, + 255 + ], "bg_color": [ 0, 0, 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 0c89575d74..9f7a573df9 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 @@ -202,6 +202,16 @@ "minimum": 0, "maximum": 100000 }, + { + "type": "label", + "label": "Overscan color is used only when output aspect pixel ratio is not same as input ratio." + }, + { + "type": "color", + "label": "Overscan color", + "key": "overscan_color", + "use_alpha": false + }, { "type": "label", "label": "Background color is used only when input have transparency and Alpha is higher than 0." From a733640a7f8c34ac496d236888e2e223da7e0b6d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 15:07:57 +0200 Subject: [PATCH 014/180] overscan color is used inside extract review if is set --- openpype/plugins/publish/extract_review.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index e1e24af3ea..47c5461517 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -1154,13 +1154,24 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("height_scale: `{}`".format(height_scale)) self.log.debug("height_half_pad: `{}`".format(height_half_pad)) + # Overscal color + overscan_color_value = "black" + overscan_color = output_def.get("overscan_color") + if overscan_color: + bg_red, bg_green, bg_blue, _ = overscan_color + overscan_color_value = "#{0:0>2X}{1:0>2X}{2:0>2X}".format( + bg_red, bg_green, bg_blue + ) + self.log.debug("Overscan color: `{}`".format(overscan_color_value)) + filters.extend([ "scale={}x{}:flags=lanczos".format( width_scale, height_scale ), - "pad={}:{}:{}:{}:black".format( + "pad={}:{}:{}:{}:{}".format( output_width, output_height, - width_half_pad, height_half_pad + width_half_pad, height_half_pad, + overscan_color_value ), "setsar=1" ]) From cc06efc4156bdedd59b4382aec72a5b7dce8a2f2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 14 Jun 2021 23:00:35 +0200 Subject: [PATCH 015/180] Nuke: settings prerender use limit by default --- openpype/settings/defaults/project_settings/nuke.json | 3 ++- .../schemas/projects_schema/schema_project_nuke.json | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 3736f67268..6ff732634e 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -13,7 +13,8 @@ "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}" }, "CreateWritePrerender": { - "fpath_template": "{work}/prerenders/nuke/{subset}/{subset}.{frame}.{ext}" + "fpath_template": "{work}/prerenders/nuke/{subset}/{subset}.{frame}.{ext}", + "use_range_limit": true } }, "publish": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index f709e84651..01a954f283 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -77,6 +77,11 @@ "type": "text", "key": "fpath_template", "label": "Path template" + }, + { + "type": "boolean", + "key": "use_range_limit", + "label": "Use Frame range limit by default" } ] } From b78f990c3c53ec61d42302ac6e23caeb3e144c95 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 14 Jun 2021 23:01:41 +0200 Subject: [PATCH 016/180] Nuke: render node linked knobs move to lib --- openpype/hosts/nuke/api/lib.py | 40 ++++++++++++------- .../plugins/create/create_write_prerender.py | 20 +++------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 7c274a03c7..d7f3fdc6ba 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -286,7 +286,8 @@ def add_button_write_to_read(node): node.addKnob(knob) -def create_write_node(name, data, input=None, prenodes=None, review=True): +def create_write_node(name, data, input=None, prenodes=None, + review=True, linked_knobs=None): ''' Creating write node which is group node Arguments: @@ -465,12 +466,16 @@ def create_write_node(name, data, input=None, prenodes=None, review=True): GN.addKnob(nuke.Text_Knob('', 'Rendering')) # Add linked knobs. - linked_knob_names = [ - "_grp-start_", - "use_limit", "first", "last", - "_grp-end_", - "Render" - ] + linked_knob_names = [] + + # add input linked knobs and create group only if any input + if linked_knobs: + linked_knob_names.append("_grp-start_") + linked_knob_names.extend(linked_knobs) + linked_knob_names.append("_grp-end_") + + linked_knob_names.append("Render") + for name in linked_knob_names: if "_grp-start_" in name: knob = nuke.Tab_Knob( @@ -481,13 +486,20 @@ def create_write_node(name, data, input=None, prenodes=None, review=True): "rnd_attr_end", "Rendering attributes", nuke.TABENDGROUP) GN.addKnob(knob) else: - link = nuke.Link_Knob("") - link.makeLink(write_node.name(), name) - link.setName(name) - if "Render" in name: - link.setLabel("Render Local") - link.setFlag(0x1000) - GN.addKnob(link) + if "___" in name: + # add devider + GN.addKnob(nuke.Text_Knob("")) + else: + # add linked knob by name + link = nuke.Link_Knob("") + link.makeLink(write_node.name(), name) + link.setName(name) + + # make render + if "Render" in name: + link.setLabel("Render Local") + link.setFlag(0x1000) + GN.addKnob(link) # adding write to read button add_button_write_to_read(GN) diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index 6e1a2ddd96..bb01236801 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -103,7 +103,8 @@ class CreateWritePrerender(plugin.PypeCreator): write_data, input=selected_node, prenodes=[], - review=False) + review=False, + linked_knobs=["channels", "___", "first", "last", "use_limit"]) # relinking to collected connections for i, input in enumerate(inputs): @@ -122,19 +123,10 @@ class CreateWritePrerender(plugin.PypeCreator): w_node = n write_node.end() - # add inner write node Tab - write_node.addKnob(nuke.Tab_Knob("WriteLinkedKnobs")) + if self.presets.get("use_range_limit"): + w_node["use_limit"].setValue(True) + w_node["first"].setValue(nuke.root()["first_frame"].value()) + w_node["last"].setValue(nuke.root()["last_frame"].value()) - # linking knobs to group property panel - linking_knobs = ["channels", "___", "first", "last", "use_limit"] - for k in linking_knobs: - if "___" in k: - write_node.addKnob(nuke.Text_Knob('')) - else: - lnk = nuke.Link_Knob(k) - lnk.makeLink(w_node.name(), k) - lnk.setName(k.replace('_', ' ').capitalize()) - lnk.clearFlag(nuke.STARTLINE) - write_node.addKnob(lnk) return write_node From 5692d570b760f7c1de00a320b84f564918b431e5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 11:08:36 +0200 Subject: [PATCH 017/180] #680 - Toggle Ftrack upload in StandalonePublisher Removed default_family in collect_context --- .../plugins/publish/collect_ftrack_family.py | 68 +++++++++++++------ .../defaults/project_settings/ftrack.json | 31 +++++++++ .../project_settings/standalonepublisher.json | 21 ++++-- .../schema_project_ftrack.json | 53 +++++++++++++++ 4 files changed, 146 insertions(+), 27 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index e6daed9a33..85e9c4ab9e 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -1,29 +1,57 @@ +""" +Requires: + none + +Provides: + instance -> families ([]) +""" +import os import pyblish.api +from openpype.lib.plugin_tools import filter_profiles -class CollectFtrackFamilies(pyblish.api.InstancePlugin): - """Collect family for ftrack publishing - - Add ftrack family to those instance that should be published to ftrack +class CollectFtrackFamily(pyblish.api.InstancePlugin): """ + Adds explicitly 'ftrack' to families to upload instance to FTrack. - order = pyblish.api.CollectorOrder + 0.3 - label = 'Add ftrack family' - families = ["model", - "setdress", - "model", - "animation", - "look", - "rig", - "camera" - ] - hosts = ["maya"] + Uses selection by combination of hosts/families/tasks names via + profiles resolution. + + Triggered everywhere, checks instance against configured + """ + label = "Collect Ftrack Family" + order = pyblish.api.CollectorOrder + 0.4999 + + profiles = None def process(self, instance): + if self.profiles: + anatomy_data = instance.context.data["anatomyData"] + task_name = anatomy_data.get("task", + os.environ["AVALON_TASK"]) + host_name = anatomy_data.get("app", + os.environ["AVALON_APP"]) + family = instance.data["family"] - # make ftrack publishable - if instance.data.get('families'): - instance.data['families'].append('ftrack') - else: - instance.data['families'] = ['ftrack'] + filtering_criteria = { + "hosts": host_name, + "families": family, + "tasks": task_name + } + profile = filter_profiles(self.profiles, filtering_criteria) + + if profile: + families = instance.data.get("families") + if profile["add_ftrack_family"]: + self.log.debug("Adding ftrack family") + if families and "ftrack" not in families: + instance.data["families"].append("ftrack") + else: + instance.data["families"] = ["ftrack"] + else: + self.log.debug("Removing ftrack family if present") + if families and "ftrack" in families: + instance.data["families"].pop("ftrack") + + self.log.debug("instance.data:: {}".format(instance.data)) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index b964ce07c3..26361a091a 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -200,6 +200,37 @@ } }, "publish": { + "CollectFtrackFamily": { + "enabled": true, + "profiles": [ + { + "families": [ + "render", + "image" + ], + "tasks": [], + "hosts": [ + "standalonepublisher" + ], + "add_ftrack_family": true + }, + { + "families": [ + "model", + "setdress", + "animation", + "look", + "rig", + "camera" + ], + "tasks": [], + "hosts": [ + "maya" + ], + "add_ftrack_family": true + } + ] + }, "IntegrateFtrackNote": { "enabled": true, "note_with_intent_template": "{intent}: {comment}", diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index 979f5285d3..8e833de7e0 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -105,16 +105,23 @@ "label": "Render", "family": "render", "icon": "image", - "defaults": ["Animation", "Lighting", "Lookdev", "Compositing"], + "defaults": [ + "Animation", + "Lighting", + "Lookdev", + "Compositing" + ], "help": "Rendered images or video files" }, "create_mov_batch": { - "name": "mov_batch", - "label": "Batch Mov", - "family": "render_mov_batch", - "icon": "image", - "defaults": ["Main"], - "help": "Process multiple Mov files and publish them for layout and comp." + "name": "mov_batch", + "label": "Batch Mov", + "family": "render_mov_batch", + "icon": "image", + "defaults": [ + "Main" + ], + "help": "Process multiple Mov files and publish them for layout and comp." }, "__dynamic_keys_labels__": { "create_workfile": "Workfile", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index aae2bb2539..af85ccb254 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -604,6 +604,59 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "CollectFtrackFamily", + "label": "Collect Ftrack Family", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "collapsible": true, + "key": "profiles", + "label": "Profiles", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "hosts", + "label": "Host names", + "type": "list", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "key": "add_ftrack_family", + "label": "Add Ftrack Family", + "type": "boolean" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, From dad17b9184be8d96c9ca39b9028d7f3e32675ce9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 11:27:08 +0200 Subject: [PATCH 018/180] #680 - fixed where to check for Task definition Modified default profile for StandalonePublisher --- .../modules/ftrack/plugins/publish/collect_ftrack_family.py | 4 ++-- openpype/settings/defaults/project_settings/ftrack.json | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index 85e9c4ab9e..a66d4217b5 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -28,8 +28,8 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): def process(self, instance): if self.profiles: anatomy_data = instance.context.data["anatomyData"] - task_name = anatomy_data.get("task", - os.environ["AVALON_TASK"]) + task_name = instance.data("task", + instance.context.data["task"]) host_name = anatomy_data.get("app", os.environ["AVALON_APP"]) family = instance.data["family"] diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 26361a091a..a8e121d7c3 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -204,10 +204,7 @@ "enabled": true, "profiles": [ { - "families": [ - "render", - "image" - ], + "families": [], "tasks": [], "hosts": [ "standalonepublisher" From 137097631d3f0189c380b94f4e853fb67746c810 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 11:30:37 +0200 Subject: [PATCH 019/180] #680 - added warning if no profiles configured, changed if order --- .../plugins/publish/collect_ftrack_family.py | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index a66d4217b5..e910970290 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -26,32 +26,30 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): profiles = None def process(self, instance): - if self.profiles: - anatomy_data = instance.context.data["anatomyData"] - task_name = instance.data("task", - instance.context.data["task"]) - host_name = anatomy_data.get("app", - os.environ["AVALON_APP"]) - family = instance.data["family"] + if not self.profiles: + self.log.warning("No profiles present for adding Ftrack family") + return - filtering_criteria = { - "hosts": host_name, - "families": family, - "tasks": task_name - } - profile = filter_profiles(self.profiles, filtering_criteria) + anatomy_data = instance.context.data["anatomyData"] + task_name = instance.data("task", + instance.context.data["task"]) + host_name = anatomy_data.get("app", + os.environ["AVALON_APP"]) + family = instance.data["family"] - if profile: - families = instance.data.get("families") - if profile["add_ftrack_family"]: - self.log.debug("Adding ftrack family") - if families and "ftrack" not in families: - instance.data["families"].append("ftrack") - else: - instance.data["families"] = ["ftrack"] - else: - self.log.debug("Removing ftrack family if present") - if families and "ftrack" in families: - instance.data["families"].pop("ftrack") + filtering_criteria = { + "hosts": host_name, + "families": family, + "tasks": task_name + } + profile = filter_profiles(self.profiles, filtering_criteria) + + if profile and profile["add_ftrack_family"]: + self.log.debug("Adding ftrack family") + families = instance.data.get("families") + if families and "ftrack" not in families: + instance.data["families"].append("ftrack") + else: + instance.data["families"] = ["ftrack"] self.log.debug("instance.data:: {}".format(instance.data)) From ad59c70a985fb9818866ee6302645d0eae18c2c0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 11:31:07 +0200 Subject: [PATCH 020/180] #680 - removed implicit assignment for SP --- .../standalonepublisher/plugins/publish/collect_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py index 43ab13cd79..d4855da140 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py @@ -34,7 +34,7 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): # presets batch_extensions = ["edl", "xml", "psd"] - default_families = ["ftrack"] + default_families = [] def process(self, context): # get json paths from os and load them From be02d1582247db69e9b79a68998ac3d10664e4ac Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Jun 2021 11:57:09 +0200 Subject: [PATCH 021/180] overscan color is also used in overscan crop --- openpype/plugins/publish/extract_review.py | 41 ++++++++++++++-------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 47c5461517..42fb2a8f93 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -986,9 +986,21 @@ class ExtractReview(pyblish.api.InstancePlugin): output_width = output_def.get("width") or None output_height = output_def.get("height") or None + # Overscal color + overscan_color_value = "black" + overscan_color = output_def.get("overscan_color") + if overscan_color: + bg_red, bg_green, bg_blue, _ = overscan_color + overscan_color_value = "#{0:0>2X}{1:0>2X}{2:0>2X}".format( + bg_red, bg_green, bg_blue + ) + self.log.debug("Overscan color: `{}`".format(overscan_color_value)) + # Convert overscan value video filters overscan_crop = output_def.get("overscan_crop") - overscan = OverscanCrop(input_width, input_height, overscan_crop) + overscan = OverscanCrop( + input_width, input_height, overscan_crop, overscan_color_value + ) overscan_crop_filters = overscan.video_filters() # Add overscan filters to filters if are any and modify input # resolution by it's values @@ -1154,16 +1166,6 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("height_scale: `{}`".format(height_scale)) self.log.debug("height_half_pad: `{}`".format(height_half_pad)) - # Overscal color - overscan_color_value = "black" - overscan_color = output_def.get("overscan_color") - if overscan_color: - bg_red, bg_green, bg_blue, _ = overscan_color - overscan_color_value = "#{0:0>2X}{1:0>2X}{2:0>2X}".format( - bg_red, bg_green, bg_blue - ) - self.log.debug("Overscan color: `{}`".format(overscan_color_value)) - filters.extend([ "scale={}x{}:flags=lanczos".format( width_scale, height_scale @@ -1718,12 +1720,15 @@ class OverscanCrop: item_regex = re.compile(r"([\+\-])?([0-9]+)(.+)?") relative_source_regex = re.compile(r"%([\+\-])") - def __init__(self, input_width, input_height, string_value): + def __init__( + self, input_width, input_height, string_value, overscal_color=None + ): # Make sure that is not None string_value = string_value or "" self.input_width = input_width self.input_height = input_height + self.overscal_color = overscal_color width, height = self._convert_string_to_values(string_value) self._width_value = width @@ -1778,16 +1783,22 @@ class OverscanCrop: elif width >= self.input_width and height >= self.input_height: output.append( - "pad={}:{}:(iw-ow)/2:(ih-oh)/2".format(width, height) + "pad={}:{}:(iw-ow)/2:(ih-oh)/2:{}".format( + width, height, self.overscal_color + ) ) elif width > self.input_width and height < self.input_height: output.append("crop=iw:{}".format(height)) - output.append("pad={}:ih:(iw-ow)/2:(ih-oh)/2".format(width)) + output.append("pad={}:ih:(iw-ow)/2:(ih-oh)/2:{}".format( + width, self.overscal_color + )) elif width < self.input_width and height > self.input_height: output.append("crop={}:ih".format(width)) - output.append("pad=iw:{}:(iw-ow)/2:(ih-oh)/2".format(height)) + output.append("pad=iw:{}:(iw-ow)/2:(ih-oh)/2:{}".format( + height, self.overscal_color + )) return output From 5ea17137495657c7f32c73359c890c252841ab92 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 12:00:00 +0200 Subject: [PATCH 022/180] #680 - changed logging Added explicit removing of ftrack family if configured to remove --- .../plugins/publish/collect_ftrack_family.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index e910970290..a66e6db29f 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -44,12 +44,19 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): } profile = filter_profiles(self.profiles, filtering_criteria) - if profile and profile["add_ftrack_family"]: - self.log.debug("Adding ftrack family") + if profile: families = instance.data.get("families") - if families and "ftrack" not in families: - instance.data["families"].append("ftrack") - else: - instance.data["families"] = ["ftrack"] + if profile["add_ftrack_family"]: + self.log.debug("Adding ftrack family") - self.log.debug("instance.data:: {}".format(instance.data)) + if families and "ftrack" not in families: + instance.data["families"].append("ftrack") + else: + instance.data["families"] = ["ftrack"] + else: + if families and "ftrack" in families: + self.log.debug("Explicitly removing 'ftrack'") + instance.data["families"].remove("ftrack") + + self.log.debug("Resulting families '{}' for '{}'".format( + instance.data["families"], instance.data["family"])) From 8b7326e9ec43e8a7cf7ca4a67bfa4bb1475dafb2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Jun 2021 12:21:33 +0200 Subject: [PATCH 023/180] changed position of overscan color --- .../defaults/project_settings/global.json | 4 ++-- .../schemas/schema_global_publish.json | 20 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index a86b3c712a..b7fa5e32e8 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -56,14 +56,14 @@ ] }, "overscan_crop": "", - "width": 0, - "height": 0, "overscan_color": [ 0, 0, 0, 255 ], + "width": 0, + "height": 0, "bg_color": [ 0, 0, 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 9f7a573df9..8ca203e3bc 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 @@ -182,6 +182,16 @@ "key": "overscan_crop", "label": "Overscan crop" }, + { + "type": "label", + "label": "Overscan color is used when input aspect ratio is not same as output aspect ratio." + }, + { + "type": "color", + "label": "Overscan color", + "key": "overscan_color", + "use_alpha": false + }, { "type": "label", "label": "Width and Height must be both set to higher value than 0 else source resolution is used." @@ -202,16 +212,6 @@ "minimum": 0, "maximum": 100000 }, - { - "type": "label", - "label": "Overscan color is used only when output aspect pixel ratio is not same as input ratio." - }, - { - "type": "color", - "label": "Overscan color", - "key": "overscan_color", - "use_alpha": false - }, { "type": "label", "label": "Background color is used only when input have transparency and Alpha is higher than 0." From 2c6c381450a4e334a3ef935db5c0d693272d8814 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Jun 2021 12:37:00 +0200 Subject: [PATCH 024/180] updated output defs settings image --- .../global_extract_review_output_defs.png | Bin 19871 -> 26078 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/website/docs/project_settings/assets/global_extract_review_output_defs.png b/website/docs/project_settings/assets/global_extract_review_output_defs.png index ce3c00ca40387344fbcad18194a0fa73c82801f2..f4c1661b110d93aeab337e2556fb13ae5692ea16 100644 GIT binary patch literal 26078 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 literal 19871 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 From 9aec3761de8b920aba0635abf3c777bd3c2dd9ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Jun 2021 13:05:09 +0200 Subject: [PATCH 025/180] added basic infor about overscan color and background color --- .../docs/project_settings/settings_project_global.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 5d23dd75e6..5c46cd185a 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -112,6 +112,10 @@ Profile may generate multiple outputs from a single input. Each output must defi | "-10% -200px" | 1800px 800px | | "-10% -0px" | 1800px 1000px | +- **`Overscan color`** + - Color of empty area caused by different aspect ratio of input and output. + - By default is set to black color. + - **`Letter Box`** - **Enabled** - Enable letter boxes - **Ratio** - Ratio of letter boxes @@ -124,6 +128,14 @@ Profile may generate multiple outputs from a single input. Each output must defi ![global_extract_review_letter_box_settings](assets/global_extract_review_letter_box_settings.png) ![global_extract_review_letter_box](assets/global_extract_review_letter_box.png) +- **`Background color`** + - Background color can be used for inputs with possible transparency (e.g. png sequence). + - Input's without possible alpha channel are ignored all the time (e.g. mov). + - Background color slows down rendering process. + - 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 + + ### IntegrateAssetNew Saves information for all published subsets into DB, published assets are available for other hosts, tools and tasks after. From d1f0003a927ebac997f3f898ee57c58836c0d320 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 15 Jun 2021 14:04:46 +0200 Subject: [PATCH 026/180] Nuke: fixing version frame range prerender exception --- .../nuke/plugins/publish/precollect_writes.py | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_writes.py b/openpype/hosts/nuke/plugins/publish/precollect_writes.py index 5eaac89e84..0b5fbc0479 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_writes.py @@ -1,5 +1,6 @@ import os import re +from pprint import pformat import nuke import pyblish.api import openpype.api as pype @@ -17,6 +18,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): def process(self, instance): _families_test = [instance.data["family"]] + instance.data["families"] + self.log.debug("_families_test: {}".format(_families_test)) node = None for x in instance: @@ -133,22 +135,29 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): "outputDir": output_dir, "ext": ext, "label": label, - "handleStart": handle_start, - "handleEnd": handle_end, - "frameStart": first_frame + handle_start, - "frameEnd": last_frame - handle_end, - "frameStartHandle": first_frame, - "frameEndHandle": last_frame, "outputType": output_type, "colorspace": colorspace, "deadlineChunkSize": deadlineChunkSize, "deadlinePriority": deadlinePriority }) - if "prerender" in _families_test: + if self.is_prerender(_families_test): instance.data.update({ - "family": "prerender", - "families": [] + "handleStart": 0, + "handleEnd": 0, + "frameStart": first_frame, + "frameEnd": last_frame, + "frameStartHandle": first_frame, + "frameEndHandle": last_frame, + }) + else: + instance.data.update({ + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStart": first_frame + handle_start, + "frameEnd": last_frame - handle_end, + "frameStartHandle": first_frame, + "frameEndHandle": last_frame, }) # * Add audio to instance if exists. @@ -170,4 +179,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): "filename": api.get_representation_path(repre_doc) }] - self.log.debug("instance.data: {}".format(instance.data)) + self.log.debug("instance.data: {}".format(pformat(instance.data))) + + def is_prerender(self, families): + return next((f for f in families if "prerender" in f), None) From b2cc66ba2282934b04dc8b8fd2369bc08b39dcb1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Jun 2021 14:34:58 +0200 Subject: [PATCH 027/180] added smoothnes to alpha slider --- openpype/widgets/color_widgets/color_inputs.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index eda8c618f1..ada8befd65 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -80,7 +80,7 @@ class AlphaSlider(QtWidgets.QSlider): painter.fillRect(event.rect(), QtCore.Qt.transparent) - painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform) + painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) rect = self.style().subControlRect( QtWidgets.QStyle.CC_Slider, opt, @@ -135,19 +135,8 @@ class AlphaSlider(QtWidgets.QSlider): painter.save() - gradient = QtGui.QRadialGradient() - radius = handle_rect.height() / 2 - center_x = handle_rect.width() / 2 + handle_rect.x() - center_y = handle_rect.height() - gradient.setCenter(center_x, center_y) - gradient.setCenterRadius(radius) - gradient.setFocalPoint(center_x, center_y) - - gradient.setColorAt(0.9, QtGui.QColor(127, 127, 127)) - gradient.setColorAt(1, QtCore.Qt.transparent) - painter.setPen(QtCore.Qt.NoPen) - painter.setBrush(gradient) + painter.setBrush(QtGui.QColor(127, 127, 127)) painter.drawEllipse(handle_rect) painter.restore() From bf6504eac53ff536a6b677f89dd98b55a38924eb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Jun 2021 14:35:09 +0200 Subject: [PATCH 028/180] removed unused slide_style --- .../widgets/color_widgets/color_inputs.py | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index ada8befd65..6f5d4baa02 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -4,35 +4,6 @@ from Qt import QtWidgets, QtCore, QtGui from .color_view import draw_checkerboard_tile -slide_style = """ -QSlider::groove:horizontal { - background: qlineargradient( - x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #000, stop: 1 #fff - ); - height: 8px; - border-radius: 4px; -} - -QSlider::handle:horizontal { - background: qlineargradient( - x1:0, y1:0, x2:1, y2:1, stop:0 #ddd, stop:1 #bbb - ); - border: 1px solid #777; - width: 8px; - margin-top: -1px; - margin-bottom: -1px; - border-radius: 4px; -} - -QSlider::handle:horizontal:hover { - background: qlineargradient( - x1:0, y1:0, x2:1, y2:1, stop:0 #eee, stop:1 #ddd - ); - border: 1px solid #444;ff - border-radius: 4px; -}""" - - class AlphaSlider(QtWidgets.QSlider): def __init__(self, *args, **kwargs): super(AlphaSlider, self).__init__(*args, **kwargs) From 98a1b9973ee6d8118c395c9dc4bbc96594883931 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 15 Jun 2021 15:12:21 +0200 Subject: [PATCH 029/180] Nuke: improving validator of rendered frames --- .../publish/validate_rendered_frames.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py index 8b71aff1ac..0c88014649 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py @@ -61,7 +61,6 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): hosts = ["nuke", "nukestudio"] actions = [RepairCollectionActionToLocal, RepairCollectionActionToFarm] - def process(self, instance): for repre in instance.data["representations"]: @@ -78,10 +77,10 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): collection = collections[0] - frame_length = int( - instance.data["frameEndHandle"] - - instance.data["frameStartHandle"] + 1 - ) + fstartH = instance.data["frameStartHandle"] + fendH = instance.data["frameEndHandle"] + + frame_length = int(fendH - fstartH + 1) if frame_length != 1: if len(collections) != 1: @@ -95,7 +94,16 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): raise ValidationException(msg) collected_frames_len = int(len(collection.indexes)) + coll_start = min(collection.indexes) + coll_end = max(collection.indexes) + self.log.info("frame_length: {}".format(frame_length)) + self.log.info("collected_frames_len: {}".format( + collected_frames_len)) + self.log.info("fstartH-fendH: {}-{}".format(fstartH, fendH)) + self.log.info( + "coll_start-coll_end: {}-{}".format(coll_start, coll_end)) + self.log.info( "len(collection.indexes): {}".format(collected_frames_len) ) @@ -103,8 +111,11 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): if ("slate" in instance.data["families"]) \ and (frame_length != collected_frames_len): collected_frames_len -= 1 + fstartH += 1 - assert (collected_frames_len == frame_length), ( + assert ((collected_frames_len >= frame_length) + and (coll_start <= fstartH) + and (coll_end >= fendH)), ( "{} missing frames. Use repair to render all frames" ).format(__name__) From c105eedbc7503f0ef8475ef26b0c68c53a023241 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 15:23:49 +0200 Subject: [PATCH 030/180] #680 - fixed logging, task selection --- .../plugins/publish/collect_ftrack_family.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index a66e6db29f..13ae40fd44 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -6,7 +6,9 @@ Provides: instance -> families ([]) """ import os + import pyblish.api +import avalon.api from openpype.lib.plugin_tools import filter_profiles @@ -31,10 +33,11 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): return anatomy_data = instance.context.data["anatomyData"] - task_name = instance.data("task", - instance.context.data["task"]) + task_name = instance.data.get("task", + instance.context.data.get("task", + avalon.api.Session["AVALON_TASK"])) host_name = anatomy_data.get("app", - os.environ["AVALON_APP"]) + avalon.api.Session["AVALON_APP"]) family = instance.data["family"] filtering_criteria = { @@ -47,7 +50,8 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): if profile: families = instance.data.get("families") if profile["add_ftrack_family"]: - self.log.debug("Adding ftrack family") + self.log.debug("Adding ftrack family for '{}'". + format(instance.data.get("family"))) if families and "ftrack" not in families: instance.data["families"].append("ftrack") @@ -57,6 +61,6 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): if families and "ftrack" in families: self.log.debug("Explicitly removing 'ftrack'") instance.data["families"].remove("ftrack") - - self.log.debug("Resulting families '{}' for '{}'".format( - instance.data["families"], instance.data["family"])) + else: + self.log.debug("Instance '{}' doesn't match any profile".format( + instance.data.get("family"))) From 2b202dd7599fcc1aafe337923040a345043f274f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 16:25:02 +0200 Subject: [PATCH 031/180] #680 - added remove "matchmove" to settings Removed unneeded plugin --- .../plugins/publish/collect_matchmove.py | 29 ------------------- .../defaults/project_settings/ftrack.json | 8 +++++ 2 files changed, 8 insertions(+), 29 deletions(-) delete mode 100644 openpype/hosts/standalonepublisher/plugins/publish/collect_matchmove.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_matchmove.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_matchmove.py deleted file mode 100644 index 5d9e8ddfb4..0000000000 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_matchmove.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Requires: - Nothing - -Provides: - Instance -""" - -import pyblish.api -import logging - - -log = logging.getLogger("collector") - - -class CollectMatchmovePublish(pyblish.api.InstancePlugin): - """ - Collector with only one reason for its existence - remove 'ftrack' - family implicitly added by Standalone Publisher - """ - - label = "Collect Matchmove - SA Publish" - order = pyblish.api.CollectorOrder - families = ["matchmove"] - hosts = ["standalonepublisher"] - - def process(self, instance): - if "ftrack" in instance.data["families"]: - instance.data["families"].remove("ftrack") diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index a8e121d7c3..f83b6cef3a 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -211,6 +211,14 @@ ], "add_ftrack_family": true }, + { + "families": ["matchmove"], + "tasks": [], + "hosts": [ + "standalonepublisher" + ], + "add_ftrack_family": false + }, { "families": [ "model", From 4f0e30b46debfa44f2f4b8816248a0f9bca15f82 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 16:26:09 +0200 Subject: [PATCH 032/180] #680 - removed default_families as now there are profiles in Setting doing that --- .../standalonepublisher/plugins/publish/collect_context.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py index d4855da140..6913e0836d 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py @@ -34,7 +34,6 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): # presets batch_extensions = ["edl", "xml", "psd"] - default_families = [] def process(self, context): # get json paths from os and load them @@ -213,10 +212,6 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): subset = in_data["subset"] # If instance data already contain families then use it instance_families = in_data.get("families") or [] - # Make sure default families are in instance - for default_family in self.default_families or []: - if default_family not in instance_families: - instance_families.append(default_family) instance = context.create_instance(subset) instance.data.update( From 692195bf9378e029e6f1acbf1cbeeec77b13ecd4 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Tue, 15 Jun 2021 14:35:12 +0000 Subject: [PATCH 033/180] [Automated] Bump version --- CHANGELOG.md | 14 +++++++++++--- openpype/version.py | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 537be94076..0b31b0ac45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,14 @@ # Changelog -## [3.1.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.1.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.0.0...HEAD) #### 🚀 Enhancements +- Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703) +- Scrolling in OpenPype info widget [\#1702](https://github.com/pypeclub/OpenPype/pull/1702) +- OpenPype style in modules [\#1694](https://github.com/pypeclub/OpenPype/pull/1694) - Sort applications and tools alphabetically in Settings UI [\#1689](https://github.com/pypeclub/OpenPype/pull/1689) - \#683 - Validate Frame Range in Standalone Publisher [\#1683](https://github.com/pypeclub/OpenPype/pull/1683) - Hiero: old container versions identify with red color [\#1682](https://github.com/pypeclub/OpenPype/pull/1682) @@ -13,26 +16,31 @@ - Remove outline in stylesheet [\#1667](https://github.com/pypeclub/OpenPype/pull/1667) - TVPaint: Creator take layer name as default value for subset variant [\#1663](https://github.com/pypeclub/OpenPype/pull/1663) - TVPaint custom subset template [\#1662](https://github.com/pypeclub/OpenPype/pull/1662) +- Editorial: conform assets validator [\#1659](https://github.com/pypeclub/OpenPype/pull/1659) - Feature Slack integration [\#1657](https://github.com/pypeclub/OpenPype/pull/1657) - Nuke - Publish simplification [\#1653](https://github.com/pypeclub/OpenPype/pull/1653) -- StandalonePublisher: adding exception for adding `delete` tag to repre [\#1650](https://github.com/pypeclub/OpenPype/pull/1650) - \#1333 - added tooltip hints to Pyblish buttons [\#1649](https://github.com/pypeclub/OpenPype/pull/1649) #### 🐛 Bug fixes +- Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) +- Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) +- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) - Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671) - Mac launch arguments fix [\#1660](https://github.com/pypeclub/OpenPype/pull/1660) - Fix missing dbm python module [\#1652](https://github.com/pypeclub/OpenPype/pull/1652) +- Transparent branches in view on Mac [\#1648](https://github.com/pypeclub/OpenPype/pull/1648) - Add asset on task item [\#1646](https://github.com/pypeclub/OpenPype/pull/1646) - Project manager save and queue [\#1645](https://github.com/pypeclub/OpenPype/pull/1645) - New project anatomy values [\#1644](https://github.com/pypeclub/OpenPype/pull/1644) +- Farm publishing: check if published items do exist [\#1573](https://github.com/pypeclub/OpenPype/pull/1573) **Merged pull requests:** +- update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697) - Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686) -- Add docstrings to Project manager tool [\#1556](https://github.com/pypeclub/OpenPype/pull/1556) # Changelog diff --git a/openpype/version.py b/openpype/version.py index bf261d41b2..448110d653 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.1.0-nightly.3" +__version__ = "3.1.0-nightly.4" From 91d75c89b0947a6e16df50726312d664a545d18c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 15 Jun 2021 17:54:00 +0200 Subject: [PATCH 034/180] settings: fix imageio granularity --- .../projects_schema/schemas/schema_anatomy_imageio.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json index 3c589f9492..2b2eab8868 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json @@ -3,7 +3,6 @@ "key": "imageio", "label": "Color Management and Output Formats", "is_file": true, - "is_group": true, "children": [ { "key": "hiero", @@ -15,6 +14,7 @@ "type": "dict", "label": "Workfile", "collapsible": false, + "is_group": true, "children": [ { "type": "form", @@ -89,6 +89,7 @@ "type": "dict", "label": "Colorspace on Inputs by regex detection", "collapsible": true, + "is_group": true, "children": [ { "type": "list", @@ -123,6 +124,7 @@ "type": "dict", "label": "Viewer", "collapsible": false, + "is_group": true, "children": [ { "type": "text", @@ -136,6 +138,7 @@ "type": "dict", "label": "Workfile", "collapsible": false, + "is_group": true, "children": [ { "type": "form", @@ -233,6 +236,7 @@ "type": "dict", "label": "Nodes", "collapsible": true, + "is_group": true, "children": [ { "key": "requiredNodes", @@ -335,6 +339,7 @@ "type": "dict", "label": "Colorspace on Inputs by regex detection", "collapsible": true, + "is_group": true, "children": [ { "type": "list", From 69dc652db35410e0d85f4ec351bd9e9cff213116 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 15 Jun 2021 17:54:43 +0200 Subject: [PATCH 035/180] nuke: default `create_directories` nuke write node https://github.com/pypeclub/client/issues/66 --- openpype/settings/defaults/project_anatomy/imageio.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/settings/defaults/project_anatomy/imageio.json b/openpype/settings/defaults/project_anatomy/imageio.json index ff16c22663..fcebc876f5 100644 --- a/openpype/settings/defaults/project_anatomy/imageio.json +++ b/openpype/settings/defaults/project_anatomy/imageio.json @@ -78,6 +78,10 @@ { "name": "colorspace", "value": "linear" + }, + { + "name": "create_directories", + "value": "True" } ] }, @@ -114,6 +118,10 @@ { "name": "colorspace", "value": "linear" + }, + { + "name": "create_directories", + "value": "True" } ] } From 53112e04d67cd85cc87eb1265871daf67a6165e6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 15 Jun 2021 19:19:53 +0200 Subject: [PATCH 036/180] fix condition --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 2 +- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index fa1ce7f9a9..57e3f478f1 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -72,7 +72,7 @@ class ExtractPlayblast(openpype.api.Extractor): # Isolate view is requested by having objects in the set besides a # camera. - if preset.pop("isolate_view", False) or instance.data.get("isolate"): + if preset.pop("isolate_view", False) and instance.data.get("isolate"): preset["isolate"] = instance.data["setMembers"] # Show/Hide image planes on request. diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 5a91888781..aa8adc3986 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -75,7 +75,7 @@ class ExtractThumbnail(openpype.api.Extractor): # Isolate view is requested by having objects in the set besides a # camera. - if preset.pop("isolate_view", False) or instance.data.get("isolate"): + if preset.pop("isolate_view", False) and instance.data.get("isolate"): preset["isolate"] = instance.data["setMembers"] with maintained_time(): From e00072deae7719d3d1871ceb418642602fb5f86d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 15 Jun 2021 23:00:22 +0200 Subject: [PATCH 037/180] update release workflow --- .github/workflows/release.yml | 108 +++++++++++++++++++++------------- 1 file changed, 67 insertions(+), 41 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a5d1822310..835657d2d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,9 @@ name: Stable Release on: - push: - tags: - - '*[0-9].*[0-9].*[0-9]*' + release: + types: + - prereleased jobs: create_release: @@ -23,27 +23,18 @@ jobs: - name: Install Python requirements run: pip install gitpython semver - - name: Set env - run: | - echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - git config user.email ${{ secrets.CI_EMAIL }} - git config user.name ${{ secrets.CI_USER }} - git fetch - git checkout -b main origin/main - git tag -d ${GITHUB_REF#refs/*/} - git remote set-url --push origin https://pypebot:${{ secrets.ADMIN_TOKEN }}@github.com/pypeclub/openpype - git push origin --delete ${GITHUB_REF#refs/*/} - echo PREVIOUS_VERSION=`git describe --tags --match="[0-9]*" --abbrev=0` >> $GITHUB_ENV - - name: 💉 Inject new version into files id: version - if: steps.version_type.outputs.type != 'skip' run: | - python ./tools/ci_tools.py --version ${{ env.RELEASE_VERSION }} + echo ::set-output name=current_version::${GITHUB_REF#refs/*/} + RESULT=$(python ./tools/ci_tools.py --finalize ${GITHUB_REF#refs/*/}) + LASTRELEASE=$(python ./tools/ci_tools.py --lastversion release) + + echo ::set-output name=last_release::$LASTRELEASE + echo ::set-output name=release_tag::$RESULT - name: "✏️ Generate full changelog" - if: steps.version_type.outputs.type != 'skip' + if: steps.version.outputs.release_tag != 'skip' id: generate-full-changelog uses: heinrichreimer/github-changelog-generator-action@v2.2 with: @@ -55,7 +46,6 @@ jobs: addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}' issues: false issuesWoLabels: false - sinceTag: "3.0.0" maxIssues: 100 pullRequests: true prWoLabels: false @@ -64,39 +54,75 @@ jobs: compareLink: true stripGeneratorNotice: true verbose: true - futureRelease: ${{ env.RELEASE_VERSION }} + futureRelease: ${{ steps.version.outputs.release_tag }} excludeTagsRegex: "CI/.+" releaseBranch: "main" - - name: "🖨️ Print changelog to console" - run: echo ${{ steps.generate-last-changelog.outputs.changelog }} - - name: 💾 Commit and Tag id: git_commit - if: steps.version_type.outputs.type != 'skip' + if: steps.version.outputs.release_tag != 'skip' run: | + git config user.email ${{ secrets.CI_EMAIL }} + git config user.name ${{ secrets.CI_USER }} git add . git commit -m "[Automated] Release" - tag_name="${{ env.RELEASE_VERSION }}" - git push - git tag -fa $tag_name -m "stable release" - git remote set-url --push origin https://pypebot:${{ secrets.ADMIN_TOKEN }}@github.com/pypeclub/openpype - git push origin $tag_name + tag_name="${{ steps.version.outputs.release_tag }}" + git tag -a $tag_name -m "stable release" - - name: "🚀 Github Release" - uses: docker://antonyurchenko/git-release:latest - env: - GITHUB_TOKEN: ${{ secrets.ADMIN_TOKEN }} - DRAFT_RELEASE: "false" - PRE_RELEASE: "false" - CHANGELOG_FILE: "CHANGELOG.md" - ALLOW_EMPTY_CHANGELOG: "false" - ALLOW_TAG_PREFIX: "true" + - name: 🔏 Push to protected main branch + if: steps.version.outputs.release_tag != 'skip' + uses: CasperWA/push-protected@v2 + with: + token: ${{ secrets.ADMIN_TOKEN }} + branch: main + tags: true + unprotect_reviews: true + + - name: "✏️ Generate last changelog" + if: steps.version.outputs.release_tag != 'skip' + id: generate-last-changelog + uses: heinrichreimer/github-changelog-generator-action@v2.2 + with: + token: ${{ secrets.ADMIN_TOKEN }} + breakingLabel: '**💥 Breaking**' + enhancementLabel: '**🚀 Enhancements**' + bugsLabel: '**🐛 Bug fixes**' + deprecatedLabel: '**⚠️ Deprecations**' + addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}' + issues: false + issuesWoLabels: false + maxIssues: 100 + pullRequests: true + prWoLabels: false + author: false + unreleased: true + compareLink: true + stripGeneratorNotice: true + verbose: true + futureRelease: ${{ steps.version.outputs.release_tag }} + excludeTagsRegex: "CI/.+" + releaseBranch: "main" + stripHeaders: true + sinceTag: ${{ steps.version.outputs.last_release }} - - name: 🔨 Merge main back to develop + - name: 🚀 Github Release + if: steps.version.outputs.release_tag != 'skip' + uses: ncipollo/release-action@v1 + with: + body: ${{ steps.generate-last-changelog.outputs.changelog }} + tag: ${{ steps.version.outputs.release_tag }} + token: ${{ secrets.ADMIN_TOKEN }} + + - name: ☠ Delete Pre-release + if: steps.version.outputs.release_tag != 'skip' + uses: cb80/delrel@latest + with: + tag: "${{ steps.version.outputs.current_version }}" + + - name: 🔁 Merge main back to develop + if: steps.version.outputs.release_tag != 'skip' uses: everlytic/branch-merge@1.1.0 - if: steps.version_type.outputs.type != 'skip' with: github_token: ${{ secrets.ADMIN_TOKEN }} source_ref: 'main' From b878df6c3bf7a74dc732ff9f9df93949beaaa5b1 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 15 Jun 2021 23:07:10 +0200 Subject: [PATCH 038/180] upgrade ci tools --- tools/ci_tools.py | 53 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/tools/ci_tools.py b/tools/ci_tools.py index 85ef6bdcee..436551c243 100644 --- a/tools/ci_tools.py +++ b/tools/ci_tools.py @@ -92,6 +92,24 @@ def calculate_next_nightly(token="nightly"): next_tag = last_pre_v.bump_prerelease(token=token).__str__() return next_tag +def finalize_latest_nightly(): + last_prerelease, last_pre_tag = get_last_version("CI") + last_pre_v = VersionInfo.parse(last_prerelease) + last_pre_v_finalized = last_pre_v.finalize_version() + # print(last_pre_v_finalized) + + return last_pre_v_finalized.__str__() + +def finalize_prerelease(prerelease): + + if "/" in prerelease: + prerelease = prerelease.split("/")[-1] + + prerelease_v = VersionInfo.parse(prerelease) + prerelease_v_finalized = prerelease_v.finalize_version() + + return prerelease_v_finalized.__str__() + def main(): usage = "usage: %prog [options] arg" @@ -102,12 +120,22 @@ def main(): parser.add_option("-b", "--bump", dest="bump", action="store_true", help="Return if there is something to bump") - parser.add_option("-v", "--version", - dest="version", action="store", - help="work with explicit version") + parser.add_option("-r", "--release-latest", + dest="releaselatest", action="store_true", + help="finalize latest prerelease to a release") parser.add_option("-p", "--prerelease", dest="prerelease", action="store", help="define prerelease token") + parser.add_option("-f", "--finalize", + dest="finalize", action="store", + help="define prerelease token") + parser.add_option("-v", "--version", + dest="version", action="store", + help="work with explicit version") + parser.add_option("-l", "--lastversion", + dest="lastversion", action="store", + help="work with explicit version") + (options, args) = parser.parse_args() @@ -124,6 +152,25 @@ def main(): print(next_tag_v) bump_file_versions(next_tag_v) + if options.finalize: + new_release = finalize_prerelease(options.finalize) + print(new_release) + bump_file_versions(new_release) + + if options.lastversion: + last_release, last_release_tag = get_last_version(options.lastversion) + print(last_release_tag) + + if options.releaselatest: + new_release = finalize_latest_nightly() + last_release, last_release_tag = get_last_version("release") + + if VersionInfo.parse(new_release) > VersionInfo.parse(last_release): + print(new_release) + bump_file_versions(new_release) + else: + print("skip") + if options.prerelease: current_prerelease = VersionInfo.parse(options.prerelease) new_prerelease = current_prerelease.bump_prerelease().__str__() From 664c1823cbcb9949eccd41bbe594aee97b1d842f Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 15 Jun 2021 23:35:00 +0200 Subject: [PATCH 039/180] exclude history from last changelog --- .github/workflows/prerelease.yml | 8 ++++---- .github/workflows/release.yml | 12 +++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 45604e431d..d0853e74d6 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -43,10 +43,10 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - breakingLabel: '#### 💥 Breaking' - enhancementLabel: '#### 🚀 Enhancements' - bugsLabel: '#### 🐛 Bug fixes' - deprecatedLabel: '#### ⚠️ Deprecations' + breakingLabel: '**💥 Breaking**' + enhancementLabel: '**🚀 Enhancements**' + bugsLabel: '**🐛 Bug fixes**' + deprecatedLabel: '**⚠️ Deprecations**' addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}' issues: false issuesWoLabels: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 835657d2d2..e818929ffe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,13 +39,14 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - breakingLabel: '#### 💥 Breaking' - enhancementLabel: '#### 🚀 Enhancements' - bugsLabel: '#### 🐛 Bug fixes' - deprecatedLabel: '#### ⚠️ Deprecations' + breakingLabel: '**💥 Breaking**' + enhancementLabel: '**🚀 Enhancements**' + bugsLabel: '**🐛 Bug fixes**' + deprecatedLabel: '**⚠️ Deprecations**' addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}' issues: false issuesWoLabels: false + sinceTag: "3.0.0" maxIssues: 100 pullRequests: true prWoLabels: false @@ -91,6 +92,7 @@ jobs: addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}' issues: false issuesWoLabels: false + sinceTag: ${{ steps.version.outputs.last_release }} maxIssues: 100 pullRequests: true prWoLabels: false @@ -103,7 +105,7 @@ jobs: excludeTagsRegex: "CI/.+" releaseBranch: "main" stripHeaders: true - sinceTag: ${{ steps.version.outputs.last_release }} + base: '' - name: 🚀 Github Release From 9a70c3d7aea8e3d58a582b81967031f49ab2d046 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Tue, 15 Jun 2021 21:41:17 +0000 Subject: [PATCH 040/180] [Automated] Release --- CHANGELOG.md | 10 ++++------ openpype/version.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b31b0ac45..043295eb8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ # Changelog -## [3.1.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.1.0](https://github.com/pypeclub/OpenPype/tree/3.1.0) (2021-06-15) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.0.0...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.0.0...3.1.0) -#### 🚀 Enhancements +**🚀 Enhancements** - Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703) - Scrolling in OpenPype info widget [\#1702](https://github.com/pypeclub/OpenPype/pull/1702) @@ -21,7 +21,7 @@ - Nuke - Publish simplification [\#1653](https://github.com/pypeclub/OpenPype/pull/1653) - \#1333 - added tooltip hints to Pyblish buttons [\#1649](https://github.com/pypeclub/OpenPype/pull/1649) -#### 🐛 Bug fixes +**🐛 Bug fixes** - Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) - Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) @@ -33,9 +33,7 @@ - Fix missing dbm python module [\#1652](https://github.com/pypeclub/OpenPype/pull/1652) - Transparent branches in view on Mac [\#1648](https://github.com/pypeclub/OpenPype/pull/1648) - Add asset on task item [\#1646](https://github.com/pypeclub/OpenPype/pull/1646) -- Project manager save and queue [\#1645](https://github.com/pypeclub/OpenPype/pull/1645) - New project anatomy values [\#1644](https://github.com/pypeclub/OpenPype/pull/1644) -- Farm publishing: check if published items do exist [\#1573](https://github.com/pypeclub/OpenPype/pull/1573) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index 448110d653..4312333660 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.1.0-nightly.4" +__version__ = "3.1.0" From cfc3e49f3209f04714be46e24b728dc2879208f9 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 16 Jun 2021 00:00:10 +0200 Subject: [PATCH 041/180] cut out history from last changelog --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e818929ffe..37e1cb4b15 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -105,7 +105,7 @@ jobs: excludeTagsRegex: "CI/.+" releaseBranch: "main" stripHeaders: true - base: '' + base: 'none' - name: 🚀 Github Release From bca04289ef1771f96679ab3da94fb957e9b0fdff Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 12:16:33 +0200 Subject: [PATCH 042/180] set default subset template for review family in tvpaint --- .../settings/defaults/project_settings/global.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index b7fa5e32e8..c3c43c3b3b 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -232,6 +232,16 @@ ], "tasks": [], "template": "{family}{Task}_{Render_layer}_{Render_pass}" + }, + { + "families": [ + "review" + ], + "hosts": [ + "tvpaint" + ], + "tasks": [], + "template": "{family}{Task}" } ] }, From 29271dc0c777234a7ad3952fef50ed42a6d3ea2c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 16 Jun 2021 12:19:11 +0200 Subject: [PATCH 043/180] #680 - added additional filtering on 'families' instead of main 'family' --- .../plugins/publish/collect_ftrack_family.py | 64 +++++++++++++++---- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index 13ae40fd44..8dbcb0ab2c 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -5,8 +5,6 @@ Requires: Provides: instance -> families ([]) """ -import os - import pyblish.api import avalon.api @@ -20,10 +18,14 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): Uses selection by combination of hosts/families/tasks names via profiles resolution. - Triggered everywhere, checks instance against configured + Triggered everywhere, checks instance against configured. + + Checks advanced filtering which works on 'families' not on main + 'family', as some variants dynamically resolves addition of ftrack + based on 'families' (editorial drives it by presence of 'review') """ label = "Collect Ftrack Family" - order = pyblish.api.CollectorOrder + 0.4999 + order = pyblish.api.CollectorOrder + 0.4998 profiles = None @@ -34,8 +36,7 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): anatomy_data = instance.context.data["anatomyData"] task_name = instance.data.get("task", - instance.context.data.get("task", - avalon.api.Session["AVALON_TASK"])) + avalon.api.Session["AVALON_TASK"]) host_name = anatomy_data.get("app", avalon.api.Session["AVALON_APP"]) family = instance.data["family"] @@ -49,7 +50,17 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): if profile: families = instance.data.get("families") - if profile["add_ftrack_family"]: + add_ftrack_family = profile["add_ftrack_family"] + + additional_filters = profile.get("additional_filters") + if additional_filters: + add_ftrack_family = self._get_add_ftrack_f_from_addit_filters( + additional_filters, + families, + add_ftrack_family + ) + + if add_ftrack_family: self.log.debug("Adding ftrack family for '{}'". format(instance.data.get("family"))) @@ -57,10 +68,41 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): instance.data["families"].append("ftrack") else: instance.data["families"] = ["ftrack"] - else: - if families and "ftrack" in families: - self.log.debug("Explicitly removing 'ftrack'") - instance.data["families"].remove("ftrack") else: self.log.debug("Instance '{}' doesn't match any profile".format( instance.data.get("family"))) + + def _get_add_ftrack_f_from_addit_filters(self, + additional_filters, + families, + add_ftrack_family): + """ + Compares additional filters - working on instance's families. + + Triggered for more detailed filtering when main family matches, + but content of 'families' actually matter. + (For example 'review' in 'families' should result in adding to + Ftrack) + + Args: + additional_filters (dict) - from Setting + families (list) - subfamilies + add_ftrack_family (bool) - add ftrack to families if True + """ + override_filter = None + override_filter_value = -1 + for additional_filter in additional_filters: + filter_families = set(additional_filter["families"]) + valid = filter_families <= families # issubset + if not valid: + continue + + value = len(filter_families) + if value > override_filter_value: + override_filter = additional_filter + override_filter_value = value + + if override_filter: + add_ftrack_family = override_filter["add_ftrack_family"] + + return add_ftrack_family From 18b8666b8166a89f570bbfd5d3c43a07e7327378 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 12:20:57 +0200 Subject: [PATCH 044/180] added workfile to families --- openpype/settings/defaults/project_settings/global.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index c3c43c3b3b..037fa63a29 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -235,7 +235,8 @@ }, { "families": [ - "review" + "review", + "workfile" ], "hosts": [ "tvpaint" From 00998f310d650dfa54bcfcfdd07e2397fdda8778 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 14:33:58 +0200 Subject: [PATCH 045/180] initial commit of subset name template docstrings --- .../settings_project_global.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 5c46cd185a..4bda13f91b 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -172,6 +172,38 @@ Applicable context filters: ## Tools Settings for OpenPype tools. +## Creator +Settings related to [Creator tool](artist_tools.md#details). + +### Subset name profiles +![global_tools_creator_subset_template](assets/global_tools_creator_subset_template.png) +Subset name helps to identify published content. More specific name helps with organization and avoid mixing of content. Subset name is defined using one of templates defined in Subset name profiles settings. The template is filled with information from context in which creation was triggered. + +Templates in settings are filtered by creator's family, host and task name. Template without filters is used as default template. It is recommend to set default template. If default template is not available `"{family}{Task}"` is used. + +**Formatting keys** + +All templates can contain text and formatting keys **family**, **task** and **variant** e.g. `"MyStudio_{family}_{task}"` (example - not recommended in production). + +|Key|Description| +|---|---| +|family|Creators family| +|task|Task under which is creation triggered| +|variant|User input in creator tool| + +**Formatting keys have 3 variants with different letter capitalization.** + +|Task|Key variant|Description|Result| +|---|---|---|---| +|`bgAnim`|`{task}`|Keep original value as is.|`bgAnim`| +|`bgAnim`|`{Task}`|Capitalize first letter of value.|`BgAnim`| +|`bgAnim`|`{TASK}`|Each letter which be capitalized.|`BGANIM`| + +Template may look like `"{family}{Task}{Variant}"`. + +Some creators may have other keys as their context may require more information or more specific values. Make sure you've read documentation of host you're using. + + ## Workfiles All settings related to Workfile tool. From d0cfbb03bdf22974d1665e4ef29f7d7b56d211f7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 14:34:08 +0200 Subject: [PATCH 046/180] added screenshot --- .../global_tools_creator_subset_template.png | Bin 0 -> 17550 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 website/docs/project_settings/assets/global_tools_creator_subset_template.png diff --git a/website/docs/project_settings/assets/global_tools_creator_subset_template.png b/website/docs/project_settings/assets/global_tools_creator_subset_template.png new file mode 100644 index 0000000000000000000000000000000000000000..c4e863c4e0f28982c91e0d4768d113af200e4440 GIT binary patch literal 17550 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 Date: Wed, 16 Jun 2021 15:22:47 +0200 Subject: [PATCH 047/180] added admin tvpaint host documentation with families --- website/docs/admin_hosts_tvpaint.md | 41 +++++++++++++++++++++++++++++ website/sidebars.js | 3 ++- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 website/docs/admin_hosts_tvpaint.md diff --git a/website/docs/admin_hosts_tvpaint.md b/website/docs/admin_hosts_tvpaint.md new file mode 100644 index 0000000000..4db8f89166 --- /dev/null +++ b/website/docs/admin_hosts_tvpaint.md @@ -0,0 +1,41 @@ +--- +id: admin_hosts_tvpaint +title: TVPaint +sidebar_label: TVPaint +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Families +Families that can be published form TVPaint using OpenPype integration. + +### renderLayer +Render layer is represented by TVPaint group and all layers under the group. Output of `renderLayer` family are all visible layers rendered together into png sequence. + +Render layer has additional keys for subset name template. It is possible to use **render_layer** and **render_pass**. + +- Key **render_layer** is alias for variant (user's input). +- For key **render_pass** is used predefined value `"Beauty"` (ATM value can't be changed). + +### renderPass +Render pass is represented by one or more TVPaint layers. Is dependent on created `renderLayer`. All layers must be in same group of `renderLayer`. + +Render pass has additional keys for subset name template. It is possible to use **render_layer** and **render_pass**. +- Key **render_layer** is filled with value of **render_pass** from `renderLayer` group. +- Key **render_pass** is alias for variant (user's input). + +:::note Subset name template +It is recommended to use same subset name template for both **renderLayer** and **renderPass** families. +- Example template: `"{family}{Task}_{Render_layer}_{Render_pass}"` +::: + +### review +Review of whole scene. Exports all visible layers into sequence which is then processed in ExtractReview plugin. It is possible to deactivate publishing of review after collection. + +### workfile +Publish workfile and create it's backup outside of workfiles directory. It is possible to deactivate publishing of workfile after collection. + +:::note Dynamic families +Families **review** and **workfile** are not manually created but are automatically generated during publishing. That's why it is recommended to not use **variant** key in their subset name template. Recommented subset name template is `"{family}{Task}"`. +::: diff --git a/website/sidebars.js b/website/sidebars.js index 59071ec34f..d38973e40f 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -90,7 +90,8 @@ module.exports = { "admin_hosts_maya", "admin_hosts_resolve", "admin_hosts_harmony", - "admin_hosts_aftereffects" + "admin_hosts_aftereffects", + "admin_hosts_tvpaint" ], }, { From 56b858defa287de328d6ba54508845ed30daa7f7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 15:34:36 +0200 Subject: [PATCH 048/180] removed most of not important stuff from admin section --- website/docs/admin_hosts_tvpaint.md | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/website/docs/admin_hosts_tvpaint.md b/website/docs/admin_hosts_tvpaint.md index 4db8f89166..6c9c5ff881 100644 --- a/website/docs/admin_hosts_tvpaint.md +++ b/website/docs/admin_hosts_tvpaint.md @@ -7,35 +7,24 @@ sidebar_label: TVPaint import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -## Families -Families that can be published form TVPaint using OpenPype integration. - -### renderLayer -Render layer is represented by TVPaint group and all layers under the group. Output of `renderLayer` family are all visible layers rendered together into png sequence. +## Subset name templates +Definition of possibile subset name templates in TVPaint integration. +### [Render Layer](artist_hosts_tvpaint#render-layer) Render layer has additional keys for subset name template. It is possible to use **render_layer** and **render_pass**. - Key **render_layer** is alias for variant (user's input). - For key **render_pass** is used predefined value `"Beauty"` (ATM value can't be changed). -### renderPass -Render pass is represented by one or more TVPaint layers. Is dependent on created `renderLayer`. All layers must be in same group of `renderLayer`. - +### [Render pass](artist_hosts_tvpaint#render-pass) Render pass has additional keys for subset name template. It is possible to use **render_layer** and **render_pass**. - Key **render_layer** is filled with value of **render_pass** from `renderLayer` group. - Key **render_pass** is alias for variant (user's input). -:::note Subset name template +:::important Render Layer/Pass templates It is recommended to use same subset name template for both **renderLayer** and **renderPass** families. - Example template: `"{family}{Task}_{Render_layer}_{Render_pass}"` ::: -### review -Review of whole scene. Exports all visible layers into sequence which is then processed in ExtractReview plugin. It is possible to deactivate publishing of review after collection. - -### workfile -Publish workfile and create it's backup outside of workfiles directory. It is possible to deactivate publishing of workfile after collection. - -:::note Dynamic families -Families **review** and **workfile** are not manually created but are automatically generated during publishing. That's why it is recommended to not use **variant** key in their subset name template. Recommented subset name template is `"{family}{Task}"`. -::: +### [Review](artist_hosts_tvpaint#review) and Workfile +Families **review** and **workfile** are not manually created but are automatically generated during publishing. That's why it is recommended to not use **variant** key in their subset name template. From 774595fba8fada980453e0aed3e650ccb99ef043 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 15:43:50 +0200 Subject: [PATCH 049/180] updated review and workfile families in tvpaint docs --- website/docs/artist_hosts_tvpaint.md | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/website/docs/artist_hosts_tvpaint.md b/website/docs/artist_hosts_tvpaint.md index 19cb615158..2e831e64d8 100644 --- a/website/docs/artist_hosts_tvpaint.md +++ b/website/docs/artist_hosts_tvpaint.md @@ -45,7 +45,7 @@ In TVPaint you can find the Tools in OpenPype menu extension. The OpenPype Tools ## Create -In TVPaint you can create and publish **[Reviews](#review)**, **[Render Passes](#render-pass)**, and **[Render Layers](#render-layer)**. +In TVPaint you can create and publish **[Reviews](#review)**, **[Workfile](#workfile)**, **[Render Passes](#render-pass)** and **[Render Layers](#render-layer)**. You have the possibility to organize your layers by using `Color group`. @@ -67,26 +67,13 @@ OpenPype specifically never tries to guess what you want to publish from the sce When you want to publish `review` or `render layer` or `render pass`, open the `Creator` through the Tools menu `Create` button. -### Review +### Review +`Review` renders the whole file as is and sends the resulting QuickTime to Ftrack. +- Is automatically created during publishing. -
-
- -`Review` renders the whole file as is and sends the resulting QuickTime to Ftrack. - -To create reviewable quicktime of your animation: - -- select `Review` in the `Creator` -- press `Create` -- When you run [publish](#publish), file will be rendered and converted to quicktime.` - -
-
- -![createreview](assets/tvp_create_review.png) - -
-
+### Workfile +`Workfile` stores the source workfile as is during publishing (e.g. for backup). +- Is automatically created during publishing. ### Render Layer From 776e784df9c111cec4914e74e315fa62d615d790 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 15:44:07 +0200 Subject: [PATCH 050/180] added link to workfile family --- website/docs/admin_hosts_tvpaint.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_hosts_tvpaint.md b/website/docs/admin_hosts_tvpaint.md index 6c9c5ff881..a99cd19010 100644 --- a/website/docs/admin_hosts_tvpaint.md +++ b/website/docs/admin_hosts_tvpaint.md @@ -26,5 +26,5 @@ It is recommended to use same subset name template for both **renderLayer** and - Example template: `"{family}{Task}_{Render_layer}_{Render_pass}"` ::: -### [Review](artist_hosts_tvpaint#review) and Workfile +### [Review](artist_hosts_tvpaint#review) and [Workfile](artist_hosts_tvpaint#workfile) Families **review** and **workfile** are not manually created but are automatically generated during publishing. That's why it is recommended to not use **variant** key in their subset name template. From a9d51ce003b99b6d8c51e3ef7394d257c71ded40 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 15:44:15 +0200 Subject: [PATCH 051/180] removed unused image --- website/docs/assets/tvp_create_review.png | Bin 30635 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 website/docs/assets/tvp_create_review.png diff --git a/website/docs/assets/tvp_create_review.png b/website/docs/assets/tvp_create_review.png deleted file mode 100644 index d6e9f6342850dda4afbc534b4678dacd8366b264..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30635 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 Date: Wed, 16 Jun 2021 15:51:20 +0200 Subject: [PATCH 052/180] rephrasing subset templates --- website/docs/project_settings/settings_project_global.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 4bda13f91b..e6336c36e2 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -177,9 +177,10 @@ Settings related to [Creator tool](artist_tools.md#details). ### Subset name profiles ![global_tools_creator_subset_template](assets/global_tools_creator_subset_template.png) -Subset name helps to identify published content. More specific name helps with organization and avoid mixing of content. Subset name is defined using one of templates defined in Subset name profiles settings. The template is filled with information from context in which creation was triggered. -Templates in settings are filtered by creator's family, host and task name. Template without filters is used as default template. It is recommend to set default template. If default template is not available `"{family}{Task}"` is used. +Subset name helps to identify published content. More specific name helps with organization and avoid mixing of published content. Subset name is defined using one of templates defined in **Subset name profiles settings**. The template is filled with context information at the time of creation. + +Usage of template is defined by profile filtering using creator's family, host and task name. Profile without filters is used as default template and it is recommend to set default template. If default template is not available `"{family}{Task}"` is used. **Formatting keys** From eb8de6b3fa2b6789aace940017d23ff6c3bcb0dc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 16:45:22 +0200 Subject: [PATCH 053/180] delete group apps without inputs --- openpype/tools/settings/local_settings/apps_widget.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/local_settings/apps_widget.py b/openpype/tools/settings/local_settings/apps_widget.py index f228ecc826..e6a4132955 100644 --- a/openpype/tools/settings/local_settings/apps_widget.py +++ b/openpype/tools/settings/local_settings/apps_widget.py @@ -196,8 +196,12 @@ class LocalApplicationsWidgets(QtWidgets.QWidget): # Create App group specific widget and store it by the key group_widget = AppGroupWidget(entity, self) - self.widgets_by_group_name[key] = group_widget - self.content_layout.addWidget(group_widget) + if group_widget.widgets_by_variant_name: + self.widgets_by_group_name[key] = group_widget + self.content_layout.addWidget(group_widget) + else: + group_widget.setVisible(False) + group_widget.deleteLater() def update_local_settings(self, value): if not value: From 855d5a9ae410fcd67405e90c51fb16b1d2120e80 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Jun 2021 13:46:59 +0200 Subject: [PATCH 054/180] use layer name instead of group name for default render layer variant --- .../plugins/create/create_render_layer.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py index eeb7d32d50..af6c0f0eee 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py @@ -58,18 +58,14 @@ class CreateRenderlayer(plugin.Creator): # Get currently selected layers layers_data = lib.layers_data() - group_ids = set() - for layer in layers_data: - if layer["selected"]: - group_ids.add(layer["group_id"]) - + selected_layers = [ + layer + for layer in layers_data + if layer["selected"] + ] # Return layer name if only one is selected - if len(group_ids) == 1: - group_id = list(group_ids)[0] - groups_data = lib.groups_data() - for group in groups_data: - if group["group_id"] == group_id: - return group["name"] + if len(selected_layers) == 1: + return selected_layers[0]["name"] # Use defaults if cls.defaults: From 4ecc8669717faf0e4113723ea04f645dabe32276 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Jun 2021 15:00:13 +0200 Subject: [PATCH 055/180] ProjectModel has resetless logic of refresh --- openpype/tools/launcher/models.py | 52 +++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index 25b6dcdbf0..846a07e081 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -325,19 +325,59 @@ class ProjectModel(QtGui.QStandardItemModel): self.hide_invisible = False self.project_icon = qtawesome.icon("fa.map", color="white") + self._project_names = set() def refresh(self): - self.clear() - self.beginResetModel() - + project_names = set() for project_doc in self.get_projects(): - item = QtGui.QStandardItem(self.project_icon, project_doc["name"]) - self.appendRow(item) + project_names.add(project_doc["name"]) - self.endResetModel() + origin_project_names = set(self._project_names) + self._project_names = project_names + + project_names_to_remove = origin_project_names - project_names + if project_names_to_remove: + row_counts = {} + continuous = None + for row in range(self.rowCount()): + index = self.index(row, 0) + index_name = index.data(QtCore.Qt.DisplayRole) + if index_name in project_names_to_remove: + if continuous is None: + continuous = row + row_counts[continuous] = 0 + row_counts[continuous] += 1 + else: + continuous = None + + for row in reversed(sorted(row_counts.keys())): + count = row_counts[row] + self.removeRows(row, count) + + continuous = None + row_counts = {} + for idx, project_name in enumerate(sorted(project_names)): + if project_name in origin_project_names: + continuous = None + continue + + if continuous is None: + continuous = idx + row_counts[continuous] = [] + + row_counts[continuous].append(project_name) + + for row in reversed(sorted(row_counts.keys())): + items = [] + for project_name in row_counts[row]: + item = QtGui.QStandardItem(self.project_icon, project_name) + items.append(item) + + self.invisibleRootItem().insertRows(row, items) def get_projects(self): project_docs = [] + for project_doc in sorted( self.dbcon.projects(), key=lambda x: x["name"] ): From 912c6b3b575e4e66e7de35168890917cc2b810a0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Jun 2021 15:08:45 +0200 Subject: [PATCH 056/180] added refresh timer which will update projects each 10 seconds --- openpype/tools/launcher/widgets.py | 22 ++++++++++++++++++++++ openpype/tools/launcher/window.py | 22 ++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 22b08d7d15..59fa2b729e 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -22,6 +22,9 @@ from .constants import ( class ProjectBar(QtWidgets.QWidget): project_changed = QtCore.Signal(int) + # Project list will be refreshed each 10000 msecs + refresh_interval = 10000 + def __init__(self, dbcon, parent=None): super(ProjectBar, self).__init__(parent) @@ -47,14 +50,19 @@ class ProjectBar(QtWidgets.QWidget): QtWidgets.QSizePolicy.Maximum ) + refresh_timer = QtCore.QTimer() + refresh_timer.setInterval(self.refresh_interval) + self.model = model self.project_delegate = project_delegate self.project_combobox = project_combobox + self.refresh_timer = refresh_timer # Initialize self.refresh() # Signals + refresh_timer.timeout.connect(self._on_refresh_timeout) self.project_combobox.currentIndexChanged.connect(self.project_changed) # Set current project by default if it's set. @@ -62,6 +70,20 @@ class ProjectBar(QtWidgets.QWidget): if project_name: self.set_project(project_name) + def showEvent(self, event): + if not self.refresh_timer.isActive(): + self.refresh_timer.start() + super(ProjectBar, self).showEvent(event) + + def _on_refresh_timeout(self): + if not self.isVisible(): + # Stop timer if widget is not visible + self.refresh_timer.stop() + + elif self.isActiveWindow(): + # Refresh projects if window is active + self.model.refresh() + def get_current_project(self): return self.project_combobox.currentText() diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index af749814b7..25aa273ca0 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -91,6 +91,8 @@ class ProjectsPanel(QtWidgets.QWidget): """Projects Page""" project_clicked = QtCore.Signal(str) + # Refresh projects each 10000 msecs + refresh_interval = 10000 def __init__(self, dbcon, parent=None): super(ProjectsPanel, self).__init__(parent=parent) @@ -111,16 +113,36 @@ class ProjectsPanel(QtWidgets.QWidget): layout.addWidget(view) + refresh_timer = QtCore.QTimer() + refresh_timer.setInterval(self.refresh_interval) + + refresh_timer.timeout.connect(self._on_refresh_timeout) view.clicked.connect(self.on_clicked) self.model = model self.view = view + self.refresh_timer = refresh_timer def on_clicked(self, index): if index.isValid(): project_name = index.data(QtCore.Qt.DisplayRole) self.project_clicked.emit(project_name) + def showEvent(self, event): + self.model.refresh() + if not self.refresh_timer.isActive(): + self.refresh_timer.start() + super(ProjectsPanel, self).showEvent(event) + + def _on_refresh_timeout(self): + if not self.isVisible(): + # Stop timer if widget is not visible + self.refresh_timer.stop() + + elif self.isActiveWindow(): + # Refresh projects if window is active + self.model.refresh() + class AssetsPanel(QtWidgets.QWidget): """Assets page""" From d6e345addc67006c2e1d48e30c9a4c0c0b95e378 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Jun 2021 15:09:09 +0200 Subject: [PATCH 057/180] removed unnecessary refresh calls --- openpype/tools/launcher/window.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index 25aa273ca0..5e47cfd154 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -108,7 +108,6 @@ class ProjectsPanel(QtWidgets.QWidget): flick.activateOn(view) model = ProjectModel(self.dbcon) model.hide_invisible = True - model.refresh() view.setModel(model) layout.addWidget(view) @@ -434,7 +433,6 @@ class LauncherWindow(QtWidgets.QDialog): def on_back_clicked(self): self.dbcon.Session["AVALON_PROJECT"] = None self.set_page(0) - self.project_panel.model.refresh() # Refresh projects self.discover_actions() def on_action_clicked(self, action): From 8d9ccaf45bb6a0c5d96455359eb5d43fb317aee8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Jun 2021 15:15:27 +0200 Subject: [PATCH 058/180] simplified refreshing of project bar --- openpype/tools/launcher/widgets.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 59fa2b729e..11301aba64 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -91,27 +91,14 @@ class ProjectBar(QtWidgets.QWidget): index = self.project_combobox.findText(project_name) if index < 0: # Try refresh combobox model - self.project_combobox.blockSignals(True) - self.model.refresh() - self.project_combobox.blockSignals(False) - + self.refresh() index = self.project_combobox.findText(project_name) if index >= 0: self.project_combobox.setCurrentIndex(index) def refresh(self): - prev_project_name = self.get_current_project() - - # Refresh without signals - self.project_combobox.blockSignals(True) - self.model.refresh() - self.set_project(prev_project_name) - - self.project_combobox.blockSignals(False) - - self.project_changed.emit(self.project_combobox.currentIndex()) class ActionBar(QtWidgets.QWidget): From 193296ffaa63732eb43938b7b81e247218d76156 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Jun 2021 15:15:44 +0200 Subject: [PATCH 059/180] dont refresh on init --- openpype/tools/launcher/widgets.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 11301aba64..0e8caeb278 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -58,9 +58,6 @@ class ProjectBar(QtWidgets.QWidget): self.project_combobox = project_combobox self.refresh_timer = refresh_timer - # Initialize - self.refresh() - # Signals refresh_timer.timeout.connect(self._on_refresh_timeout) self.project_combobox.currentIndexChanged.connect(self.project_changed) From 5872a1364caeaa0739b5b9cc421644fd6e66dbf4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Jun 2021 15:24:54 +0200 Subject: [PATCH 060/180] added refresh timer for actions --- openpype/tools/launcher/window.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index 5e47cfd154..a6d34bbe9d 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -297,6 +297,8 @@ class AssetsPanel(QtWidgets.QWidget): class LauncherWindow(QtWidgets.QDialog): """Launcher interface""" + # Refresh actions each 10000msecs + actions_refresh_timeout = 10000 def __init__(self, parent=None): super(LauncherWindow, self).__init__(parent) @@ -365,6 +367,10 @@ class LauncherWindow(QtWidgets.QDialog): layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) + actions_refresh_timer = QtCore.QTimer() + actions_refresh_timer.setInterval(self.actions_refresh_timeout) + + self.actions_refresh_timer = actions_refresh_timer self.message_label = message_label self.project_panel = project_panel self.asset_panel = asset_panel @@ -374,6 +380,7 @@ class LauncherWindow(QtWidgets.QDialog): self._page = 0 # signals + actions_refresh_timer.timeout.connect(self._on_action_timer) actions_bar.action_clicked.connect(self.on_action_clicked) action_history.trigger_history.connect(self.on_history_action) project_panel.project_clicked.connect(self.on_project_clicked) @@ -388,9 +395,11 @@ class LauncherWindow(QtWidgets.QDialog): self.resize(520, 740) def showEvent(self, event): - super().showEvent(event) - # TODO implement refresh/reset which will trigger updating - self.discover_actions() + if not self.actions_refresh_timer.isActive(): + self.actions_refresh_timer.start() + self.discover_actions() + + super(LauncherWindow, self).showEvent(event) def set_page(self, page): current = self.page_slider.currentIndex() @@ -423,6 +432,15 @@ class LauncherWindow(QtWidgets.QDialog): def filter_actions(self): self.actions_bar.filter_actions() + def _on_action_timer(self): + if not self.isVisible(): + # Stop timer if widget is not visible + self.actions_refresh_timer.stop() + + elif self.isActiveWindow(): + # Refresh projects if window is active + self.discover_actions() + def on_project_clicked(self, project_name): self.dbcon.Session["AVALON_PROJECT"] = project_name # Refresh projects From 68e9102f7263e70c50c7f7a959ae48ba5c16280e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 17 Jun 2021 17:04:45 +0200 Subject: [PATCH 061/180] #636 - PS created loader to load reference frame from 'render' sequence Loader cannot do containerization for now as updates would be too difficult. (Customer doesn't mind.) --- .../plugins/load/load_image_from_sequence.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py new file mode 100644 index 0000000000..1409065d43 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -0,0 +1,90 @@ +import os +from avalon import photoshop +from avalon.vendor import qargparse + +from openpype.hosts.photoshop.plugins.load.load_image import ImageLoader + +stub = photoshop.stub() + + +class ImageFromSequenceLoader(ImageLoader): + """ Load specifing image from sequence + + Used only as quick load of reference file from a sequence. + + Plain ImageLoader picks first frame from sequence. + + Loads only existing files - currently not possible to limit loaders + to single select - multiselect. If user selects multiple repres, list + for all of them is provided, but selection is only single file. + This loader will be triggered multiple times, but selected name will + match only to proper path. + + Loader doesnt do containerization as there is currently no data model + of 'frame of rendered files' (only rendered sequence), update would be + difficult. + """ + + families = ["render"] + representations = ["*"] + options = [] + + def load(self, context, name=None, namespace=None, data=None): + if data.get("frame"): + self.fname = os.path.join(os.path.dirname(self.fname), + data["frame"]) + if not os.path.exists(self.fname): + return + + layer_name = self._get_unique_layer_name(context["asset"]["name"], + name) + with photoshop.maintained_selection(): + layer = stub.import_smart_object(self.fname, layer_name) + + self[:] = [layer] + namespace = namespace or layer_name + + return namespace + + @classmethod + def get_options(cls, repre_contexts): + """ + Returns list of files for selected 'repre_contexts'. + + It returns only files with same extension as in context as it is + expected that context points to sequence of frames. + + Returns: + (list) of qargparse.Choice + """ + files = [] + for context in repre_contexts: + fname = ImageLoader.filepath_from_context(context) + _, file_extension = os.path.splitext(fname) + + for file_name in os.listdir(os.path.dirname(fname)): + if not file_name.endswith(file_extension): + continue + files.append(file_name) + + # return selection only if there is something + if not files or len(files) <= 1: + return [] + + return [ + qargparse.Choice( + "frame", + label="Select specific file", + items=files, + default=0, + help="Which frame should be loaded?" + ) + ] + + def update(self, container, representation): + """No update possible, not containerized.""" + pass + + def remove(self, container): + """No update possible, not containerized.""" + pass From 852a2f35aa9352e3d9b3efe8b95d96c5697b7aa1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 17 Jun 2021 18:30:15 +0200 Subject: [PATCH 062/180] #636 - un subclassed loader from sequence, created lib for reusable methods Loader from sequence didnt was dropping connection to PS after first successful import --- openpype/hosts/photoshop/plugins/lib.py | 26 +++++++++++++++ .../photoshop/plugins/load/load_image.py | 33 ++++--------------- .../plugins/load/load_image_from_sequence.py | 15 ++++++--- 3 files changed, 43 insertions(+), 31 deletions(-) create mode 100644 openpype/hosts/photoshop/plugins/lib.py diff --git a/openpype/hosts/photoshop/plugins/lib.py b/openpype/hosts/photoshop/plugins/lib.py new file mode 100644 index 0000000000..74aff06114 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/lib.py @@ -0,0 +1,26 @@ +import re + + +def get_unique_layer_name(layers, asset_name, subset_name): + """ + Gets all layer names and if 'asset_name_subset_name' is present, it + increases suffix by 1 (eg. creates unique layer name - for Loader) + Args: + layers (list) of dict with layers info (name, id etc.) + asset_name (string): + subset_name (string): + + Returns: + (string): name_00X (without version) + """ + name = "{}_{}".format(asset_name, subset_name) + names = {} + for layer in layers: + layer_name = re.sub(r'_\d{3}$', '', layer.name) + if layer_name in names.keys(): + names[layer_name] = names[layer_name] + 1 + else: + names[layer_name] = 1 + occurrences = names.get(name, 0) + + return "{}_{:0>3d}".format(name, occurrences + 1) diff --git a/openpype/hosts/photoshop/plugins/load/load_image.py b/openpype/hosts/photoshop/plugins/load/load_image.py index 44cc96c96f..d043323768 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image.py +++ b/openpype/hosts/photoshop/plugins/load/load_image.py @@ -1,7 +1,9 @@ -from avalon import api, photoshop -import os import re +from avalon import api, photoshop + +from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name + stub = photoshop.stub() @@ -15,8 +17,9 @@ class ImageLoader(api.Loader): representations = ["*"] def load(self, context, name=None, namespace=None, data=None): - layer_name = self._get_unique_layer_name(context["asset"]["name"], - name) + layer_name = get_unique_layer_name(stub.get_layers(), + context["asset"]["name"], + name) with photoshop.maintained_selection(): layer = stub.import_smart_object(self.fname, layer_name) @@ -69,25 +72,3 @@ class ImageLoader(api.Loader): def switch(self, container, representation): self.update(container, representation) - - def _get_unique_layer_name(self, asset_name, subset_name): - """ - Gets all layer names and if 'name' is present in them, increases - suffix by 1 (eg. creates unique layer name - for Loader) - Args: - name (string): in format asset_subset - - Returns: - (string): name_00X (without version) - """ - name = "{}_{}".format(asset_name, subset_name) - names = {} - for layer in stub.get_layers(): - layer_name = re.sub(r'_\d{3}$', '', layer.name) - if layer_name in names.keys(): - names[layer_name] = names[layer_name] + 1 - else: - names[layer_name] = 1 - occurrences = names.get(name, 0) - - return "{}_{:0>3d}".format(name, occurrences + 1) diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py index 1409065d43..09525d2791 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -1,13 +1,15 @@ import os + +from avalon import api from avalon import photoshop from avalon.vendor import qargparse -from openpype.hosts.photoshop.plugins.load.load_image import ImageLoader +from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name stub = photoshop.stub() -class ImageFromSequenceLoader(ImageLoader): +class ImageFromSequenceLoader(api.Loader): """ Load specifing image from sequence Used only as quick load of reference file from a sequence. @@ -36,8 +38,11 @@ class ImageFromSequenceLoader(ImageLoader): if not os.path.exists(self.fname): return - layer_name = self._get_unique_layer_name(context["asset"]["name"], - name) + stub = photoshop.stub() + layer_name = get_unique_layer_name(stub.get_layers(), + context["asset"]["name"], + name) + with photoshop.maintained_selection(): layer = stub.import_smart_object(self.fname, layer_name) @@ -59,7 +64,7 @@ class ImageFromSequenceLoader(ImageLoader): """ files = [] for context in repre_contexts: - fname = ImageLoader.filepath_from_context(context) + fname = ImageFromSequenceLoader.filepath_from_context(context) _, file_extension = os.path.splitext(fname) for file_name in os.listdir(os.path.dirname(fname)): From 8813c7cc062742ee27a75726d08bc07b2c2413ae Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 18 Jun 2021 13:11:39 +0200 Subject: [PATCH 063/180] #680 - added advanced_filtering to schema --- .../schema_project_ftrack.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index af85ccb254..a94ebc8888 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -651,6 +651,29 @@ "key": "add_ftrack_family", "label": "Add Ftrack Family", "type": "boolean" + }, + { + "type": "list", + "collapsible": true, + "key": "advanced_filtering", + "label": "Advanced adding if additional families present", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "families", + "label": "Additional Families", + "type": "list", + "object_type": "text" + }, + { + "key": "add_ftrack_family", + "label": "Add Ftrack Family", + "type": "boolean" + } + ] + } } ] } From 186bd21edc47af44745b6fedecbc143dba39dce6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 18 Jun 2021 16:16:44 +0200 Subject: [PATCH 064/180] #680 - added documentation --- .../assets/ftrack/ftrack-collect-advanced.png | Bin 0 -> 16622 bytes .../assets/ftrack/ftrack-collect-main.png | Bin 0 -> 40169 bytes website/docs/module_ftrack.md | 28 +++++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 website/docs/assets/ftrack/ftrack-collect-advanced.png create mode 100644 website/docs/assets/ftrack/ftrack-collect-main.png diff --git a/website/docs/assets/ftrack/ftrack-collect-advanced.png b/website/docs/assets/ftrack/ftrack-collect-advanced.png new file mode 100644 index 0000000000000000000000000000000000000000..5249685c86d21461589b6556a5453becf0d742ae GIT binary patch literal 16622 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 C%`C70 literal 0 HcmV?d00001 diff --git a/website/docs/assets/ftrack/ftrack-collect-main.png b/website/docs/assets/ftrack/ftrack-collect-main.png new file mode 100644 index 0000000000000000000000000000000000000000..7c75cd6269e8862845f392dea0b98852b4dd4bff GIT binary patch literal 40169 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 Ch7%9~ literal 0 HcmV?d00001 diff --git a/website/docs/module_ftrack.md b/website/docs/module_ftrack.md index bd0dbaef4f..c3b9fd6bc2 100644 --- a/website/docs/module_ftrack.md +++ b/website/docs/module_ftrack.md @@ -196,7 +196,7 @@ Is used to remove value from `Avalon/Mongo Id` Custom Attribute when entity is c ### Sync status from Task to Parent -List of parent boject types where this is triggered ("Shot", "Asset build", etc. Skipped if it is empty) +List of parent object types where this is triggered ("Shot", "Asset build", etc. Skipped if it is empty) ### Sync status from Version to Task @@ -214,3 +214,29 @@ This is usefull for example if first version publish doesn't contain any actual ### Update status on next task Change status on next task by task types order when task status state changed to "Done". All tasks with the same Task mapping of next task status changes From → To. Some status can be ignored. + +## Publish plugins + +### Collect Ftrack Family + +Reviews uploads to Ftrack could be configured by combination of hosts, families and task names. +(Currently implemented only in Standalone Publisher, Maya.) + +#### Profiles + +Profiles are used to select when to add Ftrack family to the instance. One or multiple profiles could be configured, Families, Task names (regex available), Host names combination is needed. + +Eg. If I want review created and uploaded to Ftrack for render published from Maya , setting is: + +Host names: 'Maya' +Families: 'render' +Add Ftrack Family: enabled + +![Collect Ftrack Family](assets/ftrack/ftrack-collect-main.png) + +#### Advanced adding if additional families present + +In special cases adding 'ftrack' based on main family ('Families' set higher) is not enough. +(For example upload to Ftrack for 'plate' main family should only happen if 'review' is contained in instance 'families', not added in other cases. ) + +![Collect Ftrack Family](assets/ftrack/ftrack-collect-advanced.png) \ No newline at end of file From ba5d6a17d23439d98094792075f65e7c29345897 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 18 Jun 2021 16:17:06 +0200 Subject: [PATCH 065/180] Fixed wrong label --- website/docs/admin_hosts_aftereffects.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_hosts_aftereffects.md b/website/docs/admin_hosts_aftereffects.md index dc43820465..e3a09058da 100644 --- a/website/docs/admin_hosts_aftereffects.md +++ b/website/docs/admin_hosts_aftereffects.md @@ -14,7 +14,7 @@ All of them are Project based, eg. each project could have different configurati Location: Settings > Project > AfterEffects -![Harmony Project Settings](assets/admin_hosts_aftereffects_settings.png) +![AfterEffects Project Settings](assets/admin_hosts_aftereffects_settings.png) ## Publish plugins From 235059f9cdcae4fadad3f1c90c10c1636618dc13 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 19 Jun 2021 03:47:04 +0000 Subject: [PATCH 066/180] [Automated] Bump version --- CHANGELOG.md | 54 ++++++++++++++++++++++++++++++++++++++++++++- openpype/version.py | 2 +- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 043295eb8c..d7c0a25c7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,59 @@ # Changelog +## [3.2.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...HEAD) + +**🚀 Enhancements** + +- Autoupdate launcher [\#1725](https://github.com/pypeclub/OpenPype/pull/1725) +- Subset template and TVPaint subset template docs [\#1717](https://github.com/pypeclub/OpenPype/pull/1717) +- Overscan color extract review [\#1701](https://github.com/pypeclub/OpenPype/pull/1701) +- Nuke: Prerender Frame Range by default [\#1699](https://github.com/pypeclub/OpenPype/pull/1699) +- Smoother edges of color triangle [\#1695](https://github.com/pypeclub/OpenPype/pull/1695) + +**🐛 Bug fixes** + +- TVPaint use layer name for default variant [\#1724](https://github.com/pypeclub/OpenPype/pull/1724) +- Default subset template for TVPaint review and workfile families [\#1716](https://github.com/pypeclub/OpenPype/pull/1716) +- Maya: Extract review hotfix [\#1714](https://github.com/pypeclub/OpenPype/pull/1714) +- Settings: Imageio improving granularity [\#1711](https://github.com/pypeclub/OpenPype/pull/1711) + +## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-18) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.2...2.18.3) + +**🐛 Bug fixes** + +- Remove publish highlight icon in AE [\#1664](https://github.com/pypeclub/OpenPype/pull/1664) + +**Merged pull requests:** + +- Sync main 2.x back to 2.x develop [\#1715](https://github.com/pypeclub/OpenPype/pull/1715) +- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) +- \#683 - Validate frame range in Standalone Publisher [\#1680](https://github.com/pypeclub/OpenPype/pull/1680) +- Maya: Split model content validator [\#1654](https://github.com/pypeclub/OpenPype/pull/1654) + +## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.1.0...2.18.2) + +**🚀 Enhancements** + +- StandalonePublisher: adding exception for adding `delete` tag to repre [\#1650](https://github.com/pypeclub/OpenPype/pull/1650) + +**🐛 Bug fixes** + +- Maya: Extract review hotfix - 2.x backport [\#1713](https://github.com/pypeclub/OpenPype/pull/1713) +- StandalonePublisher: instance data attribute `keepSequence` [\#1668](https://github.com/pypeclub/OpenPype/pull/1668) + +**Merged pull requests:** + +- 1698 Nuke: Prerender Frame Range by default [\#1709](https://github.com/pypeclub/OpenPype/pull/1709) + ## [3.1.0](https://github.com/pypeclub/OpenPype/tree/3.1.0) (2021-06-15) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.0.0...3.1.0) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.1.0-nightly.4...3.1.0) **🚀 Enhancements** @@ -33,6 +84,7 @@ - Fix missing dbm python module [\#1652](https://github.com/pypeclub/OpenPype/pull/1652) - Transparent branches in view on Mac [\#1648](https://github.com/pypeclub/OpenPype/pull/1648) - Add asset on task item [\#1646](https://github.com/pypeclub/OpenPype/pull/1646) +- Project manager save and queue [\#1645](https://github.com/pypeclub/OpenPype/pull/1645) - New project anatomy values [\#1644](https://github.com/pypeclub/OpenPype/pull/1644) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index 4312333660..ece0359506 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.1.0" +__version__ = "3.2.0-nightly.1" From 582f228d0a7fd1d0748bf7a7862b55611e069b32 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 21 Jun 2021 10:54:22 +0200 Subject: [PATCH 067/180] #680 - small fixes --- .../ftrack/plugins/publish/collect_ftrack_family.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index 8dbcb0ab2c..b505a429b5 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -34,11 +34,9 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): self.log.warning("No profiles present for adding Ftrack family") return - anatomy_data = instance.context.data["anatomyData"] task_name = instance.data.get("task", avalon.api.Session["AVALON_TASK"]) - host_name = anatomy_data.get("app", - avalon.api.Session["AVALON_APP"]) + host_name = avalon.api.Session["AVALON_APP"] family = instance.data["family"] filtering_criteria = { @@ -46,7 +44,8 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): "families": family, "tasks": task_name } - profile = filter_profiles(self.profiles, filtering_criteria) + profile = filter_profiles(self.profiles, filtering_criteria, + logger=self.log) if profile: families = instance.data.get("families") @@ -93,7 +92,7 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): override_filter_value = -1 for additional_filter in additional_filters: filter_families = set(additional_filter["families"]) - valid = filter_families <= families # issubset + valid = filter_families <= set(families) # issubset if not valid: continue From 10f5c76973ba97929ea01a10c12cb01ce3324930 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 21 Jun 2021 11:07:07 +0200 Subject: [PATCH 068/180] #680 - allow configuration for editorial --- .../plugins/publish/collect_instances.py | 4 +-- .../defaults/project_settings/ftrack.json | 30 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py index eb04217136..d753a3d9bb 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py @@ -16,12 +16,12 @@ class CollectInstances(pyblish.api.InstancePlugin): subsets = { "referenceMain": { "family": "review", - "families": ["clip", "ftrack"], + "families": ["clip"], "extensions": [".mp4"] }, "audioMain": { "family": "audio", - "families": ["clip", "ftrack"], + "families": ["clip"], "extensions": [".wav"], }, "shotMain": { diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index f83b6cef3a..f43900c76f 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -204,22 +204,30 @@ "enabled": true, "profiles": [ { + "hosts": [ + "standalonepublisher" + ], "families": [], "tasks": [], + "add_ftrack_family": true, + "advanced_filtering": [] + }, + { "hosts": [ "standalonepublisher" ], - "add_ftrack_family": true - }, - { - "families": ["matchmove"], + "families": [ + "matchmove", + "shot" + ], "tasks": [], - "hosts": [ - "standalonepublisher" - ], - "add_ftrack_family": false + "add_ftrack_family": false, + "advanced_filtering": [] }, { + "hosts": [ + "maya" + ], "families": [ "model", "setdress", @@ -229,10 +237,8 @@ "camera" ], "tasks": [], - "hosts": [ - "maya" - ], - "add_ftrack_family": true + "add_ftrack_family": true, + "advanced_filtering": [] } ] }, From 9a35e2d4f8629d923c31ade1a83440150220d3e1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 15:51:27 +0200 Subject: [PATCH 069/180] don't skip processing if mapping is not set and use default mapping --- .../event_version_to_task_statuses.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py b/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py index d20e2ff5a8..f215bedcc2 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py +++ b/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py @@ -66,15 +66,7 @@ class VersionToTaskStatus(BaseEvent): )) return - _status_mapping = event_settings["mapping"] - if not _status_mapping: - self.log.debug( - "Project \"{}\" does not have set mapping for {}".format( - project_name, self.__class__.__name__ - ) - ) - return - + _status_mapping = event_settings["mapping"] or {} status_mapping = { key.lower(): value for key, value in _status_mapping.items() From aea7d538095a794e44ce50d6e9074b7d6715e2f4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 21 Jun 2021 17:36:51 +0200 Subject: [PATCH 070/180] #636 - use new api function --- .../hosts/photoshop/plugins/load/load_image_from_sequence.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py index 09525d2791..8704627b12 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -2,8 +2,10 @@ import os from avalon import api from avalon import photoshop +from avalon.pipeline import get_representation_path_from_context from avalon.vendor import qargparse +from openpype.lib import Anatomy from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name stub = photoshop.stub() @@ -64,7 +66,7 @@ class ImageFromSequenceLoader(api.Loader): """ files = [] for context in repre_contexts: - fname = ImageFromSequenceLoader.filepath_from_context(context) + fname = get_representation_path_from_context(context) _, file_extension = os.path.splitext(fname) for file_name in os.listdir(os.path.dirname(fname)): @@ -93,3 +95,4 @@ class ImageFromSequenceLoader(api.Loader): def remove(self, container): """No update possible, not containerized.""" pass + From 0e30fd8e74e002042aa6efb00e032267e6167882 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 18:52:19 +0200 Subject: [PATCH 071/180] defined ProjectHandler which cares about project model refresh and current project --- openpype/tools/launcher/lib.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/openpype/tools/launcher/lib.py b/openpype/tools/launcher/lib.py index b4e6a0c3e9..6b02e684d6 100644 --- a/openpype/tools/launcher/lib.py +++ b/openpype/tools/launcher/lib.py @@ -15,7 +15,7 @@ provides a bridge between the file-based project inventory and configuration. """ import os -from Qt import QtGui +from Qt import QtGui, QtCore from avalon.vendor import qtawesome from openpype.api import resources @@ -23,6 +23,24 @@ ICON_CACHE = {} NOT_FOUND = type("NotFound", (object, ), {}) +class ProjectHandler(QtCore.QObject): + project_changed = QtCore.Signal(str) + + def __init__(self, dbcon, model): + super(ProjectHandler, self).__init__() + self.current_project = dbcon.Session.get("AVALON_PROJECT") + self.model = model + self.dbcon = dbcon + + def set_project(self, project_name): + self.current_project = project_name + self.dbcon.Session["AVALON_PROJECT"] = project_name + self.project_changed.emit(project_name) + + def refresh_model(self): + self.model.refresh() + + def get_action_icon(action): icon_name = action.icon if not icon_name: From e63b94351980750a76cc9b4c4ad51929f2faba95 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 19:07:30 +0200 Subject: [PATCH 072/180] use ProjectHandler in main window --- openpype/tools/launcher/window.py | 34 ++++++++++++------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index a6d34bbe9d..8f8af9880a 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -12,7 +12,7 @@ from avalon.tools import lib as tools_lib from avalon.tools.widgets import AssetWidget from avalon.vendor import qtawesome from .models import ProjectModel -from .lib import get_action_label +from .lib import get_action_label, ProjectHandler from .widgets import ( ProjectBar, ActionBar, @@ -321,8 +321,12 @@ class LauncherWindow(QtWidgets.QDialog): self.windowFlags() | QtCore.Qt.WindowMinimizeButtonHint ) - project_panel = ProjectsPanel(self.dbcon) - asset_panel = AssetsPanel(self.dbcon) + project_model = ProjectModel(self.dbcon) + project_model.hide_invisible = True + project_handler = ProjectHandler(self.dbcon, project_model) + + project_panel = ProjectsPanel(project_handler) + asset_panel = AssetsPanel(project_handler, self.dbcon) page_slider = SlidePageWidget() page_slider.addWidget(project_panel) @@ -371,6 +375,8 @@ class LauncherWindow(QtWidgets.QDialog): actions_refresh_timer.setInterval(self.actions_refresh_timeout) self.actions_refresh_timer = actions_refresh_timer + self.project_handler = project_handler + self.message_label = message_label self.project_panel = project_panel self.asset_panel = asset_panel @@ -383,15 +389,10 @@ class LauncherWindow(QtWidgets.QDialog): actions_refresh_timer.timeout.connect(self._on_action_timer) actions_bar.action_clicked.connect(self.on_action_clicked) action_history.trigger_history.connect(self.on_history_action) - project_panel.project_clicked.connect(self.on_project_clicked) + project_handler.project_changed.connect(self.on_project_change) asset_panel.back_clicked.connect(self.on_back_clicked) asset_panel.session_changed.connect(self.on_session_changed) - # todo: Simplify this callback connection - asset_panel.project_bar.project_changed.connect( - self.on_project_changed - ) - self.resize(520, 740) def showEvent(self, event): @@ -415,13 +416,6 @@ class LauncherWindow(QtWidgets.QDialog): QtCore.QTimer.singleShot(5000, lambda: self.message_label.setText("")) self.log.debug(message) - def on_project_changed(self): - project_name = self.asset_panel.project_bar.get_current_project() - self.dbcon.Session["AVALON_PROJECT"] = project_name - - # Update the Action plug-ins available for the current project - self.discover_actions() - def on_session_changed(self): self.filter_actions() @@ -441,15 +435,13 @@ class LauncherWindow(QtWidgets.QDialog): # Refresh projects if window is active self.discover_actions() - def on_project_clicked(self, project_name): - self.dbcon.Session["AVALON_PROJECT"] = project_name - # Refresh projects - self.asset_panel.set_project(project_name) + def on_project_change(self, project_name): + # Update the Action plug-ins available for the current project self.set_page(1) self.discover_actions() def on_back_clicked(self): - self.dbcon.Session["AVALON_PROJECT"] = None + self.project_handler.set_project(None) self.set_page(0) self.discover_actions() From 480900a1cc0c6095474f9879d2386cc4745ba24b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 19:08:35 +0200 Subject: [PATCH 073/180] use ProjectHandler object instead of passing dbcon changes and simplified project handling in Launcher tool --- openpype/tools/launcher/widgets.py | 40 ++++++++++++------------ openpype/tools/launcher/window.py | 50 +++++++----------------------- 2 files changed, 32 insertions(+), 58 deletions(-) diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 0e8caeb278..048210115c 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -20,25 +20,15 @@ from .constants import ( class ProjectBar(QtWidgets.QWidget): - project_changed = QtCore.Signal(int) - - # Project list will be refreshed each 10000 msecs - refresh_interval = 10000 - - def __init__(self, dbcon, parent=None): + def __init__(self, project_handler, parent=None): super(ProjectBar, self).__init__(parent) - self.dbcon = dbcon - - model = ProjectModel(dbcon) - model.hide_invisible = True - project_combobox = QtWidgets.QComboBox(self) # Change delegate so stylysheets are applied project_delegate = QtWidgets.QStyledItemDelegate(project_combobox) project_combobox.setItemDelegate(project_delegate) - project_combobox.setModel(model) + project_combobox.setModel(project_handler.model) project_combobox.setRootModelIndex(QtCore.QModelIndex()) layout = QtWidgets.QHBoxLayout(self) @@ -51,19 +41,20 @@ class ProjectBar(QtWidgets.QWidget): ) refresh_timer = QtCore.QTimer() - refresh_timer.setInterval(self.refresh_interval) + refresh_timer.setInterval(project_handler.refresh_interval) - self.model = model + self.project_handler = project_handler self.project_delegate = project_delegate self.project_combobox = project_combobox self.refresh_timer = refresh_timer # Signals refresh_timer.timeout.connect(self._on_refresh_timeout) - self.project_combobox.currentIndexChanged.connect(self.project_changed) + self.project_combobox.currentIndexChanged.connect(self.on_index_change) + project_handler.project_changed.connect(self._on_project_change) # Set current project by default if it's set. - project_name = self.dbcon.Session.get("AVALON_PROJECT") + project_name = project_handler.current_project if project_name: self.set_project(project_name) @@ -79,7 +70,12 @@ class ProjectBar(QtWidgets.QWidget): elif self.isActiveWindow(): # Refresh projects if window is active - self.model.refresh() + self.project_handler.refresh_model() + + def _on_project_change(self, project_name): + if self.get_current_project() == project_name: + return + self.set_project(project_name) def get_current_project(self): return self.project_combobox.currentText() @@ -88,14 +84,18 @@ class ProjectBar(QtWidgets.QWidget): index = self.project_combobox.findText(project_name) if index < 0: # Try refresh combobox model - self.refresh() + self.project_handler.refresh_model() index = self.project_combobox.findText(project_name) if index >= 0: self.project_combobox.setCurrentIndex(index) - def refresh(self): - self.model.refresh() + def on_index_change(self, idx): + if not self.isVisible(): + return + + project_name = self.get_current_project() + self.project_handler.set_project(project_name) class ActionBar(QtWidgets.QWidget): diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index 8f8af9880a..d9f9f5a8f3 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -89,46 +89,37 @@ class ProjectIconView(QtWidgets.QListView): class ProjectsPanel(QtWidgets.QWidget): """Projects Page""" - - project_clicked = QtCore.Signal(str) - # Refresh projects each 10000 msecs - refresh_interval = 10000 - - def __init__(self, dbcon, parent=None): + def __init__(self, project_handler, parent=None): super(ProjectsPanel, self).__init__(parent=parent) layout = QtWidgets.QVBoxLayout(self) - self.dbcon = dbcon - self.dbcon.install() - view = ProjectIconView(parent=self) view.setSelectionMode(QtWidgets.QListView.NoSelection) flick = FlickCharm(parent=self) flick.activateOn(view) - model = ProjectModel(self.dbcon) - model.hide_invisible = True - view.setModel(model) + + view.setModel(project_handler.model) layout.addWidget(view) refresh_timer = QtCore.QTimer() - refresh_timer.setInterval(self.refresh_interval) + refresh_timer.setInterval(project_handler.refresh_interval) refresh_timer.timeout.connect(self._on_refresh_timeout) view.clicked.connect(self.on_clicked) - self.model = model self.view = view self.refresh_timer = refresh_timer + self.project_handler = project_handler def on_clicked(self, index): if index.isValid(): project_name = index.data(QtCore.Qt.DisplayRole) - self.project_clicked.emit(project_name) + self.project_handler.set_project(project_name) def showEvent(self, event): - self.model.refresh() + self.project_handler.refresh_model() if not self.refresh_timer.isActive(): self.refresh_timer.start() super(ProjectsPanel, self).showEvent(event) @@ -140,7 +131,7 @@ class ProjectsPanel(QtWidgets.QWidget): elif self.isActiveWindow(): # Refresh projects if window is active - self.model.refresh() + self.project_handler.refresh_model() class AssetsPanel(QtWidgets.QWidget): @@ -148,7 +139,7 @@ class AssetsPanel(QtWidgets.QWidget): back_clicked = QtCore.Signal() session_changed = QtCore.Signal() - def __init__(self, dbcon, parent=None): + def __init__(self, project_handler, dbcon, parent=None): super(AssetsPanel, self).__init__(parent=parent) self.dbcon = dbcon @@ -163,7 +154,7 @@ class AssetsPanel(QtWidgets.QWidget): btn_back = QtWidgets.QPushButton(project_bar_widget) btn_back.setIcon(btn_back_icon) - project_bar = ProjectBar(self.dbcon, project_bar_widget) + project_bar = ProjectBar(project_handler, project_bar_widget) layout.addWidget(btn_back) layout.addWidget(project_bar) @@ -206,24 +197,19 @@ class AssetsPanel(QtWidgets.QWidget): layout.addWidget(body) # signals - project_bar.project_changed.connect(self.on_project_changed) + project_handler.project_changed.connect(self.on_project_changed) assets_widget.selection_changed.connect(self.on_asset_changed) assets_widget.refreshed.connect(self.on_asset_changed) tasks_widget.task_changed.connect(self.on_task_change) btn_back.clicked.connect(self.back_clicked) + self.project_handler = project_handler self.project_bar = project_bar self.assets_widget = assets_widget self.tasks_widget = tasks_widget self._btn_back = btn_back - # Force initial refresh for the assets since we might not be - # trigging a Project switch if we click the project that was set - # prior to launching the Launcher - # todo: remove this behavior when AVALON_PROJECT is not required - assets_widget.refresh() - def showEvent(self, event): super(AssetsPanel, self).showEvent(event) @@ -232,19 +218,7 @@ class AssetsPanel(QtWidgets.QWidget): btn_size = self.project_bar.height() self._btn_back.setFixedSize(QtCore.QSize(btn_size, btn_size)) - def set_project(self, project): - before = self.project_bar.get_current_project() - if before == project: - self.assets_widget.refresh() - return - - self.project_bar.set_project(project) - self.on_project_changed() - def on_project_changed(self): - project_name = self.project_bar.get_current_project() - self.dbcon.Session["AVALON_PROJECT"] = project_name - self.session_changed.emit() self.assets_widget.refresh() From 072718542fd73365afae1d51f0f22a8689031621 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 19:08:42 +0200 Subject: [PATCH 074/180] don't print asset change --- openpype/tools/launcher/window.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index d9f9f5a8f3..979aab42cf 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -227,11 +227,8 @@ class AssetsPanel(QtWidgets.QWidget): """Callback on asset selection changed This updates the task view. - """ - print("Asset changed..") - asset_name = None asset_silo = None From 23e2fd54d8f140fa2c8555431ae79bac012f7830 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 19:08:53 +0200 Subject: [PATCH 075/180] added few comments and docstrings --- openpype/tools/launcher/lib.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/openpype/tools/launcher/lib.py b/openpype/tools/launcher/lib.py index 6b02e684d6..65d40cd0df 100644 --- a/openpype/tools/launcher/lib.py +++ b/openpype/tools/launcher/lib.py @@ -24,17 +24,44 @@ NOT_FOUND = type("NotFound", (object, ), {}) class ProjectHandler(QtCore.QObject): + """Handler of project model and current project in Launcher tool. + + Helps to organize two separate widgets handling current project selection. + + It is easier to trigger project change callbacks from one place than from + multiple differect places without proper handling or sequence changes. + + Args: + dbcon(AvalonMongoDB): Mongo connection with Session. + model(ProjectModel): Object of projects model which is shared across + all widgets using projects. Arg dbcon should be used as source for + the model. + """ + # Project list will be refreshed each 10000 msecs + # - this is not part of helper implementation but should be used by widgets + # that may require reshing of projects + refresh_interval = 10000 + + # Signal emmited when project has changed project_changed = QtCore.Signal(str) def __init__(self, dbcon, model): super(ProjectHandler, self).__init__() - self.current_project = dbcon.Session.get("AVALON_PROJECT") + # Store project model for usage self.model = model + # Store dbcon self.dbcon = dbcon + self.current_project = dbcon.Session.get("AVALON_PROJECT") + def set_project(self, project_name): + # Change current project of this handler self.current_project = project_name + # Change session project to take effect for other widgets using the + # dbcon object. self.dbcon.Session["AVALON_PROJECT"] = project_name + + # Trigger change signal when everything is updated to new project self.project_changed.emit(project_name) def refresh_model(self): From 46f284ed15a4bdaf643f7bb9525f340b2117b0a9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 19:37:57 +0200 Subject: [PATCH 076/180] Show a missing attribute as `<` are `>` used as html tags --- openpype/modules/ftrack/lib/avalon_sync.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py index 5d1da005dc..85c8d5a273 100644 --- a/openpype/modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/ftrack/lib/avalon_sync.py @@ -402,9 +402,9 @@ class SyncEntitiesFactory: items = [] items.append({ "type": "label", - "value": "# Can't access Custom attribute <{}>".format( - CUST_ATTR_ID_KEY - ) + "value": ( + "# Can't access Custom attribute: \"{}\"" + ).format(CUST_ATTR_ID_KEY) }) items.append({ "type": "label", From ca26f56c2662cb70d1b611eb963c52594e6d317e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 19:38:02 +0200 Subject: [PATCH 077/180] better message --- openpype/modules/ftrack/lib/avalon_sync.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py index 85c8d5a273..2458308af5 100644 --- a/openpype/modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/ftrack/lib/avalon_sync.py @@ -409,9 +409,11 @@ class SyncEntitiesFactory: items.append({ "type": "label", "value": ( - "

- Check if user \"{}\" has permissions" - " to access the Custom attribute

" - ).format(self._api_key) + "

- Check if your User and API key has permissions" + " to access the Custom attribute." + "
Username:\"{}\"" + "
API key:\"{}\"

" + ).format(self._api_user, self._api_key) }) items.append({ "type": "label", From bf284caccf998352222722af616287e4fd411c16 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 20:03:31 +0200 Subject: [PATCH 078/180] make sure bg compositing is happening only if alpha is available --- .../plugins/publish/extract_sequence.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 007b5c41f1..8588a5b07c 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -293,10 +293,15 @@ class ExtractSequence(pyblish.api.Extractor): thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") if first_frame_filepath and os.path.exists(first_frame_filepath): + # Composite background only on rgba images + # - just making sure source_img = Image.open(first_frame_filepath) - thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) - thumbnail_obj.paste(source_img) - thumbnail_obj.save(thumbnail_filepath) + if source_img.mode.lower() == "rgba": + bg_image = Image.new("RGBA", source_img.size, (255, 255, 255)) + thumbnail_obj = Image.alpha_composite(bg_image, source_img) + thumbnail_obj.convert("RGB").save(thumbnail_filepath) + else: + source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath @@ -392,9 +397,14 @@ class ExtractSequence(pyblish.api.Extractor): if thumbnail_src_filepath and os.path.exists(thumbnail_src_filepath): source_img = Image.open(thumbnail_src_filepath) thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") - thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) - thumbnail_obj.paste(source_img) - thumbnail_obj.save(thumbnail_filepath) + # Composite background only on rgba images + # - just making sure + if source_img.mode.lower() == "rgba": + bg_image = Image.new("RGBA", source_img.size, (255, 255, 255)) + thumbnail_obj = Image.alpha_composite(bg_image, source_img) + thumbnail_obj.convert("RGB").save(thumbnail_filepath) + else: + source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath From b9861d48dee01e8cd31d28a08a1840c0664be12d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 10:40:02 +0200 Subject: [PATCH 079/180] added settings for ValidateContainers into each host --- .../projects_schema/schema_project_harmony.json | 10 ++++++++++ .../projects_schema/schema_project_photoshop.json | 12 +++++++++++- .../projects_schema/schemas/schema_maya_publish.json | 11 ++++++++++- .../projects_schema/schemas/schema_nuke_publish.json | 10 ++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json index 8b5d638cd8..ce6246a8de 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json @@ -25,6 +25,16 @@ } ] }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateContainers", + "label": "ValidateContainers" + } + ] + }, { "type": "dict", "collapsible": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index 4eb6c26dbb..3b65f08ac4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -33,6 +33,16 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateContainers", + "label": "ValidateContainers" + } + ] + }, { "type": "dict", "collapsible": true, @@ -50,7 +60,7 @@ "object_type": "text" } ] - } + } ] }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 0abcdd2965..5ca7059ee5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -28,7 +28,16 @@ "type": "label", "label": "Validators" }, - + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateContainers", + "label": "ValidateContainers" + } + ] + }, { "type": "dict", "collapsible": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 6873ed5190..782179cfd1 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -29,6 +29,16 @@ "type": "label", "label": "Validators" }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateContainers", + "label": "ValidateContainers" + } + ] + }, { "type": "dict", "collapsible": true, From cc79caa90fa63efa6d96e6858bc5c4705d1e14f3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 10:40:13 +0200 Subject: [PATCH 080/180] added settings for houdini --- .../schemas/projects_schema/schema_main.json | 4 +++ .../schema_project_houdini.json | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json index bee9712878..4a8a9d496e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -82,6 +82,10 @@ "type": "schema", "name": "schema_project_hiero" }, + { + "type": "schema", + "name": "schema_project_houdini" + }, { "type": "schema", "name": "schema_project_blender" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json new file mode 100644 index 0000000000..c6de257a61 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -0,0 +1,27 @@ +{ + "type": "dict", + "collapsible": true, + "key": "houdini", + "label": "Houdini", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "publish", + "label": "Publish plugins", + "children": [ + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateContainers", + "label": "ValidateContainers" + } + ] + } + ] + } + ] +} From 70176a3de10778697993f3e82b019888b5ecdf14 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 10:40:37 +0200 Subject: [PATCH 081/180] saved defaults for ValidateContainers plugin --- openpype/settings/defaults/project_settings/harmony.json | 5 +++++ openpype/settings/defaults/project_settings/houdini.json | 9 +++++++++ openpype/settings/defaults/project_settings/maya.json | 5 +++++ openpype/settings/defaults/project_settings/nuke.json | 5 +++++ .../settings/defaults/project_settings/photoshop.json | 5 +++++ 5 files changed, 29 insertions(+) create mode 100644 openpype/settings/defaults/project_settings/houdini.json diff --git a/openpype/settings/defaults/project_settings/harmony.json b/openpype/settings/defaults/project_settings/harmony.json index 0c7a35c058..3cc175ae72 100644 --- a/openpype/settings/defaults/project_settings/harmony.json +++ b/openpype/settings/defaults/project_settings/harmony.json @@ -5,6 +5,11 @@ ".*" ] }, + "ValidateContainers": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateSceneSettings": { "enabled": true, "optional": true, diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json new file mode 100644 index 0000000000..811a446e59 --- /dev/null +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -0,0 +1,9 @@ +{ + "publish": { + "ValidateContainers": { + "enabled": true, + "optional": true, + "active": true + } + } +} \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index ba685ae502..284a1a0040 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -127,6 +127,11 @@ "CollectMayaRender": { "sync_workfile_version": false }, + "ValidateContainers": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateShaderName": { "enabled": false, "regex": "(?P.*)_(.*)_SHD" diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 6ff732634e..71bf46d5b3 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -21,6 +21,11 @@ "PreCollectNukeInstances": { "sync_workfile_version": true }, + "ValidateContainers": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateKnobs": { "enabled": false, "knobs": { diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index b306a757a6..4c36e4bd49 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -7,6 +7,11 @@ } }, "publish": { + "ValidateContainers": { + "enabled": true, + "optional": true, + "active": true + }, "ExtractImage": { "formats": [ "png", From 79918fdf17dec2b6952ea6daa6a96f053fb98242 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 11:34:59 +0200 Subject: [PATCH 082/180] show error dialog if loading or updating of local settings crash --- .../tools/settings/local_settings/window.py | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/local_settings/window.py b/openpype/tools/settings/local_settings/window.py index 69562d0b1f..f1020a0764 100644 --- a/openpype/tools/settings/local_settings/window.py +++ b/openpype/tools/settings/local_settings/window.py @@ -203,8 +203,40 @@ class LocalSettingsWindow(QtWidgets.QWidget): def reset(self): if self._reset_on_show: self._reset_on_show = False - value = get_local_settings() - self.settings_widget.update_local_settings(value) + + error_msg = None + try: + value = get_local_settings() + self.settings_widget.update_local_settings(value) + + except Exception as exc: + error_msg = str(exc) + + crashed = error_msg is not None + # Enable/Disable save button if crashed or not + self.save_btn.setEnabled(not crashed) + # Show/Hide settings widget if crashed or not + if self.settings_widget: + self.settings_widget.setVisible(not crashed) + + if not crashed: + return + + # Show message with error + title = "Something went wrong" + msg = ( + "This is probably a bug. Loading of settings failed." + "\n\nError message:\n{}" + ).format(error_msg) + + dialog = QtWidgets.QMessageBox( + QtWidgets.QMessageBox.Warning, + title, + msg, + QtWidgets.QMessageBox.Ok, + self + ) + dialog.exec_() def _on_reset_clicked(self): self.reset() From 4c58e2aadca6a5a29b217a88e2e56d759120964b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 11:35:28 +0200 Subject: [PATCH 083/180] create LocalSettingsWidget on reset --- openpype/tools/settings/local_settings/window.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/tools/settings/local_settings/window.py b/openpype/tools/settings/local_settings/window.py index f1020a0764..806b8840bd 100644 --- a/openpype/tools/settings/local_settings/window.py +++ b/openpype/tools/settings/local_settings/window.py @@ -168,9 +168,6 @@ class LocalSettingsWindow(QtWidgets.QWidget): scroll_widget = QtWidgets.QScrollArea(self) scroll_widget.setObjectName("GroupWidget") - settings_widget = LocalSettingsWidget(scroll_widget) - - scroll_widget.setWidget(settings_widget) scroll_widget.setWidgetResizable(True) footer = QtWidgets.QWidget(self) @@ -191,7 +188,8 @@ class LocalSettingsWindow(QtWidgets.QWidget): save_btn.clicked.connect(self._on_save_clicked) reset_btn.clicked.connect(self._on_reset_clicked) - self.settings_widget = settings_widget + self.settings_widget = None + self.scroll_widget = scroll_widget self.reset_btn = reset_btn self.save_btn = save_btn @@ -206,6 +204,10 @@ class LocalSettingsWindow(QtWidgets.QWidget): error_msg = None try: + if self.settings_widget is None: + self.settings_widget = LocalSettingsWidget(self.scroll_widget) + self.scroll_widget.setWidget(self.settings_widget) + value = get_local_settings() self.settings_widget.update_local_settings(value) From c0663c4e51a6657e69c907a76e549c955b63701d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 11:41:20 +0200 Subject: [PATCH 084/180] modified message and change icon --- openpype/tools/settings/local_settings/window.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/local_settings/window.py b/openpype/tools/settings/local_settings/window.py index 806b8840bd..e0fe8092f2 100644 --- a/openpype/tools/settings/local_settings/window.py +++ b/openpype/tools/settings/local_settings/window.py @@ -227,12 +227,13 @@ class LocalSettingsWindow(QtWidgets.QWidget): # Show message with error title = "Something went wrong" msg = ( - "This is probably a bug. Loading of settings failed." + "Bug: Loading of settings failed." + " Please contact your project manager or OpenPype team." "\n\nError message:\n{}" ).format(error_msg) dialog = QtWidgets.QMessageBox( - QtWidgets.QMessageBox.Warning, + QtWidgets.QMessageBox.Critical, title, msg, QtWidgets.QMessageBox.Ok, From d5c94ab20ca8f28d387aa5fefccde2474300dc68 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 11:41:31 +0200 Subject: [PATCH 085/180] changed attribute names --- .../tools/settings/local_settings/window.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/openpype/tools/settings/local_settings/window.py b/openpype/tools/settings/local_settings/window.py index e0fe8092f2..9e8fd89b23 100644 --- a/openpype/tools/settings/local_settings/window.py +++ b/openpype/tools/settings/local_settings/window.py @@ -188,8 +188,12 @@ class LocalSettingsWindow(QtWidgets.QWidget): save_btn.clicked.connect(self._on_save_clicked) reset_btn.clicked.connect(self._on_reset_clicked) - self.settings_widget = None - self.scroll_widget = scroll_widget + # Do not create local settings widget in init phase as it's using + # settings objects that must be OK to be able create this widget + # - we want to show dialog if anything goes wrong + # - without reseting nothing is shown + self._settings_widget = None + self._scroll_widget = scroll_widget self.reset_btn = reset_btn self.save_btn = save_btn @@ -204,12 +208,15 @@ class LocalSettingsWindow(QtWidgets.QWidget): error_msg = None try: - if self.settings_widget is None: - self.settings_widget = LocalSettingsWidget(self.scroll_widget) - self.scroll_widget.setWidget(self.settings_widget) + # Create settings widget if is not created yet + if self._settings_widget is None: + self._settings_widget = LocalSettingsWidget( + self._scroll_widget + ) + self._scroll_widget.setWidget(self._settings_widget) value = get_local_settings() - self.settings_widget.update_local_settings(value) + self._settings_widget.update_local_settings(value) except Exception as exc: error_msg = str(exc) @@ -218,8 +225,8 @@ class LocalSettingsWindow(QtWidgets.QWidget): # Enable/Disable save button if crashed or not self.save_btn.setEnabled(not crashed) # Show/Hide settings widget if crashed or not - if self.settings_widget: - self.settings_widget.setVisible(not crashed) + if self._settings_widget: + self._settings_widget.setVisible(not crashed) if not crashed: return @@ -245,6 +252,6 @@ class LocalSettingsWindow(QtWidgets.QWidget): self.reset() def _on_save_clicked(self): - value = self.settings_widget.settings_value() + value = self._settings_widget.settings_value() save_local_settings(value) self.reset() From ba13064a654c42cf0157ba67a5d31d1312aaa5e3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 11:42:05 +0200 Subject: [PATCH 086/180] local settings action won't trigger reset on first trigger --- openpype/modules/settings_action.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/modules/settings_action.py b/openpype/modules/settings_action.py index 1035dc0dcd..9db4a252bc 100644 --- a/openpype/modules/settings_action.py +++ b/openpype/modules/settings_action.py @@ -114,6 +114,7 @@ class LocalSettingsAction(PypeModule, ITrayAction): # Tray attributes self.settings_window = None + self._first_trigger = True def connect_with_modules(self, *_a, **_kw): return @@ -153,6 +154,9 @@ class LocalSettingsAction(PypeModule, ITrayAction): self.settings_window.raise_() self.settings_window.activateWindow() - # Reset content if was not visible - if not was_visible: + # Do not reset if it's first trigger of action + if self._first_trigger: + self._first_trigger = False + elif not was_visible: + # Reset content if was not visible self.settings_window.reset() From 4b707d4cb2b4781bb14325b9051a24496d29a5cb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 12:14:26 +0200 Subject: [PATCH 087/180] Settings: ftrack family for editorial standalonepublishing --- .../defaults/project_settings/ftrack.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index f43900c76f..03ecf024a6 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -224,6 +224,26 @@ "add_ftrack_family": false, "advanced_filtering": [] }, + { + "hosts": [ + "standalonepublisher" + ], + "families": [ + "review", + "plate" + ], + "tasks": [], + "add_ftrack_family": false, + "advanced_filtering": [ + { + "families": [ + "clip", + "review" + ], + "add_ftrack_family": true + } + ] + }, { "hosts": [ "maya" From c807bef0cb1c028c15d67d08c2576faf406d8171 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 12:43:07 +0200 Subject: [PATCH 088/180] added `hosts-enum` entity for settings --- openpype/settings/entities/__init__.py | 2 + openpype/settings/entities/enum_entity.py | 52 +++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index f64ca1e98d..94eb819f2b 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -101,6 +101,7 @@ from .color_entity import ColorEntity from .enum_entity import ( BaseEnumEntity, EnumEntity, + HostsEnumEntity, AppsEnumEntity, ToolsEnumEntity, TaskTypeEnumEntity, @@ -153,6 +154,7 @@ __all__ = ( "BaseEnumEntity", "EnumEntity", + "HostsEnumEntity", "AppsEnumEntity", "ToolsEnumEntity", "TaskTypeEnumEntity", diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 0b0575a255..add5c0298c 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -101,6 +101,58 @@ class EnumEntity(BaseEnumEntity): super(EnumEntity, self).schema_validations() +class HostsEnumEntity(BaseEnumEntity): + schema_types = ["hosts-enum"] + + def _item_initalization(self): + self.multiselection = self.schema_data.get("multiselection", True) + self.use_empty_value = self.schema_data.get( + "use_empty_value", not self.multiselection + ) + self.empty_label = ( + self.schema_data.get("empty_label") or "< without host >" + ) + + self.enum_items = [ + {"aftereffects": "aftereffects"}, + {"blender": "blender"}, + {"celaction": "celaction"}, + {"fusion": "fusion"}, + {"harmony": "harmony"}, + {"hiero": "hiero"}, + {"houdini": "houdini"}, + {"maya": "maya"}, + {"nuke": "nuke"}, + {"photoshop": "photoshop"}, + {"resolve": "resolve"}, + {"tvpaint": "tvpaint"}, + {"unreal": "unreal"} + ] + + if self.use_empty_value: + self.enum_items.insert(0, {"": self.empty_label}) + + valid_keys = set() + for item in self.enum_items or []: + valid_keys.add(tuple(item.keys())[0]) + + self.valid_keys = valid_keys + + if self.multiselection: + self.valid_value_types = (list, ) + self.value_on_not_set = [] + else: + for key in valid_keys: + if self.value_on_not_set is NOT_SET: + self.value_on_not_set = key + break + + self.valid_value_types = (STRING_TYPE, ) + + # GUI attribute + self.placeholder = self.schema_data.get("placeholder") + + class AppsEnumEntity(BaseEnumEntity): schema_types = ["apps-enum"] From c1bb5fec8f6ecf3689408653210fc081039d93d9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 12:43:56 +0200 Subject: [PATCH 089/180] use host-enum instead of list of strings for hosts --- .../projects_schema/schema_project_slack.json | 4 ++-- .../schemas/schema_global_publish.json | 12 ++++++------ .../projects_schema/schemas/schema_global_tools.json | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json index 58708776ca..170de7c8a2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json @@ -59,10 +59,10 @@ "object_type": "text" }, { + "type": "hosts-enum", "key": "hosts", "label": "Host names", - "type": "list", - "object_type": "text" + "multiselection": true }, { "type": "separator" 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 8ca203e3bc..496635287f 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 @@ -90,10 +90,10 @@ "object_type": "text" }, { + "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "type": "list", - "object_type": "text" + "multiselection": true }, { "type": "splitter" @@ -358,10 +358,10 @@ "object_type": "text" }, { + "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "type": "list", - "object_type": "text" + "multiselection": true }, { "type": "splitter" @@ -492,10 +492,10 @@ "object_type": "text" }, { + "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "type": "list", - "object_type": "text" + "multiselection": true }, { "key": "tasks", 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 224389d42e..8c92a45a56 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 @@ -35,10 +35,10 @@ "object_type": "text" }, { + "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "type": "list", - "object_type": "text" + "multiselection": true }, { "key": "tasks", @@ -75,10 +75,10 @@ "type": "dict", "children": [ { + "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "type": "list", - "object_type": "text" + "multiselection": true }, { "key": "tasks", From 9d84a39e11653d6f88f774d94e0963816ea51943 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 12:44:18 +0200 Subject: [PATCH 090/180] replaced hosts enum in applications with hosts-enum --- .../template_host_unchangables.json | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/template_host_unchangables.json b/openpype/settings/entities/schemas/system_schema/host_settings/template_host_unchangables.json index e8b2a70076..c4d8d89209 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/template_host_unchangables.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/template_host_unchangables.json @@ -14,25 +14,11 @@ "roles": ["developer"] }, { - "type": "enum", + "type": "hosts-enum", "key": "host_name", "label": "Host implementation", - "enum_items": [ - { "": "< without host >" }, - { "aftereffects": "aftereffects" }, - { "blender": "blender" }, - { "celaction": "celaction" }, - { "fusion": "fusion" }, - { "harmony": "harmony" }, - { "hiero": "hiero" }, - { "houdini": "houdini" }, - { "maya": "maya" }, - { "nuke": "nuke" }, - { "photoshop": "photoshop" }, - { "resolve": "resolve" }, - { "tvpaint": "tvpaint" }, - { "unreal": "unreal" } - ], + "multiselection": false, + "use_empty_value": true, "roles": ["developer"] } ] From fbcb46ac0922cc03c69262d709c765ba319d5803 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 12:51:44 +0200 Subject: [PATCH 091/180] adde hosts-enum to readme --- openpype/settings/entities/schemas/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 6c31b61f59..0ad13bfe1a 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -272,6 +272,22 @@ } ``` +### hosts-enum +- enumeration of available hosts +- multiselection can be allowed with setting key `"multiselection"` to `True` (Default: `False`) +- it is possible to add empty value (represented with empty string) with setting `"use_empty_value"` to `True` (Default: `False`) +- to modify label of empty value set `"empty_label"` key with your label (Default: `< without host >`) +``` +{ + "key": "host", + "label": "Host name", + "type": "hosts-enum", + "multiselection": false, + "use_empty_value": true, + "empty_label": "N/A" +} +``` + ## Inputs for setting value using Pure inputs - these inputs also have required `"key"` - attribute `"label"` is required in few conditions From d7ab6bb7ae8e2c33adf247978f884552c808e10d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 12:57:19 +0200 Subject: [PATCH 092/180] added few comments --- openpype/settings/entities/enum_entity.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index add5c0298c..050f0038f7 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -102,6 +102,21 @@ class EnumEntity(BaseEnumEntity): class HostsEnumEntity(BaseEnumEntity): + """Enumeration of host names. + + Enum items are hardcoded in definition of the entity. + + Hosts enum can have defined empty value as valid option which is + represented by empty string. Schema key to set this option is + `use_empty_value` (true/false). And to set label of empty value set + `empty_label` (string). + + Enum can have single and multiselection. + + NOTE: + Host name is not the same as application name. Host name defines + implementation instead of application name. + """ schema_types = ["hosts-enum"] def _item_initalization(self): @@ -113,6 +128,7 @@ class HostsEnumEntity(BaseEnumEntity): self.schema_data.get("empty_label") or "< without host >" ) + # These are hardcoded there is not list of available host in OpenPype self.enum_items = [ {"aftereffects": "aftereffects"}, {"blender": "blender"}, From ca36bb2fb7bd4e554ab8014bf20ba6c64573d09f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 13:05:19 +0200 Subject: [PATCH 093/180] Added settings for ExtractSequence to be able modify color of thumbnail background --- .../defaults/project_settings/tvpaint.json | 8 ++++++++ .../projects_schema/schema_project_tvpaint.json | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index b4f3b315ec..25b7056ce1 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -1,5 +1,13 @@ { "publish": { + "ExtractSequence": { + "thumbnail_bg": [ + 255, + 255, + 255, + 255 + ] + }, "ValidateProjectSettings": { "enabled": true, "optional": true, 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 6f90bb4263..1894384bb9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -11,6 +11,21 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "key": "ExtractSequence", + "label": "ExtractSequence", + "is_group": true, + "children": [ + { + "type": "color", + "key": "thumbnail_bg", + "label": "Thumbnail BG color", + "use_alpha": false + } + ] + }, { "type": "schema_template", "name": "template_publish_plugin", From 4628b7627a492642f7cb086b46be683260bbd51d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 13:05:43 +0200 Subject: [PATCH 094/180] define default value in ExtractSequence fot thumbnail background color --- openpype/hosts/tvpaint/plugins/publish/extract_sequence.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 8588a5b07c..e0ea207db3 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -13,6 +13,9 @@ class ExtractSequence(pyblish.api.Extractor): hosts = ["tvpaint"] families = ["review", "renderPass", "renderLayer"] + # Modifiable with settings + thumbnail_bg = [255, 255, 255, 255] + def process(self, instance): self.log.info( "* Processing instance \"{}\"".format(instance.data["label"]) From 4d484189ed71114602bf9ebd46e1f434bde33e77 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 13:13:51 +0200 Subject: [PATCH 095/180] added function to get thumbnail bg color --- .../plugins/publish/extract_sequence.py | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index e0ea207db3..c13066d2c3 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -300,10 +300,18 @@ class ExtractSequence(pyblish.api.Extractor): # - just making sure source_img = Image.open(first_frame_filepath) if source_img.mode.lower() == "rgba": - bg_image = Image.new("RGBA", source_img.size, (255, 255, 255)) + bg_color = self._get_thumbnail_bg_color() + self.log.debug("Adding thumbnail background color {}.".format( + " ".join(bg_color) + )) + bg_image = Image.new("RGBA", source_img.size, bg_color) thumbnail_obj = Image.alpha_composite(bg_image, source_img) thumbnail_obj.convert("RGB").save(thumbnail_filepath) else: + self.log.info(( + "Source for thumbnail has mode \"{}\" (Expected: RGBA)." + " Can't use thubmanail background color." + ).format(source_img.mode)) source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath @@ -403,14 +411,32 @@ class ExtractSequence(pyblish.api.Extractor): # Composite background only on rgba images # - just making sure if source_img.mode.lower() == "rgba": - bg_image = Image.new("RGBA", source_img.size, (255, 255, 255)) + bg_color = self._get_thumbnail_bg_color() + self.log.debug("Adding thumbnail background color {}.".format( + " ".join(bg_color) + )) + bg_image = Image.new("RGBA", source_img.size, bg_color) thumbnail_obj = Image.alpha_composite(bg_image, source_img) thumbnail_obj.convert("RGB").save(thumbnail_filepath) + else: + self.log.info(( + "Source for thumbnail has mode \"{}\" (Expected: RGBA)." + " Can't use thubmanail background color." + ).format(source_img.mode)) source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath + def _get_thumbnail_bg_color(self): + red = green = blue = 255 + if self.thumbnail_bg: + if len(self.thumbnail_bg) == 4: + red, green, blue, _ = self.thumbnail_bg + elif len(self.thumbnail_bg) == 3: + red, green, blue = self.thumbnail_bg + return (red, green, blue) + def _render_layer( self, layer, From a7962aa3a05769a14a8091a7445e63945656b900 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 14:09:48 +0200 Subject: [PATCH 096/180] modified how labels in hosts enum works --- openpype/settings/entities/enum_entity.py | 52 +++++++++++--------- openpype/settings/entities/schemas/README.md | 7 ++- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 050f0038f7..d197683afe 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -124,34 +124,38 @@ class HostsEnumEntity(BaseEnumEntity): self.use_empty_value = self.schema_data.get( "use_empty_value", not self.multiselection ) - self.empty_label = ( - self.schema_data.get("empty_label") or "< without host >" - ) + custom_labels = self.schema_data.get("custom_labels") or {} + + host_names = [ + "aftereffects", + "blender", + "celaction", + "fusion", + "harmony", + "hiero", + "houdini", + "maya", + "nuke", + "photoshop", + "resolve", + "tvpaint", + "unreal" + ] + if self.use_empty_value: + host_names.insert(0, "") + # Add default label for empty value if not available + if "" not in custom_labels: + custom_labels[""] = "< without host >" # These are hardcoded there is not list of available host in OpenPype - self.enum_items = [ - {"aftereffects": "aftereffects"}, - {"blender": "blender"}, - {"celaction": "celaction"}, - {"fusion": "fusion"}, - {"harmony": "harmony"}, - {"hiero": "hiero"}, - {"houdini": "houdini"}, - {"maya": "maya"}, - {"nuke": "nuke"}, - {"photoshop": "photoshop"}, - {"resolve": "resolve"}, - {"tvpaint": "tvpaint"}, - {"unreal": "unreal"} - ] - - if self.use_empty_value: - self.enum_items.insert(0, {"": self.empty_label}) - + enum_items = [] valid_keys = set() - for item in self.enum_items or []: - valid_keys.add(tuple(item.keys())[0]) + for key in host_names: + label = custom_labels.get(key, key) + valid_keys.add(key) + enum_items.append({key, label}) + self.enum_items = enum_items self.valid_keys = valid_keys if self.multiselection: diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 0ad13bfe1a..bbd53fa46b 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -276,7 +276,7 @@ - enumeration of available hosts - multiselection can be allowed with setting key `"multiselection"` to `True` (Default: `False`) - it is possible to add empty value (represented with empty string) with setting `"use_empty_value"` to `True` (Default: `False`) -- to modify label of empty value set `"empty_label"` key with your label (Default: `< without host >`) +- it is possible to set `"custom_labels"` for host names where key `""` is empty value (Default: `{}`) ``` { "key": "host", @@ -284,7 +284,10 @@ "type": "hosts-enum", "multiselection": false, "use_empty_value": true, - "empty_label": "N/A" + "custom_labels": { + "": "N/A", + "nuke": "Nuke" + } } ``` From 9cbb10251947577e3e7b2c9b542b088c7bf2bc3c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 14:11:57 +0200 Subject: [PATCH 097/180] fix dict/set type --- openpype/settings/entities/enum_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index d197683afe..63e0afeb47 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -153,7 +153,7 @@ class HostsEnumEntity(BaseEnumEntity): for key in host_names: label = custom_labels.get(key, key) valid_keys.add(key) - enum_items.append({key, label}) + enum_items.append({key: label}) self.enum_items = enum_items self.valid_keys = valid_keys From b5d7da72404aa7edde91d1527d9f4978ba3bab73 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 15:19:01 +0200 Subject: [PATCH 098/180] nuke: create render crop format to nuke.root.format if selected to none --- .../hosts/nuke/plugins/create/create_write_render.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index 9ddf0e4a87..a1381122ee 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -100,6 +100,13 @@ class CreateWriteRender(plugin.PypeCreator): "/{subset}.{frame}.{ext}")}) # add crop node to cut off all outside of format bounding box + # get width and height + try: + width, height = (selected_node.width(), selected_node.height()) + except AttributeError: + actual_format = nuke.root().knob('format').value() + width, height = (actual_format.width(), actual_format.height()) + _prenodes = [ { "name": "Crop01", @@ -108,8 +115,8 @@ class CreateWriteRender(plugin.PypeCreator): ("box", [ 0.0, 0.0, - selected_node.width(), - selected_node.height() + width, + height ]) ], "dependent": None From 7ca8ce40a2aecafa272406d2fbd1908dd12ab8a1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 15:20:07 +0200 Subject: [PATCH 099/180] pep8 fix --- openpype/hosts/nuke/plugins/create/create_write_prerender.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index bb01236801..1b925014ad 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -128,5 +128,4 @@ class CreateWritePrerender(plugin.PypeCreator): w_node["first"].setValue(nuke.root()["first_frame"].value()) w_node["last"].setValue(nuke.root()["last_frame"].value()) - return write_node From e430797b39cd4acd061af10210bc89785922477c Mon Sep 17 00:00:00 2001 From: jezscha Date: Tue, 22 Jun 2021 13:36:12 +0000 Subject: [PATCH 100/180] Create draft PR for #1688 From 4f66746d9e1e44ed69d95a9f49dfb995084c5102 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 15:46:40 +0200 Subject: [PATCH 101/180] collect scene bg color --- .../plugins/publish/collect_workfile_data.py | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py index 13c6c9eb78..d8bb03f541 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -1,5 +1,6 @@ import os import json +import tempfile import pyblish.api import avalon.api @@ -153,9 +154,45 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): "sceneMarkIn": int(mark_in_frame), "sceneMarkInState": mark_in_state == "set", "sceneMarkOut": int(mark_out_frame), - "sceneMarkOutState": mark_out_state == "set" + "sceneMarkOutState": mark_out_state == "set", + "sceneBgColor": self._get_bg_color() } self.log.debug( "Scene data: {}".format(json.dumps(scene_data, indent=4)) ) context.data.update(scene_data) + + def _get_bg_color(self): + """Background color set on scene. + + Is important for review exporting where scene bg color is used as + background. + """ + output_file = tempfile.NamedTemporaryFile( + mode="w", prefix="a_tvp_", suffix=".txt", delete=False + ) + output_file.close() + output_filepath = output_file.name.replace("\\", "/") + george_script_lines = [ + # Variable containing full path to output file + "output_path = \"{}\"".format(output_filepath), + "tv_background", + "bg_color = result", + # Write data to output file + ( + "tv_writetextfile" + " \"strict\" \"append\" '\"'output_path'\"' bg_color" + ) + ] + + george_script = "\n".join(george_script_lines) + lib.execute_george_through_file(george_script) + + with open(output_filepath, "r") as stream: + data = stream.read() + + os.remove(output_filepath) + data = data.strip() + if not data: + return None + return data.split(" ") From ddd3668277ace0136e95e3b489e46e935bfeb2ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 15:51:56 +0200 Subject: [PATCH 102/180] renamed thumbnail_bg_color to review_bg_color --- .../plugins/publish/extract_sequence.py | 32 ++++++------------- .../defaults/project_settings/tvpaint.json | 2 +- .../schema_project_tvpaint.json | 8 +++-- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index c13066d2c3..6e5ec39f46 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -14,7 +14,7 @@ class ExtractSequence(pyblish.api.Extractor): families = ["review", "renderPass", "renderLayer"] # Modifiable with settings - thumbnail_bg = [255, 255, 255, 255] + review_bg = [255, 255, 255, 255] def process(self, instance): self.log.info( @@ -299,20 +299,6 @@ class ExtractSequence(pyblish.api.Extractor): # Composite background only on rgba images # - just making sure source_img = Image.open(first_frame_filepath) - if source_img.mode.lower() == "rgba": - bg_color = self._get_thumbnail_bg_color() - self.log.debug("Adding thumbnail background color {}.".format( - " ".join(bg_color) - )) - bg_image = Image.new("RGBA", source_img.size, bg_color) - thumbnail_obj = Image.alpha_composite(bg_image, source_img) - thumbnail_obj.convert("RGB").save(thumbnail_filepath) - else: - self.log.info(( - "Source for thumbnail has mode \"{}\" (Expected: RGBA)." - " Can't use thubmanail background color." - ).format(source_img.mode)) - source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath @@ -411,9 +397,9 @@ class ExtractSequence(pyblish.api.Extractor): # Composite background only on rgba images # - just making sure if source_img.mode.lower() == "rgba": - bg_color = self._get_thumbnail_bg_color() + bg_color = self._get_review_bg_color() self.log.debug("Adding thumbnail background color {}.".format( - " ".join(bg_color) + " ".join([str(val) for val in bg_color]) )) bg_image = Image.new("RGBA", source_img.size, bg_color) thumbnail_obj = Image.alpha_composite(bg_image, source_img) @@ -428,13 +414,13 @@ class ExtractSequence(pyblish.api.Extractor): return output_filenames, thumbnail_filepath - def _get_thumbnail_bg_color(self): + def _get_review_bg_color(self): red = green = blue = 255 - if self.thumbnail_bg: - if len(self.thumbnail_bg) == 4: - red, green, blue, _ = self.thumbnail_bg - elif len(self.thumbnail_bg) == 3: - red, green, blue = self.thumbnail_bg + if self.review_bg: + if len(self.review_bg) == 4: + red, green, blue, _ = self.review_bg + elif len(self.review_bg) == 3: + red, green, blue = self.review_bg return (red, green, blue) def _render_layer( diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 25b7056ce1..763802a73f 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -1,7 +1,7 @@ { "publish": { "ExtractSequence": { - "thumbnail_bg": [ + "review_bg": [ 255, 255, 255, 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 1894384bb9..67aa4b0a06 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -18,10 +18,14 @@ "label": "ExtractSequence", "is_group": true, "children": [ + { + "type": "label", + "label": "Review BG color is used for whole scene review and for thumbnails." + }, { "type": "color", - "key": "thumbnail_bg", - "label": "Thumbnail BG color", + "key": "review_bg", + "label": "Review BG color", "use_alpha": false } ] From d5b0976b2d07ef75f2c4c3f58897094b6918f904 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 15:52:26 +0200 Subject: [PATCH 103/180] pass scene bg color to review extaction --- .../hosts/tvpaint/plugins/publish/extract_sequence.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 6e5ec39f46..dc6e7eb64f 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -56,6 +56,8 @@ class ExtractSequence(pyblish.api.Extractor): handle_start = instance.context.data["handleStart"] handle_end = instance.context.data["handleEnd"] + scene_bg_color = instance.context.data["sceneBgColor"] + # --- Fallbacks ---------------------------------------------------- # This is required if validations of ranges are ignored. # - all of this code won't change processing if range to render @@ -123,7 +125,8 @@ class ExtractSequence(pyblish.api.Extractor): if instance.data["family"] == "review": output_filenames, thumbnail_fullpath = self.render_review( - filename_template, output_dir, mark_in, mark_out + filename_template, output_dir, mark_in, mark_out, + scene_bg_color ) else: # Render output @@ -244,7 +247,9 @@ class ExtractSequence(pyblish.api.Extractor): for path in repre_filepaths ] - def render_review(self, filename_template, output_dir, mark_in, mark_out): + def render_review( + self, filename_template, output_dir, mark_in, mark_out, scene_bg_color + ): """ Export images from TVPaint using `tv_savesequence` command. Args: @@ -255,6 +260,8 @@ class ExtractSequence(pyblish.api.Extractor): output_dir (str): Directory where files will be stored. mark_in (int): Starting frame index from which export will begin. mark_out (int): On which frame index export will end. + scene_bg_color (list): Bg color set in scene. Result of george + script command `tv_background`. Retruns: tuple: With 2 items first is list of filenames second is path to From 70bd4460a054c8843145874aed9e5fa9168399b6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 15:52:46 +0200 Subject: [PATCH 104/180] change bg color to color from settings and back --- .../tvpaint/plugins/publish/extract_sequence.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index dc6e7eb64f..c09cf08d82 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -1,5 +1,6 @@ import os import shutil +import copy import tempfile import pyblish.api @@ -273,7 +274,11 @@ class ExtractSequence(pyblish.api.Extractor): filename_template.format(frame=mark_in) ) + bg_color = self._get_review_bg_color() + george_script_lines = [ + # Change bg color to color from settings + "tv_background \"color\" {} {} {}".format(*bg_color), "tv_SaveMode \"PNG\"", "export_path = \"{}\"".format( first_frame_filepath.replace("\\", "/") @@ -282,6 +287,18 @@ class ExtractSequence(pyblish.api.Extractor): mark_in, mark_out ) ] + if scene_bg_color: + # Change bg color back to previous scene bg color + _scene_bg_color = copy.deepcopy(scene_bg_color) + bg_type = _scene_bg_color.pop(0) + orig_color_command = [ + "tv_background", + "\"{}\"".format(bg_type) + ] + orig_color_command.extend(_scene_bg_color) + + george_script_lines.append(" ".join(orig_color_command)) + lib.execute_george_through_file("\n".join(george_script_lines)) first_frame_filepath = None From e708f95f0dd61f069f3add62ebb6e7fec64ab77a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 15:53:16 +0200 Subject: [PATCH 105/180] thumbnail creation from review is simplified as bg color is already applied on output --- .../hosts/tvpaint/plugins/publish/extract_sequence.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index c09cf08d82..536df2adb0 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -318,11 +318,13 @@ class ExtractSequence(pyblish.api.Extractor): if first_frame_filepath is None: first_frame_filepath = filepath - thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") + thumbnail_filepath = None if first_frame_filepath and os.path.exists(first_frame_filepath): - # Composite background only on rgba images - # - just making sure + thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") source_img = Image.open(first_frame_filepath) + if source_img.mode.lower() != "rgb": + source_img = source_img.convert("RGB") + source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath From 9105b9eb6d314471f4a0ba91994494e964de55f3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 16:16:13 +0200 Subject: [PATCH 106/180] hiero: fixing creator (deepcopy) --- .../hiero/plugins/create/create_shot_clip.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/hiero/plugins/create/create_shot_clip.py b/openpype/hosts/hiero/plugins/create/create_shot_clip.py index 25be9f090b..0c5bf93a3f 100644 --- a/openpype/hosts/hiero/plugins/create/create_shot_clip.py +++ b/openpype/hosts/hiero/plugins/create/create_shot_clip.py @@ -1,3 +1,4 @@ +from copy import deepcopy import openpype.hosts.hiero.api as phiero # from openpype.hosts.hiero.api import plugin, lib # reload(lib) @@ -206,20 +207,24 @@ class CreateShotClip(phiero.Creator): presets = None def process(self): + # Creator copy of object attributes that are modified during `process` + presets = deepcopy(self.presets) + gui_inputs = deepcopy(self.gui_inputs) + # get key pares from presets and match it on ui inputs - for k, v in self.gui_inputs.items(): + for k, v in gui_inputs.items(): if v["type"] in ("dict", "section"): # nested dictionary (only one level allowed # for sections and dict) for _k, _v in v["value"].items(): - if self.presets.get(_k): - self.gui_inputs[k][ - "value"][_k]["value"] = self.presets[_k] - if self.presets.get(k): - self.gui_inputs[k]["value"] = self.presets[k] + if presets.get(_k): + gui_inputs[k][ + "value"][_k]["value"] = presets[_k] + if presets.get(k): + gui_inputs[k]["value"] = presets[k] # open widget for plugins inputs - widget = self.widget(self.gui_name, self.gui_info, self.gui_inputs) + widget = self.widget(self.gui_name, self.gui_info, gui_inputs) widget.exec_() if len(self.selected) < 1: From 0c91aee6e16500e620f2135eb54ad18c551df6da Mon Sep 17 00:00:00 2001 From: jezscha Date: Tue, 22 Jun 2021 14:19:38 +0000 Subject: [PATCH 107/180] Create draft PR for #1740 From c739967b48c78ab540df9bc283d370776b203c89 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 16:53:32 +0200 Subject: [PATCH 108/180] hiero: collector ignoring audio clips --- .../hiero/plugins/publish/precollect_instances.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 6e5c640e35..40c6be0f78 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -46,12 +46,6 @@ class PrecollectInstances(pyblish.api.ContextPlugin): source_clip = track_item.source() self.log.debug("clip_name: {}".format(clip_name)) - # get clips subtracks and anotations - annotations = self.clip_annotations(source_clip) - subtracks = self.clip_subtrack(track_item) - self.log.debug("Annotations: {}".format(annotations)) - self.log.debug(">> Subtracks: {}".format(subtracks)) - # get openpype tag data tag_data = phiero.get_track_item_pype_data(track_item) self.log.debug("__ tag_data: {}".format(pformat(tag_data))) @@ -62,6 +56,12 @@ class PrecollectInstances(pyblish.api.ContextPlugin): if tag_data.get("id") != "pyblish.avalon.instance": continue + # get clips subtracks and anotations + annotations = self.clip_annotations(source_clip) + subtracks = self.clip_subtrack(track_item) + self.log.debug("Annotations: {}".format(annotations)) + self.log.debug(">> Subtracks: {}".format(subtracks)) + # solve handles length tag_data["handleStart"] = min( tag_data["handleStart"], int(track_item.handleInLength())) From 374f327ccd9145f23849c3627752db4b118c9101 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 16:58:09 +0200 Subject: [PATCH 109/180] pep8 fix --- openpype/hosts/hiero/plugins/publish/precollect_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 40c6be0f78..4984849aa7 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -56,7 +56,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): if tag_data.get("id") != "pyblish.avalon.instance": continue - # get clips subtracks and anotations + # get clips subtracks and anotations annotations = self.clip_annotations(source_clip) subtracks = self.clip_subtrack(track_item) self.log.debug("Annotations: {}".format(annotations)) From ac6278f076128946f7f4fce6563a6485c59a571f Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 23 Jun 2021 03:40:30 +0000 Subject: [PATCH 110/180] [Automated] Bump version --- CHANGELOG.md | 9 +++++++-- openpype/version.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7c0a25c7d..9fe6de4bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # Changelog -## [3.2.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...HEAD) **🚀 Enhancements** +- Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) - Autoupdate launcher [\#1725](https://github.com/pypeclub/OpenPype/pull/1725) - Subset template and TVPaint subset template docs [\#1717](https://github.com/pypeclub/OpenPype/pull/1717) - Overscan color extract review [\#1701](https://github.com/pypeclub/OpenPype/pull/1701) @@ -14,10 +15,14 @@ **🐛 Bug fixes** +- Local settings UI crash on missing defaults [\#1737](https://github.com/pypeclub/OpenPype/pull/1737) +- Ftrack missing custom attribute message [\#1734](https://github.com/pypeclub/OpenPype/pull/1734) +- Launcher project changes [\#1733](https://github.com/pypeclub/OpenPype/pull/1733) - TVPaint use layer name for default variant [\#1724](https://github.com/pypeclub/OpenPype/pull/1724) - Default subset template for TVPaint review and workfile families [\#1716](https://github.com/pypeclub/OpenPype/pull/1716) - Maya: Extract review hotfix [\#1714](https://github.com/pypeclub/OpenPype/pull/1714) - Settings: Imageio improving granularity [\#1711](https://github.com/pypeclub/OpenPype/pull/1711) +- Application without executables [\#1679](https://github.com/pypeclub/OpenPype/pull/1679) ## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-18) @@ -25,6 +30,7 @@ **🐛 Bug fixes** +- Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) - Remove publish highlight icon in AE [\#1664](https://github.com/pypeclub/OpenPype/pull/1664) **Merged pull requests:** @@ -74,7 +80,6 @@ **🐛 Bug fixes** -- Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) - Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) - Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) diff --git a/openpype/version.py b/openpype/version.py index ece0359506..f527bd4d6e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.1" +__version__ = "3.2.0-nightly.2" From 43ddbf89991b49a31463bbd3c190fbd8e19be36d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 11:01:11 +0200 Subject: [PATCH 111/180] update acre commit to fixed version of acre --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 48e6f95469..30dbe50c19 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,7 +11,7 @@ develop = false type = "git" url = "https://github.com/pypeclub/acre.git" reference = "master" -resolved_reference = "efc1b8faa8f84568538b936688ae6f7604dd194c" +resolved_reference = "68784b7eb5b7bb5f409b61ab31d4403878a3e1b7" [[package]] name = "aiohttp" From 946821b9fdf59c24a47ae627a5bb0d29ea82026b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Jun 2021 13:16:58 +0200 Subject: [PATCH 112/180] Updated submodule repos/avalon-core --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index efde026e5a..d8be0bdb37 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit efde026e5aad72dac0e69848005419e2c4f067f2 +Subproject commit d8be0bdb37961e32243f1de0eb9696e86acf7443 From e6111daec83c49fecaebb125374dfd6a3cfb8d41 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:16:51 +0200 Subject: [PATCH 113/180] created SchemasHub for handling schemas --- openpype/settings/entities/lib.py | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 05f4ea64f8..9d13655a8f 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -426,3 +426,49 @@ class OverrideState: DEFAULTS = OverrideStateItem(0, "Defaults") STUDIO = OverrideStateItem(1, "Studio overrides") PROJECT = OverrideStateItem(2, "Project Overrides") + + +class SchemasHub: + def __init__(self, schema_subfolder): + from openpype.settings import entities + + # Define known abstract classes + known_abstract_classes = ( + entities.BaseEntity, + entities.BaseItemEntity, + entities.ItemEntity, + entities.EndpointEntity, + entities.InputEntity, + entities.BaseEnumEntity + ) + + self._loaded_types = {} + _gui_types = [] + for attr in dir(entities): + item = getattr(entities, attr) + # Filter classes + if not inspect.isclass(item): + continue + + # Skip classes that do not inherit from BaseEntity + if not issubclass(item, entities.BaseEntity): + continue + + # Skip class that is abstract by design + if item in known_abstract_classes: + continue + + if inspect.isabstract(item): + # Create an object to get crash and get traceback + item() + + # Backwards compatibility + # Single entity may have multiple schema types + for schema_type in item.schema_types: + self._loaded_types[schema_type] = item + + if item.gui_type: + _gui_types.append(item) + self._gui_types = tuple(_gui_types) + + self._schema_subfolder = schema_subfolder From 070ba3070af4a61d240aeb4bef166c425c79b947 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:20:07 +0200 Subject: [PATCH 114/180] added api callbacks to SchemaHub --- openpype/settings/entities/lib.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 9d13655a8f..879e3d9cad 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -472,3 +472,19 @@ class SchemasHub: self._gui_types = tuple(_gui_types) self._schema_subfolder = schema_subfolder + + @property + def gui_types(self): + return self._gui_types + + def get_schema(self, schema_name): + pass + + def get_template(self, template_name): + pass + + def resolve_schema_data(self, schema_data): + pass + + def create_schema_object(self, schema_data, *args, **kwargs): + pass From e3c4c91f3e91ab9480408d3b1f5f5445297b6a57 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:21:41 +0200 Subject: [PATCH 115/180] use schema hub inside root entity --- openpype/settings/entities/root_entities.py | 87 ++++++--------------- 1 file changed, 22 insertions(+), 65 deletions(-) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 401d3980c9..9bb32382fb 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -1,7 +1,6 @@ import os import json import copy -import inspect from abc import abstractmethod @@ -10,8 +9,7 @@ from .lib import ( NOT_SET, WRAPPER_TYPES, OverrideState, - get_studio_settings_schema, - get_project_settings_schema + SchemasHub ) from .exceptions import ( SchemaError, @@ -53,7 +51,12 @@ class RootEntity(BaseItemEntity): """ schema_types = ["root"] - def __init__(self, schema_data, reset): + def __init__(self, schema_hub, reset, main_schema_name=None): + self.schema_hub = schema_hub + if not main_schema_name: + main_schema_name = "schema_main" + schema_data = schema_hub.get_schema(main_schema_name) + super(RootEntity, self).__init__(schema_data) self._require_restart_callbacks = [] self._item_ids_require_restart = set() @@ -143,11 +146,13 @@ class RootEntity(BaseItemEntity): child_obj = self.create_schema_object(children_schema, self) self.children.append(child_obj) added_children.append(child_obj) - if isinstance(child_obj, self._gui_types): + if isinstance(child_obj, self.schema_hub.gui_types): continue if child_obj.key in self.non_gui_children: - raise KeyError("Duplicated key \"{}\"".format(child_obj.key)) + raise KeyError( + "Duplicated key \"{}\"".format(child_obj.key) + ) self.non_gui_children[child_obj.key] = child_obj if not first: @@ -160,9 +165,6 @@ class RootEntity(BaseItemEntity): # Store `self` to `root_item` for children entities self.root_item = self - self._loaded_types = None - self._gui_types = None - # Children are stored by key as keys are immutable and are defined by # schema self.valid_value_types = (dict, ) @@ -201,54 +203,9 @@ class RootEntity(BaseItemEntity): Available entities are loaded on first run. Children entities can call this method. """ - if self._loaded_types is None: - # Load available entities - from openpype.settings import entities - - # Define known abstract classes - known_abstract_classes = ( - entities.BaseEntity, - entities.BaseItemEntity, - entities.ItemEntity, - entities.EndpointEntity, - entities.InputEntity, - entities.BaseEnumEntity - ) - - self._loaded_types = {} - _gui_types = [] - for attr in dir(entities): - item = getattr(entities, attr) - # Filter classes - if not inspect.isclass(item): - continue - - # Skip classes that do not inherit from BaseEntity - if not issubclass(item, entities.BaseEntity): - continue - - # Skip class that is abstract by design - if item in known_abstract_classes: - continue - - if inspect.isabstract(item): - # Create an object to get crash and get traceback - item() - - # Backwards compatibility - # Single entity may have multiple schema types - for schema_type in item.schema_types: - self._loaded_types[schema_type] = item - - if item.gui_type: - _gui_types.append(item) - self._gui_types = tuple(_gui_types) - - klass = self._loaded_types.get(schema_data["type"]) - if not klass: - raise KeyError("Unknown type \"{}\"".format(schema_data["type"])) - - return klass(schema_data, *args, **kwargs) + return self.schema_hub.create_schema_object( + schema_data, *args, **kwargs + ) def set_override_state(self, state): """Set override state and trigger it on children. @@ -492,13 +449,13 @@ class SystemSettings(RootEntity): and debugging purposes. """ def __init__( - self, set_studio_state=True, reset=True, schema_data=None + self, set_studio_state=True, reset=True, schema_hub=None ): - if schema_data is None: + if schema_hub is None: # Load system schemas - schema_data = get_studio_settings_schema() + schema_hub = SchemasHub("system_schema") - super(SystemSettings, self).__init__(schema_data, reset) + super(SystemSettings, self).__init__(schema_hub, reset) if set_studio_state: self.set_studio_state() @@ -605,17 +562,17 @@ class ProjectSettings(RootEntity): project_name=None, change_state=True, reset=True, - schema_data=None + schema_hub=None ): self._project_name = project_name self._system_settings_entity = None - if schema_data is None: + if schema_hub is None: # Load system schemas - schema_data = get_project_settings_schema() + schema_hub = SchemasHub("projects_schema") - super(ProjectSettings, self).__init__(schema_data, reset) + super(ProjectSettings, self).__init__(schema_hub, reset) if change_state: if self.project_name is None: From cb7e0c957ca94fa89162a242c7b64ab77df6e25b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:22:19 +0200 Subject: [PATCH 116/180] use resolving where templates can be used --- .../settings/entities/dict_immutable_keys_entity.py | 12 +++++++++++- openpype/settings/entities/root_entities.py | 13 ++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index 052bbda4d0..c965dc3b5a 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -1,4 +1,5 @@ import copy +import collections from .lib import ( WRAPPER_TYPES, @@ -138,7 +139,16 @@ class DictImmutableKeysEntity(ItemEntity): method when handling gui wrappers. """ added_children = [] - for children_schema in schema_data["children"]: + children_deque = collections.deque() + for _children_schema in schema_data["children"]: + children_schemas = self.schema_hub.resolve_schema_data( + _children_schema + ) + for children_schema in children_schemas: + children_deque.append(children_schema) + + while children_deque: + children_schema = children_deque.popleft() if children_schema["type"] in WRAPPER_TYPES: _children_schema = copy.deepcopy(children_schema) wrapper_children = self._add_children( diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 9bb32382fb..1833535a07 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -1,6 +1,7 @@ import os import json import copy +import collections from abc import abstractmethod @@ -133,7 +134,17 @@ class RootEntity(BaseItemEntity): def _add_children(self, schema_data, first=True): added_children = [] - for children_schema in schema_data["children"]: + children_deque = collections.deque() + for _children_schema in schema_data["children"]: + children_schemas = self.schema_hub.resolve_schema_data( + _children_schema + ) + for children_schema in children_schemas: + children_deque.append(children_schema) + + while children_deque: + children_schema = children_deque.popleft() + if children_schema["type"] in WRAPPER_TYPES: _children_schema = copy.deepcopy(children_schema) wrapper_children = self._add_children( From d02e6eda745b35b6bf7c221b369d157a8b901bea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:22:39 +0200 Subject: [PATCH 117/180] gave access to event hub for all entities --- openpype/settings/entities/base_entity.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index c6bff1ff47..0e29a35e1f 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -885,7 +885,11 @@ class ItemEntity(BaseItemEntity): def create_schema_object(self, *args, **kwargs): """Reference method for creation of entities defined in RootEntity.""" - return self.root_item.create_schema_object(*args, **kwargs) + return self.schema_hub.create_schema_object(*args, **kwargs) + + @property + def schema_hub(self): + return self.root_item.schema_hub def get_entity_from_path(self, path): return self.root_item.get_entity_from_path(path) From 0fc16b25767e8f7d614553def050b49552ba09a0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:23:49 +0200 Subject: [PATCH 118/180] moved template filling functions under SchemaHub --- openpype/settings/entities/lib.py | 351 +++++++++++++++--------------- 1 file changed, 175 insertions(+), 176 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 879e3d9cad..74de3e6ffa 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -25,182 +25,6 @@ TEMPLATE_METADATA_KEYS = ( template_key_pattern = re.compile(r"(\{.*?[^{0]*\})") -def _pop_metadata_item(template): - found_idx = None - for idx, item in enumerate(template): - if not isinstance(item, dict): - continue - - for key in TEMPLATE_METADATA_KEYS: - if key in item: - found_idx = idx - break - - if found_idx is not None: - break - - metadata_item = {} - if found_idx is not None: - metadata_item = template.pop(found_idx) - return metadata_item - - -def _fill_schema_template_data( - template, template_data, skip_paths, required_keys=None, missing_keys=None -): - first = False - if required_keys is None: - first = True - - if "skip_paths" in template_data: - skip_paths = template_data["skip_paths"] - if not isinstance(skip_paths, list): - skip_paths = [skip_paths] - - # Cleanup skip paths (skip empty values) - skip_paths = [path for path in skip_paths if path] - - required_keys = set() - missing_keys = set() - - # Copy template data as content may change - template = copy.deepcopy(template) - - # Get metadata item from template - metadata_item = _pop_metadata_item(template) - - # Check for default values for template data - default_values = metadata_item.get(DEFAULT_VALUES_KEY) or {} - - for key, value in default_values.items(): - if key not in template_data: - template_data[key] = value - - if not template: - output = template - - elif isinstance(template, list): - # Store paths by first part if path - # - None value says that whole key should be skipped - skip_paths_by_first_key = {} - for path in skip_paths: - parts = path.split("/") - key = parts.pop(0) - if key not in skip_paths_by_first_key: - skip_paths_by_first_key[key] = [] - - value = "/".join(parts) - skip_paths_by_first_key[key].append(value or None) - - output = [] - for item in template: - # Get skip paths for children item - _skip_paths = [] - if not isinstance(item, dict): - pass - - elif item.get("type") in WRAPPER_TYPES: - _skip_paths = copy.deepcopy(skip_paths) - - elif skip_paths_by_first_key: - # Check if this item should be skipped - key = item.get("key") - if key and key in skip_paths_by_first_key: - _skip_paths = skip_paths_by_first_key[key] - # Skip whole item if None is in skip paths value - if None in _skip_paths: - continue - - output_item = _fill_schema_template_data( - item, template_data, _skip_paths, required_keys, missing_keys - ) - if output_item: - output.append(output_item) - - elif isinstance(template, dict): - output = {} - for key, value in template.items(): - output[key] = _fill_schema_template_data( - value, template_data, skip_paths, required_keys, missing_keys - ) - if output.get("type") in WRAPPER_TYPES and not output.get("children"): - return {} - - elif isinstance(template, STRING_TYPE): - # TODO find much better way how to handle filling template data - template = template.replace("{{", "__dbcb__").replace("}}", "__decb__") - for replacement_string in template_key_pattern.findall(template): - key = str(replacement_string[1:-1]) - required_keys.add(key) - if key not in template_data: - missing_keys.add(key) - continue - - value = template_data[key] - if replacement_string == template: - # Replace the value with value from templates data - # - with this is possible to set value with different type - template = value - else: - # Only replace the key in string - template = template.replace(replacement_string, value) - - output = template.replace("__dbcb__", "{").replace("__decb__", "}") - - else: - output = template - - if first and missing_keys: - raise SchemaTemplateMissingKeys(missing_keys, required_keys) - - return output - - -def _fill_schema_template(child_data, schema_collection, schema_templates): - template_name = child_data["name"] - template = schema_templates.get(template_name) - if template is None: - if template_name in schema_collection: - raise KeyError(( - "Schema \"{}\" is used as `schema_template`" - ).format(template_name)) - raise KeyError("Schema template \"{}\" was not found".format( - template_name - )) - - # Default value must be dictionary (NOT list) - # - empty list would not add any item if `template_data` are not filled - template_data = child_data.get("template_data") or {} - if isinstance(template_data, dict): - template_data = [template_data] - - skip_paths = child_data.get("skip_paths") or [] - if isinstance(skip_paths, STRING_TYPE): - skip_paths = [skip_paths] - - output = [] - for single_template_data in template_data: - try: - filled_child = _fill_schema_template_data( - template, single_template_data, skip_paths - ) - - except SchemaTemplateMissingKeys as exc: - raise SchemaTemplateMissingKeys( - exc.missing_keys, exc.required_keys, template_name - ) - - for item in filled_child: - filled_item = _fill_inner_schemas( - item, schema_collection, schema_templates - ) - if filled_item["type"] == "schema_template": - output.extend(_fill_schema_template( - filled_item, schema_collection, schema_templates - )) - else: - output.append(filled_item) - return output def _fill_inner_schemas(schema_data, schema_collection, schema_templates): @@ -488,3 +312,178 @@ class SchemasHub: def create_schema_object(self, schema_data, *args, **kwargs): pass + + def _fill_schema_template(self, child_data, template_def): + template_name = child_data["name"] + + # Default value must be dictionary (NOT list) + # - empty list would not add any item if `template_data` are not filled + template_data = child_data.get("template_data") or {} + if isinstance(template_data, dict): + template_data = [template_data] + + skip_paths = child_data.get("skip_paths") or [] + if isinstance(skip_paths, STRING_TYPE): + skip_paths = [skip_paths] + + output = [] + for single_template_data in template_data: + try: + output.extend(self._fill_schema_template_data( + template_def, single_template_data, skip_paths + )) + + except SchemaTemplateMissingKeys as exc: + raise SchemaTemplateMissingKeys( + exc.missing_keys, exc.required_keys, template_name + ) + return output + + def _fill_schema_template_data( + self, + template, + template_data, + skip_paths, + required_keys=None, + missing_keys=None + ): + first = False + if required_keys is None: + first = True + + if "skip_paths" in template_data: + skip_paths = template_data["skip_paths"] + if not isinstance(skip_paths, list): + skip_paths = [skip_paths] + + # Cleanup skip paths (skip empty values) + skip_paths = [path for path in skip_paths if path] + + required_keys = set() + missing_keys = set() + + # Copy template data as content may change + template = copy.deepcopy(template) + + # Get metadata item from template + metadata_item = self._pop_metadata_item(template) + + # Check for default values for template data + default_values = metadata_item.get(DEFAULT_VALUES_KEY) or {} + + for key, value in default_values.items(): + if key not in template_data: + template_data[key] = value + + if not template: + output = template + + elif isinstance(template, list): + # Store paths by first part if path + # - None value says that whole key should be skipped + skip_paths_by_first_key = {} + for path in skip_paths: + parts = path.split("/") + key = parts.pop(0) + if key not in skip_paths_by_first_key: + skip_paths_by_first_key[key] = [] + + value = "/".join(parts) + skip_paths_by_first_key[key].append(value or None) + + output = [] + for item in template: + # Get skip paths for children item + _skip_paths = [] + if not isinstance(item, dict): + pass + + elif item.get("type") in WRAPPER_TYPES: + _skip_paths = copy.deepcopy(skip_paths) + + elif skip_paths_by_first_key: + # Check if this item should be skipped + key = item.get("key") + if key and key in skip_paths_by_first_key: + _skip_paths = skip_paths_by_first_key[key] + # Skip whole item if None is in skip paths value + if None in _skip_paths: + continue + + output_item = self._fill_schema_template_data( + item, + template_data, + _skip_paths, + required_keys, + missing_keys + ) + if output_item: + output.append(output_item) + + elif isinstance(template, dict): + output = {} + for key, value in template.items(): + output[key] = self._fill_schema_template_data( + value, + template_data, + skip_paths, + required_keys, + missing_keys + ) + if ( + output.get("type") in WRAPPER_TYPES + and not output.get("children") + ): + return {} + + elif isinstance(template, STRING_TYPE): + # TODO find much better way how to handle filling template data + template = ( + template + .replace("{{", "__dbcb__") + .replace("}}", "__decb__") + ) + for replacement_string in template_key_pattern.findall(template): + key = str(replacement_string[1:-1]) + required_keys.add(key) + if key not in template_data: + missing_keys.add(key) + continue + + value = template_data[key] + if replacement_string == template: + # Replace the value with value from templates data + # - with this is possible to set value with different type + template = value + else: + # Only replace the key in string + template = template.replace(replacement_string, value) + + output = template.replace("__dbcb__", "{").replace("__decb__", "}") + + else: + output = template + + if first and missing_keys: + raise SchemaTemplateMissingKeys(missing_keys, required_keys) + + return output + + def _pop_metadata_item(self, template_def): + found_idx = None + for idx, item in enumerate(template_def): + if not isinstance(item, dict): + continue + + for key in TEMPLATE_METADATA_KEYS: + if key in item: + found_idx = idx + break + + if found_idx is not None: + break + + metadata_item = {} + if found_idx is not None: + metadata_item = template_def.pop(found_idx) + return metadata_item From d0b32e129271806132d84cb37011dc69ba68ad76 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:24:29 +0200 Subject: [PATCH 119/180] implemented loading of schemas for schema hub --- openpype/settings/entities/lib.py | 64 +++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 74de3e6ffa..aae98067f7 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -296,6 +296,11 @@ class SchemasHub: self._gui_types = tuple(_gui_types) self._schema_subfolder = schema_subfolder + self._crashed_on_load = {} + loaded_templates, loaded_schemas = self._load_schemas() + + self._loaded_templates = loaded_templates + self._loaded_schemas = loaded_schemas @property def gui_types(self): @@ -313,6 +318,65 @@ class SchemasHub: def create_schema_object(self, schema_data, *args, **kwargs): pass + def _load_schemas(self): + dirpath = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "schemas", + self._schema_subfolder + ) + loaded_schemas = {} + loaded_templates = {} + for root, _, filenames in os.walk(dirpath): + for filename in filenames: + basename, ext = os.path.splitext(filename) + if ext != ".json": + continue + + filepath = os.path.join(root, filename) + with open(filepath, "r") as json_stream: + try: + schema_data = json.load(json_stream) + except Exception as exc: + msg = str(exc) + print("Unable to parse JSON file {}\n{}".format( + filepath, msg + )) + self._crashed_on_load[basename] = { + "filepath": filepath, + "message": msg + } + continue + + if basename in self._crashed_on_load: + crashed_item = self._crashed_on_load[basename] + raise KeyError(( + "Duplicated filename \"{}\"." + " One of them crashed on load \"{}\" {}" + ).format( + filename, + crashed_item["filpath"], + crashed_item["message"] + )) + + if isinstance(schema_data, list): + if basename in loaded_templates: + raise KeyError( + "Duplicated template filename \"{}\"".format( + filename + ) + ) + loaded_templates[basename] = schema_data + else: + if basename in loaded_schemas: + raise KeyError( + "Duplicated schema filename \"{}\"".format( + filename + ) + ) + loaded_schemas[basename] = schema_data + + return loaded_templates, loaded_schemas + def _fill_schema_template(self, child_data, template_def): template_name = child_data["name"] From 770e33d0f9a2e507f3499ead0b655a014ae4748b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:24:54 +0200 Subject: [PATCH 120/180] implemented get_template and get_schema --- openpype/settings/entities/lib.py | 38 +++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index aae98067f7..cdc154e441 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -307,10 +307,44 @@ class SchemasHub: return self._gui_types def get_schema(self, schema_name): - pass + if schema_name not in self._loaded_schemas: + if schema_name in self._loaded_templates: + raise KeyError(( + "Template \"{}\" is used as `schema`" + ).format(schema_name)) + + elif schema_name in self._crashed_on_load: + crashed_item = self._crashed_on_load[schema_name] + raise KeyError( + "Unable to parse schema file \"{}\". {}".format( + crashed_item["filpath"], crashed_item["message"] + ) + ) + + raise KeyError( + "Schema \"{}\" was not found".format(schema_name) + ) + return copy.deepcopy(self._loaded_schemas[schema_name]) def get_template(self, template_name): - pass + if template_name not in self._loaded_templates: + if template_name in self._loaded_schemas: + raise KeyError(( + "Schema \"{}\" is used as `template`" + ).format(template_name)) + + elif template_name in self._crashed_on_load: + crashed_item = self._crashed_on_load[template_name] + raise KeyError( + "Unable to parse templace file \"{}\". {}".format( + crashed_item["filpath"], crashed_item["message"] + ) + ) + + raise KeyError( + "Template \"{}\" was not found".format(template_name) + ) + return copy.deepcopy(self._loaded_templates[template_name]) def resolve_schema_data(self, schema_data): pass From cae6f7e6209b879908ead4d31b75ab99e818b774 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:25:42 +0200 Subject: [PATCH 121/180] implemented create_schema_object which handle creation of entities by schema data --- openpype/settings/entities/lib.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index cdc154e441..0e67e6500a 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -350,7 +350,17 @@ class SchemasHub: pass def create_schema_object(self, schema_data, *args, **kwargs): - pass + schema_type = schema_data["type"] + if schema_type in ("schema", "template", "schema_template"): + raise ValueError( + "Got unresolved schema data of type \"{}\"".format(schema_type) + ) + + klass = self._loaded_types.get(schema_type) + if not klass: + raise KeyError("Unknown type \"{}\"".format(schema_type)) + + return klass(schema_data, *args, **kwargs) def _load_schemas(self): dirpath = os.path.join( From 9ec64866a35473126a82e69bd7fde11758079e51 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:26:07 +0200 Subject: [PATCH 122/180] implemented resolving for schemas and template items --- openpype/settings/entities/lib.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 0e67e6500a..a57a391c3a 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -347,7 +347,22 @@ class SchemasHub: return copy.deepcopy(self._loaded_templates[template_name]) def resolve_schema_data(self, schema_data): - pass + schema_type = schema_data["type"] + if schema_type not in ("schema", "template", "schema_template"): + return [schema_data] + + if schema_type == "schema": + return self.resolve_schema_data( + self.get_schema(schema_data["name"]) + ) + + template_name = schema_data["name"] + template_def = self.get_template(template_name) + + filled_template = self._fill_schema_template( + schema_data, template_def + ) + return filled_template def create_schema_object(self, schema_data, *args, **kwargs): schema_type = schema_data["type"] From d4d1e177ae49e7bdbdc27821dd68d5619e64ee85 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:27:15 +0200 Subject: [PATCH 123/180] removed unused functions --- openpype/settings/entities/lib.py | 73 ------------------------------- 1 file changed, 73 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index a57a391c3a..933905a3b2 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -25,71 +25,6 @@ TEMPLATE_METADATA_KEYS = ( template_key_pattern = re.compile(r"(\{.*?[^{0]*\})") - - -def _fill_inner_schemas(schema_data, schema_collection, schema_templates): - if schema_data["type"] == "schema": - raise ValueError("First item in schema data can't be schema.") - - children_key = "children" - object_type_key = "object_type" - for item_key in (children_key, object_type_key): - children = schema_data.get(item_key) - if not children: - continue - - if object_type_key == item_key: - if not isinstance(children, dict): - continue - children = [children] - - new_children = [] - for child in children: - child_type = child["type"] - if child_type == "schema": - schema_name = child["name"] - if schema_name not in schema_collection: - if schema_name in schema_templates: - raise KeyError(( - "Schema template \"{}\" is used as `schema`" - ).format(schema_name)) - raise KeyError( - "Schema \"{}\" was not found".format(schema_name) - ) - - filled_child = _fill_inner_schemas( - schema_collection[schema_name], - schema_collection, - schema_templates - ) - - elif child_type in ("template", "schema_template"): - for filled_child in _fill_schema_template( - child, schema_collection, schema_templates - ): - new_children.append(filled_child) - continue - - else: - filled_child = _fill_inner_schemas( - child, schema_collection, schema_templates - ) - - new_children.append(filled_child) - - if item_key == object_type_key: - if len(new_children) != 1: - raise KeyError(( - "Failed to fill object type with type: {} | name {}" - ).format( - child_type, str(child.get("name")) - )) - new_children = new_children[0] - - schema_data[item_key] = new_children - return schema_data - - # TODO reimplement logic inside entities def validate_environment_groups_uniquenes( schema_data, env_groups=None, keys=None @@ -170,14 +105,6 @@ def get_gui_schema(subfolder, main_schema_name): return main_schema -def get_studio_settings_schema(): - return get_gui_schema("system_schema", "schema_main") - - -def get_project_settings_schema(): - return get_gui_schema("projects_schema", "schema_main") - - class OverrideStateItem: """Object used as item for `OverrideState` enum. From 4bc9aa821fb5e2b47a34513458c0ae957844305d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:41:53 +0200 Subject: [PATCH 124/180] add missing import --- openpype/settings/entities/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 933905a3b2..437fa05aca 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -2,6 +2,7 @@ import os import re import json import copy +import inspect from .exceptions import ( SchemaTemplateMissingKeys, From 8f00b0eb2ff88f4826c37be6513af7dc2485139f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:53:11 +0200 Subject: [PATCH 125/180] few smaller organization changes --- openpype/settings/entities/lib.py | 108 ++++++++++++++++++------------ 1 file changed, 64 insertions(+), 44 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 437fa05aca..6e1231e2f6 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -181,54 +181,26 @@ class OverrideState: class SchemasHub: - def __init__(self, schema_subfolder): - from openpype.settings import entities - - # Define known abstract classes - known_abstract_classes = ( - entities.BaseEntity, - entities.BaseItemEntity, - entities.ItemEntity, - entities.EndpointEntity, - entities.InputEntity, - entities.BaseEnumEntity - ) + def __init__(self, schema_subfolder, reset=True): + self._schema_subfolder = schema_subfolder self._loaded_types = {} - _gui_types = [] - for attr in dir(entities): - item = getattr(entities, attr) - # Filter classes - if not inspect.isclass(item): - continue + self._gui_types = tuple() - # Skip classes that do not inherit from BaseEntity - if not issubclass(item, entities.BaseEntity): - continue - - # Skip class that is abstract by design - if item in known_abstract_classes: - continue - - if inspect.isabstract(item): - # Create an object to get crash and get traceback - item() - - # Backwards compatibility - # Single entity may have multiple schema types - for schema_type in item.schema_types: - self._loaded_types[schema_type] = item - - if item.gui_type: - _gui_types.append(item) - self._gui_types = tuple(_gui_types) - - self._schema_subfolder = schema_subfolder self._crashed_on_load = {} - loaded_templates, loaded_schemas = self._load_schemas() + self._loaded_templates = {} + self._loaded_schemas = {} - self._loaded_templates = loaded_templates - self._loaded_schemas = loaded_schemas + # It doesn't make sence to reload types on each reset as they can't be + # changed + self._load_types() + + # Trigger reset + if reset: + self.reset() + + def reset(self): + self._load_schemas() @property def gui_types(self): @@ -305,7 +277,54 @@ class SchemasHub: return klass(schema_data, *args, **kwargs) + def _load_types(self): + from openpype.settings import entities + + # Define known abstract classes + known_abstract_classes = ( + entities.BaseEntity, + entities.BaseItemEntity, + entities.ItemEntity, + entities.EndpointEntity, + entities.InputEntity, + entities.BaseEnumEntity + ) + + self._loaded_types = {} + _gui_types = [] + for attr in dir(entities): + item = getattr(entities, attr) + # Filter classes + if not inspect.isclass(item): + continue + + # Skip classes that do not inherit from BaseEntity + if not issubclass(item, entities.BaseEntity): + continue + + # Skip class that is abstract by design + if item in known_abstract_classes: + continue + + if inspect.isabstract(item): + # Create an object to get crash and get traceback + item() + + # Backwards compatibility + # Single entity may have multiple schema types + for schema_type in item.schema_types: + self._loaded_types[schema_type] = item + + if item.gui_type: + _gui_types.append(item) + self._gui_types = tuple(_gui_types) + def _load_schemas(self): + # Refresh all affecting variables + self._crashed_on_load = {} + self._loaded_templates = {} + self._loaded_schemas = {} + dirpath = os.path.join( os.path.dirname(os.path.abspath(__file__)), "schemas", @@ -362,7 +381,8 @@ class SchemasHub: ) loaded_schemas[basename] = schema_data - return loaded_templates, loaded_schemas + self._loaded_templates = loaded_templates + self._loaded_schemas = loaded_schemas def _fill_schema_template(self, child_data, template_def): template_name = child_data["name"] From 568c6e5f61e9a912048f6e192a0f3b0d9b022d6e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 11:19:02 +0200 Subject: [PATCH 126/180] use shorter method names --- openpype/settings/entities/lib.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 6e1231e2f6..2d38468877 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -259,7 +259,7 @@ class SchemasHub: template_name = schema_data["name"] template_def = self.get_template(template_name) - filled_template = self._fill_schema_template( + filled_template = self._fill_template( schema_data, template_def ) return filled_template @@ -384,7 +384,7 @@ class SchemasHub: self._loaded_templates = loaded_templates self._loaded_schemas = loaded_schemas - def _fill_schema_template(self, child_data, template_def): + def _fill_template(self, child_data, template_def): template_name = child_data["name"] # Default value must be dictionary (NOT list) @@ -400,7 +400,7 @@ class SchemasHub: output = [] for single_template_data in template_data: try: - output.extend(self._fill_schema_template_data( + output.extend(self._fill_template_data( template_def, single_template_data, skip_paths )) @@ -410,7 +410,7 @@ class SchemasHub: ) return output - def _fill_schema_template_data( + def _fill_template_data( self, template, template_data, @@ -481,7 +481,7 @@ class SchemasHub: if None in _skip_paths: continue - output_item = self._fill_schema_template_data( + output_item = self._fill_template_data( item, template_data, _skip_paths, @@ -494,7 +494,7 @@ class SchemasHub: elif isinstance(template, dict): output = {} for key, value in template.items(): - output[key] = self._fill_schema_template_data( + output[key] = self._fill_template_data( value, template_data, skip_paths, From 083dd58b3937edfb6c6903aacbd7ed82ea298c90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 11:20:07 +0200 Subject: [PATCH 127/180] handle wrapper types in create object --- openpype/settings/entities/lib.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 2d38468877..e6b73b7066 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -271,6 +271,12 @@ class SchemasHub: "Got unresolved schema data of type \"{}\"".format(schema_type) ) + if schema_type in WRAPPER_TYPES: + raise ValueError(( + "Function `create_schema_object` can't create entities" + " of any wrapper type. Got type: \"{}\"" + ).format(schema_type)) + klass = self._loaded_types.get(schema_type) if not klass: raise KeyError("Unknown type \"{}\"".format(schema_type)) From 9cfd8af2bf341dfe8a603dac84c21690d793c2b5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 11:20:15 +0200 Subject: [PATCH 128/180] added brief docstrings --- openpype/settings/entities/lib.py | 143 +++++++++++++----------------- 1 file changed, 63 insertions(+), 80 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index e6b73b7066..31071a2d30 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -26,86 +26,6 @@ TEMPLATE_METADATA_KEYS = ( template_key_pattern = re.compile(r"(\{.*?[^{0]*\})") -# TODO reimplement logic inside entities -def validate_environment_groups_uniquenes( - schema_data, env_groups=None, keys=None -): - is_first = False - if env_groups is None: - is_first = True - env_groups = {} - keys = [] - - my_keys = copy.deepcopy(keys) - key = schema_data.get("key") - if key: - my_keys.append(key) - - env_group_key = schema_data.get("env_group_key") - if env_group_key: - if env_group_key not in env_groups: - env_groups[env_group_key] = [] - env_groups[env_group_key].append("/".join(my_keys)) - - children = schema_data.get("children") - if not children: - return - - for child in children: - validate_environment_groups_uniquenes( - child, env_groups, copy.deepcopy(my_keys) - ) - - if is_first: - invalid = {} - for env_group_key, key_paths in env_groups.items(): - if len(key_paths) > 1: - invalid[env_group_key] = key_paths - - if invalid: - raise SchemaDuplicatedEnvGroupKeys(invalid) - - -def validate_schema(schema_data): - validate_environment_groups_uniquenes(schema_data) - - -def get_gui_schema(subfolder, main_schema_name): - dirpath = os.path.join( - os.path.dirname(__file__), - "schemas", - subfolder - ) - loaded_schemas = {} - loaded_schema_templates = {} - for root, _, filenames in os.walk(dirpath): - for filename in filenames: - basename, ext = os.path.splitext(filename) - if ext != ".json": - continue - - filepath = os.path.join(root, filename) - with open(filepath, "r") as json_stream: - try: - schema_data = json.load(json_stream) - except Exception as exc: - raise ValueError(( - "Unable to parse JSON file {}\n{}" - ).format(filepath, str(exc))) - if isinstance(schema_data, list): - loaded_schema_templates[basename] = schema_data - else: - loaded_schemas[basename] = schema_data - - main_schema = _fill_inner_schemas( - loaded_schemas[main_schema_name], - loaded_schemas, - loaded_schema_templates - ) - validate_schema(main_schema) - return main_schema - - class OverrideStateItem: """Object used as item for `OverrideState` enum. @@ -207,6 +127,7 @@ class SchemasHub: return self._gui_types def get_schema(self, schema_name): + """Get schema definition data by it's name.""" if schema_name not in self._loaded_schemas: if schema_name in self._loaded_templates: raise KeyError(( @@ -227,6 +148,7 @@ class SchemasHub: return copy.deepcopy(self._loaded_schemas[schema_name]) def get_template(self, template_name): + """Get template definition data by it's name.""" if template_name not in self._loaded_templates: if template_name in self._loaded_schemas: raise KeyError(( @@ -247,6 +169,19 @@ class SchemasHub: return copy.deepcopy(self._loaded_templates[template_name]) def resolve_schema_data(self, schema_data): + """Resolve single item schema data as few types can be expanded. + + This is mainly for 'schema' and 'template' types. Type 'schema' does + not have entity representation and 'template' may contain more than one + output schemas. + + In other cases is retuned passed schema item in list. + + Goal is to have schema and template resolving at one place. + + Returns: + list: Resolved schema data. + """ schema_type = schema_data["type"] if schema_type not in ("schema", "template", "schema_template"): return [schema_data] @@ -265,6 +200,19 @@ class SchemasHub: return filled_template def create_schema_object(self, schema_data, *args, **kwargs): + """Create entity for passed schema data. + + Args: + schema_data(dict): Schema definition of settings entity. + + Returns: + ItemEntity: Created entity for passed schema data item. + + Raises: + ValueError: When 'schema', 'template' or any of wrapper types are + passed. + KeyError: When type of passed schema is not known. + """ schema_type = schema_data["type"] if schema_type in ("schema", "template", "schema_template"): raise ValueError( @@ -284,6 +232,8 @@ class SchemasHub: return klass(schema_data, *args, **kwargs) def _load_types(self): + """Prepare entity types for cretion of their objects.""" + from openpype.settings import entities # Define known abstract classes @@ -326,6 +276,8 @@ class SchemasHub: self._gui_types = tuple(_gui_types) def _load_schemas(self): + """Load schema definitions from json files.""" + # Refresh all affecting variables self._crashed_on_load = {} self._loaded_templates = {} @@ -391,6 +343,30 @@ class SchemasHub: self._loaded_schemas = loaded_schemas def _fill_template(self, child_data, template_def): + """Fill template based on schema definition and template definition. + + Based on `child_data` is `template_def` modified and result is + returned. + + Template definition may have defined data to fill which + should be filled with data from child data. + + Child data may contain more than one output definition of an template. + + Child data can define paths to skip. Path is full path of an item + which won't be returned. + + TODO: + Be able to handle wrapper items here. + + Args: + child_data(dict): Schema data of template item. + template_def(dict): Template definition that will be filled with + child_data. + + Returns: + list: Resolved template always returns list of schemas. + """ template_name = child_data["name"] # Default value must be dictionary (NOT list) @@ -424,6 +400,7 @@ class SchemasHub: required_keys=None, missing_keys=None ): + """Fill template values with data from schema data.""" first = False if required_keys is None: first = True @@ -547,6 +524,12 @@ class SchemasHub: return output def _pop_metadata_item(self, template_def): + """Pop template metadata from template definition. + + Template metadata may define default values if are not passed from + schema data. + """ + found_idx = None for idx, item in enumerate(template_def): if not isinstance(item, dict): From 5d651bbc618537c5750d5c55d179b6282fb84249 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 12:52:36 +0200 Subject: [PATCH 129/180] don't add ftrack family in tvpaint collect instances --- openpype/hosts/tvpaint/plugins/publish/collect_instances.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index 4468bfae40..e496b144cd 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -103,8 +103,6 @@ class CollectInstances(pyblish.api.ContextPlugin): instance.data["layers"] = copy.deepcopy( context.data["layersData"] ) - # Add ftrack family - instance.data["families"].append("ftrack") elif family == "renderLayer": instance = self.create_render_layer_instance( @@ -186,9 +184,6 @@ class CollectInstances(pyblish.api.ContextPlugin): instance_data["layers"] = group_layers - # Add ftrack family - instance_data["families"].append("ftrack") - return context.create_instance(**instance_data) def create_render_pass_instance(self, context, instance_data): From 43dca9e537eb8d9ca10c2a3dcd7f981a4165dc8f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 12:52:54 +0200 Subject: [PATCH 130/180] add tvpaint family definition in ftrack collect ftrack family --- .../defaults/project_settings/ftrack.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 03ecf024a6..88f4e1e2e7 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -259,6 +259,26 @@ "tasks": [], "add_ftrack_family": true, "advanced_filtering": [] + }, + { + "hosts": [ + "tvpaint" + ], + "families": [ + "renderPass" + ], + "tasks": [], + "add_ftrack_family": false, + "advanced_filtering": [] + }, + { + "hosts": [ + "tvpaint" + ], + "families": [], + "tasks": [], + "add_ftrack_family": true, + "advanced_filtering": [] } ] }, From 34ca28d856b71dbda31d59b2ff772d5bccd8ce66 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 24 Jun 2021 13:03:07 +0000 Subject: [PATCH 131/180] [Automated] Bump version --- CHANGELOG.md | 41 ++++++++++++++++++++++++++++------------- openpype/version.py | 2 +- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fe6de4bfa..3fe2ce33cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,44 +1,59 @@ # Changelog -## [3.2.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD) **🚀 Enhancements** +- Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739) - Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) +- PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726) - Autoupdate launcher [\#1725](https://github.com/pypeclub/OpenPype/pull/1725) - Subset template and TVPaint subset template docs [\#1717](https://github.com/pypeclub/OpenPype/pull/1717) +- Toggle Ftrack upload in StandalonePublisher [\#1708](https://github.com/pypeclub/OpenPype/pull/1708) - Overscan color extract review [\#1701](https://github.com/pypeclub/OpenPype/pull/1701) - Nuke: Prerender Frame Range by default [\#1699](https://github.com/pypeclub/OpenPype/pull/1699) - Smoother edges of color triangle [\#1695](https://github.com/pypeclub/OpenPype/pull/1695) **🐛 Bug fixes** +- Backend acre module commit update [\#1745](https://github.com/pypeclub/OpenPype/pull/1745) +- hiero: precollect instances failing when audio selected [\#1743](https://github.com/pypeclub/OpenPype/pull/1743) +- Hiero: creator instance error [\#1742](https://github.com/pypeclub/OpenPype/pull/1742) +- Nuke: fixing render creator for no selection format failing [\#1741](https://github.com/pypeclub/OpenPype/pull/1741) - Local settings UI crash on missing defaults [\#1737](https://github.com/pypeclub/OpenPype/pull/1737) +- TVPaint white background on thumbnail [\#1735](https://github.com/pypeclub/OpenPype/pull/1735) - Ftrack missing custom attribute message [\#1734](https://github.com/pypeclub/OpenPype/pull/1734) - Launcher project changes [\#1733](https://github.com/pypeclub/OpenPype/pull/1733) +- Ftrack sync status [\#1732](https://github.com/pypeclub/OpenPype/pull/1732) - TVPaint use layer name for default variant [\#1724](https://github.com/pypeclub/OpenPype/pull/1724) - Default subset template for TVPaint review and workfile families [\#1716](https://github.com/pypeclub/OpenPype/pull/1716) - Maya: Extract review hotfix [\#1714](https://github.com/pypeclub/OpenPype/pull/1714) - Settings: Imageio improving granularity [\#1711](https://github.com/pypeclub/OpenPype/pull/1711) +- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Application without executables [\#1679](https://github.com/pypeclub/OpenPype/pull/1679) -## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-18) +## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.2...2.18.3) - -**🐛 Bug fixes** - -- Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) -- Remove publish highlight icon in AE [\#1664](https://github.com/pypeclub/OpenPype/pull/1664) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...2.18.4) + +**Merged pull requests:** + +- celaction fixes [\#1754](https://github.com/pypeclub/OpenPype/pull/1754) +- celaciton: audio subset changed data structure [\#1750](https://github.com/pypeclub/OpenPype/pull/1750) + +## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-23) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.2...2.18.3) + +**🐛 Bug fixes** + +- Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727) **Merged pull requests:** -- Sync main 2.x back to 2.x develop [\#1715](https://github.com/pypeclub/OpenPype/pull/1715) - global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) -- \#683 - Validate frame range in Standalone Publisher [\#1680](https://github.com/pypeclub/OpenPype/pull/1680) -- Maya: Split model content validator [\#1654](https://github.com/pypeclub/OpenPype/pull/1654) ## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16) @@ -80,9 +95,9 @@ **🐛 Bug fixes** +- Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) - Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) -- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) - Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671) - Mac launch arguments fix [\#1660](https://github.com/pypeclub/OpenPype/pull/1660) diff --git a/openpype/version.py b/openpype/version.py index f527bd4d6e..ce6cfec003 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.2" +__version__ = "3.2.0-nightly.3" From a31524b75ced90dfc1f34f9295764347b828d557 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 16:28:08 +0200 Subject: [PATCH 132/180] try to format executable path with environments --- openpype/lib/applications.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index bed57d7022..a7dcb6dd55 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -460,6 +460,12 @@ class ApplicationExecutable: if os.path.exists(_executable): executable = _executable + # Try to format executable with environments + try: + executable = executable.format(**os.environ) + except Exception: + pass + self.executable_path = executable def __str__(self): From 29932c4766dc6711f09f947df42bb1a3703f401a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 16:54:24 +0200 Subject: [PATCH 133/180] pop others before expected keys are processed --- openpype/lib/anatomy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/lib/anatomy.py b/openpype/lib/anatomy.py index c16c6e2e99..7a4a55363c 100644 --- a/openpype/lib/anatomy.py +++ b/openpype/lib/anatomy.py @@ -733,6 +733,9 @@ class Templates: continue default_key_values[key] = templates.pop(key) + # Pop "others" key before before expected keys are processed + other_templates = templates.pop("others") or {} + keys_by_subkey = {} for sub_key, sub_value in templates.items(): key_values = {} @@ -740,7 +743,6 @@ class Templates: key_values.update(sub_value) keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) - other_templates = templates.get("others") or {} for sub_key, sub_value in other_templates.items(): if sub_key in keys_by_subkey: log.warning(( From c74216f082e2b2edd44839daf484dc7e82db73e6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 19:05:50 +0200 Subject: [PATCH 134/180] fix quotes in path for extract thumbnail in standalone publisher --- .../standalonepublisher/plugins/publish/extract_thumbnail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index 963d47956a..0792254716 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -66,7 +66,6 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): else: # Convert to jpeg if not yet full_input_path = os.path.join(thumbnail_repre["stagingDir"], file) - full_input_path = '"{}"'.format(full_input_path) self.log.info("input {}".format(full_input_path)) full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1] From 57bd695974b66f93406926000882d75f2d29794c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:05:49 +0200 Subject: [PATCH 135/180] added process_attribute_changes where previous logic happened --- .../event_push_frame_values_to_task.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index c0b3137455..613566f25d 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -131,6 +131,18 @@ class PushFrameValuesToTaskEvent(BaseEvent): name_low = object_type["name"].lower() object_types_by_name[name_low] = object_type + if interesting_data: + self.process_attribute_changes( + session, object_types_by_name, + interesting_data, changed_keys_by_object_id, + interest_entity_types, interest_attributes + ) + + def process_attribute_changes( + self, session, object_types_by_name, + interesting_data, changed_keys_by_object_id, + interest_entity_types, interest_attributes + ): # Prepare task object id task_object_id = object_types_by_name["task"]["id"] @@ -216,13 +228,13 @@ class PushFrameValuesToTaskEvent(BaseEvent): task_entity_ids.add(task_id) parent_id_by_task_id[task_id] = task_entity["parent_id"] - self.finalize( + self.finalize_attribute_changes( session, interesting_data, changed_keys, attrs_by_obj_id, hier_attrs, task_entity_ids, parent_id_by_task_id ) - def finalize( + def finalize_attribute_changes( self, session, interesting_data, changed_keys, attrs_by_obj_id, hier_attrs, task_entity_ids, parent_id_by_task_id From 143d1205eedbe5266f02d6ad6a9fecb227919dce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:06:08 +0200 Subject: [PATCH 136/180] collect also task changes if parent_id has changed --- .../event_push_frame_values_to_task.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 613566f25d..8c45efa91b 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -55,10 +55,6 @@ class PushFrameValuesToTaskEvent(BaseEvent): if entity_info.get("entityType") != "task": continue - # Skip `Task` entity type - if entity_info["entity_type"].lower() == "task": - continue - # Care only about changes of status changes = entity_info.get("changes") if not changes: @@ -74,6 +70,14 @@ class PushFrameValuesToTaskEvent(BaseEvent): if project_id is None: continue + # Skip `Task` entity type if parent didn't change + if entity_info["entity_type"].lower() == "task": + if ( + "parent_id" not in changes + or changes["parent_id"]["new"] is None + ): + continue + if project_id not in entities_info_by_project_id: entities_info_by_project_id[project_id] = [] entities_info_by_project_id[project_id].append(entity_info) From a4e84611febe1192d98a21b022a2090764864fad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:06:30 +0200 Subject: [PATCH 137/180] separate task parent changes and value changes --- .../event_push_frame_values_to_task.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 8c45efa91b..f0675bdbc8 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -121,11 +121,21 @@ class PushFrameValuesToTaskEvent(BaseEvent): )) return + # Separate value changes and task parent changes + _entities_info = [] + task_parent_changes = [] + for entity_info in entities_info: + if entity_info["entity_type"].lower() == "task": + task_parent_changes.append(entity_info) + else: + _entities_info.append(entity_info) + entities_info = _entities_info + # Filter entities info with changes interesting_data, changed_keys_by_object_id = self.filter_changes( session, event, entities_info, interest_attributes ) - if not interesting_data: + if not interesting_data and not task_parent_changes: return # Prepare object types From 18297588a59349ed41305c9741a7dcde868d5d97 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:07:15 +0200 Subject: [PATCH 138/180] convert attributes and types to set --- .../event_handlers_server/event_push_frame_values_to_task.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index f0675bdbc8..443fdafd71 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -121,6 +121,9 @@ class PushFrameValuesToTaskEvent(BaseEvent): )) return + interest_attributes = set(self.interest_attributes) + interest_entity_types = set(self.interest_entity_types) + # Separate value changes and task parent changes _entities_info = [] task_parent_changes = [] From 70b91afe199f38652078c1081b36657e9ea8dcba Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:15:45 +0200 Subject: [PATCH 139/180] implemented query_custom_attributes for querying custom attribute values from ftrack database --- openpype/modules/ftrack/lib/__init__.py | 4 +- .../modules/ftrack/lib/custom_attributes.py | 57 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/lib/__init__.py b/openpype/modules/ftrack/lib/__init__.py index ce6d5284b6..9dc2d67279 100644 --- a/openpype/modules/ftrack/lib/__init__.py +++ b/openpype/modules/ftrack/lib/__init__.py @@ -13,7 +13,8 @@ from .custom_attributes import ( default_custom_attributes_definition, app_definitions_from_app_manager, tool_definitions_from_app_manager, - get_openpype_attr + get_openpype_attr, + query_custom_attributes ) from . import avalon_sync @@ -37,6 +38,7 @@ __all__ = ( "app_definitions_from_app_manager", "tool_definitions_from_app_manager", "get_openpype_attr", + "query_custom_attributes", "avalon_sync", diff --git a/openpype/modules/ftrack/lib/custom_attributes.py b/openpype/modules/ftrack/lib/custom_attributes.py index f6b82c90b1..53facd4ab2 100644 --- a/openpype/modules/ftrack/lib/custom_attributes.py +++ b/openpype/modules/ftrack/lib/custom_attributes.py @@ -81,3 +81,60 @@ def get_openpype_attr(session, split_hierarchical=True, query_keys=None): return custom_attributes, hier_custom_attributes return custom_attributes + + +def join_query_keys(keys): + """Helper to join keys to query.""" + return ",".join(["\"{}\"".format(key) for key in keys]) + + +def query_custom_attributes(session, conf_ids, entity_ids, table_name=None): + """Query custom attribute values from ftrack database. + + Using ftrack call method result may differ based on used table name and + version of ftrack server. + + Args: + session(ftrack_api.Session): Connected ftrack session. + conf_id(list, set, tuple): Configuration(attribute) ids which are + queried. + entity_ids(list, set, tuple): Entity ids for which are values queried. + table_name(str): Table nam from which values are queried. Not + recommended to change until you know what it means. + """ + output = [] + # Just skip + if not conf_ids or not entity_ids: + return output + + if table_name is None: + table_name = "ContextCustomAttributeValue" + + # Prepare values to query + attributes_joined = join_query_keys(conf_ids) + attributes_len = len(conf_ids) + + # Query values in chunks + chunk_size = int(5000 / attributes_len) + # Make sure entity_ids is `list` for chunk selection + entity_ids = list(entity_ids) + for idx in range(0, len(entity_ids), chunk_size): + entity_ids_joined = join_query_keys( + entity_ids[idx:idx + chunk_size] + ) + + call_expr = [{ + "action": "query", + "expression": ( + "select value, entity_id from {}" + " where entity_id in ({}) and configuration_id in ({})" + ).format(table_name, entity_ids_joined, attributes_joined) + }] + if hasattr(session, "call"): + [result] = session.call(call_expr) + else: + [result] = session._call(call_expr) + + for item in result["data"]: + output.append(item) + return output From 0894f272de642c76836e6c90bf01e21c97ceb05a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:28:30 +0200 Subject: [PATCH 140/180] implemented _commit_changes for easier access --- .../event_push_frame_values_to_task.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 443fdafd71..d654b26114 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -155,6 +155,61 @@ class PushFrameValuesToTaskEvent(BaseEvent): interest_entity_types, interest_attributes ) + def _commit_changes(self, session, changes): + uncommited_changes = False + for idx, item in enumerate(changes): + new_value = item["new_value"] + attr_id = item["attr_id"] + entity_id = item["entity_id"] + attr_key = item["attr_key"] + + entity_key = collections.OrderedDict() + entity_key["configuration_id"] = attr_id + entity_key["entity_id"] = entity_id + self._cached_changes.append({ + "attr_key": attr_key, + "entity_id": entity_id, + "value": new_value, + "time": datetime.datetime.now() + }) + if new_value is None: + op = ftrack_api.operation.DeleteEntityOperation( + "CustomAttributeValue", + entity_key + ) + else: + op = ftrack_api.operation.UpdateEntityOperation( + "ContextCustomAttributeValue", + entity_key, + "value", + ftrack_api.symbol.NOT_SET, + new_value + ) + + session.recorded_operations.push(op) + self.log.info(( + "Changing Custom Attribute \"{}\" to value" + " \"{}\" on entity: {}" + ).format(attr_key, new_value, entity_id)) + + if (idx + 1) % 20 == 0: + uncommited_changes = False + try: + session.commit() + except Exception: + session.rollback() + self.log.warning( + "Changing of values failed.", exc_info=True + ) + else: + uncommited_changes = True + if uncommited_changes: + try: + session.commit() + except Exception: + session.rollback() + self.log.warning("Changing of values failed.", exc_info=True) + def process_attribute_changes( self, session, object_types_by_name, interesting_data, changed_keys_by_object_id, From 3251401da30a216887fa8fe351fb85fcf8d45d86 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:29:08 +0200 Subject: [PATCH 141/180] use _commit_changes in current implementation --- .../event_push_frame_values_to_task.py | 37 +++---------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index d654b26114..c292d856da 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -332,6 +332,7 @@ class PushFrameValuesToTaskEvent(BaseEvent): session, attr_ids, entity_ids, task_entity_ids, hier_attrs ) + changes = [] for entity_id, current_values in current_values_by_id.items(): parent_id = parent_id_by_task_id.get(entity_id) if not parent_id: @@ -356,39 +357,13 @@ class PushFrameValuesToTaskEvent(BaseEvent): if new_value == old_value: continue - entity_key = collections.OrderedDict() - entity_key["configuration_id"] = attr_id - entity_key["entity_id"] = entity_id - self._cached_changes.append({ - "attr_key": attr_key, + changes.append({ + "new_value": new_value, + "attr_id": attr_id, "entity_id": entity_id, - "value": new_value, - "time": datetime.datetime.now() + "attr_key": attr_key }) - if new_value is None: - op = ftrack_api.operation.DeleteEntityOperation( - "CustomAttributeValue", - entity_key - ) - else: - op = ftrack_api.operation.UpdateEntityOperation( - "ContextCustomAttributeValue", - entity_key, - "value", - ftrack_api.symbol.NOT_SET, - new_value - ) - - session.recorded_operations.push(op) - self.log.info(( - "Changing Custom Attribute \"{}\" to value" - " \"{}\" on entity: {}" - ).format(attr_key, new_value, entity_id)) - try: - session.commit() - except Exception: - session.rollback() - self.log.warning("Changing of values failed.", exc_info=True) + self._commit_changes(session, changes) def filter_changes( self, session, event, entities_info, interest_attributes From 10f0604173d6bfeef2ced3375e97b951fc881fe5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:32:09 +0200 Subject: [PATCH 142/180] implemented task parent changes handling --- .../event_push_frame_values_to_task.py | 177 +++++++++++++++++- 1 file changed, 176 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index c292d856da..84f26dc57a 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -2,7 +2,10 @@ import collections import datetime import ftrack_api -from openpype.modules.ftrack.lib import BaseEvent +from openpype.modules.ftrack.lib import ( + BaseEvent, + query_custom_attributes +) class PushFrameValuesToTaskEvent(BaseEvent): @@ -155,6 +158,178 @@ class PushFrameValuesToTaskEvent(BaseEvent): interest_entity_types, interest_attributes ) + if task_parent_changes: + self.process_task_parent_change( + session, object_types_by_name, task_parent_changes, + interest_entity_types, interest_attributes + ) + + def process_task_parent_change( + self, session, object_types_by_name, task_parent_changes, + interest_entity_types, interest_attributes + ): + task_ids = set() + matching_parent_ids = set() + whole_hierarchy_ids = set() + parent_id_by_entity_id = {} + for entity_info in task_parent_changes: + parents = entity_info.get("parents") or [] + # Ignore entities with less parents than 2 + # NOTE entity itself is also part of "parents" value + if len(parents) < 2: + continue + + parent_info = parents[1] + if parent_info["entity_type"] not in interest_entity_types: + continue + + task_ids.add(entity_info["entityId"]) + matching_parent_ids.add(parent_info["entityId"]) + + prev_id = None + for item in parents: + item_id = item["entityId"] + whole_hierarchy_ids.add(item_id) + + if prev_id is None: + prev_id = item_id + continue + + parent_id_by_entity_id[prev_id] = item_id + if item["entityType"] == "show": + break + prev_id = item_id + + if not matching_parent_ids: + return + + entities = session.query( + "select object_type_id from TypedContext where id in ({})".format( + self.join_query_keys(matching_parent_ids) + ) + ) + object_type_ids = set() + for entity in entities: + object_type_ids.add(entity["object_type_id"]) + + # Prepare task object id + task_object_id = object_types_by_name["task"]["id"] + object_type_ids.add(task_object_id) + + attrs_by_obj_id, hier_attrs = self.attrs_configurations( + session, object_type_ids, interest_attributes + ) + + task_attrs = attrs_by_obj_id.get(task_object_id) + if not task_attrs: + return + + for key in interest_attributes: + if key not in hier_attrs: + task_attrs.pop(key, None) + + elif key not in task_attrs: + hier_attrs.pop(key) + + if not task_attrs: + return + + attr_key_by_id = {} + nonhier_id_by_key = {} + hier_attr_ids = [] + for key, attr_id in hier_attrs.items(): + attr_key_by_id[attr_id] = key + hier_attr_ids.append(attr_id) + + conf_ids = list(hier_attr_ids) + for key, attr_id in task_attrs.items(): + attr_key_by_id[attr_id] = key + nonhier_id_by_key[key] = attr_id + conf_ids.append(attr_id) + + result = query_custom_attributes( + session, conf_ids, whole_hierarchy_ids + ) + hier_values_by_entity_id = { + entity_id: {} + for entity_id in whole_hierarchy_ids + } + values_by_entity_id = { + entity_id: { + attr_id: None + for attr_id in conf_ids + } + for entity_id in whole_hierarchy_ids + } + for item in result: + attr_id = item["configuration_id"] + entity_id = item["entity_id"] + value = item["value"] + + values_by_entity_id[entity_id][attr_id] = value + + if attr_id in hier_attr_ids and value is not None: + hier_values_by_entity_id[entity_id][attr_id] = value + + for task_id in tuple(task_ids): + for attr_id in hier_attr_ids: + entity_ids = [] + value = None + entity_id = task_id + while value is None: + entity_value = hier_values_by_entity_id[entity_id] + if attr_id in entity_value: + value = entity_value[attr_id] + if value is None: + break + + if value is None: + entity_ids.append(entity_id) + + entity_id = parent_id_by_entity_id.get(entity_id) + if entity_id is None: + break + + for entity_id in entity_ids: + hier_values_by_entity_id[entity_id][attr_id] = value + + changes = [] + for task_id in tuple(task_ids): + parent_id = parent_id_by_entity_id[task_id] + for attr_id in hier_attr_ids: + attr_key = attr_key_by_id[attr_id] + nonhier_id = nonhier_id_by_key[attr_key] + + # Real value of hierarchical attribute on parent + # - If is none then should be unset + real_parent_value = values_by_entity_id[parent_id][attr_id] + # Current hierarchical value of a task + # - Will be compared to real parent value + hier_value = hier_values_by_entity_id[task_id][attr_id] + + # Parent value that can be inherited from it's parent entity + parent_value = hier_values_by_entity_id[parent_id][attr_id] + # Task value of nonhierarchical custom attribute + nonhier_value = values_by_entity_id[task_id][nonhier_id] + + if real_parent_value != hier_value: + changes.append({ + "new_value": real_parent_value, + "attr_id": attr_id, + "entity_id": task_id, + "attr_key": attr_key + }) + + if parent_value != nonhier_value: + changes.append({ + "new_value": parent_value, + "attr_id": nonhier_id, + "entity_id": task_id, + "attr_key": attr_key + }) + + self._commit_changes(session, changes) + def _commit_changes(self, session, changes): uncommited_changes = False for idx, item in enumerate(changes): From 9fa01ac76ea1b74e76c8cc99195af9e05cefbd97 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:43:55 +0200 Subject: [PATCH 143/180] object type ids preparation is at one place --- .../event_push_frame_values_to_task.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 84f26dc57a..3ee148b3ed 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -208,13 +208,13 @@ class PushFrameValuesToTaskEvent(BaseEvent): self.join_query_keys(matching_parent_ids) ) ) - object_type_ids = set() - for entity in entities: - object_type_ids.add(entity["object_type_id"]) # Prepare task object id task_object_id = object_types_by_name["task"]["id"] + object_type_ids = set() object_type_ids.add(task_object_id) + for entity in entities: + object_type_ids.add(entity["object_type_id"]) attrs_by_obj_id, hier_attrs = self.attrs_configurations( session, object_type_ids, interest_attributes From 877b5b853548099a3fad09551e681dc7423d1959 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:44:13 +0200 Subject: [PATCH 144/180] added few comments --- .../event_push_frame_values_to_task.py | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 3ee148b3ed..d1393796ff 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -168,24 +168,42 @@ class PushFrameValuesToTaskEvent(BaseEvent): self, session, object_types_by_name, task_parent_changes, interest_entity_types, interest_attributes ): + """Push custom attribute values if task parent has changed. + + Parent is changed if task is created or if is moved under different + entity. We don't care about all task changes only about those that + have it's parent in interest types (from settings). + + Tasks hierarchical value should be unset or set based on parents + real hierarchical value and non hierarchical custom attribute value + should be set to hierarchical value. + """ + # Store task ids which were created or moved under parent with entity + # type defined in settings (interest_entity_types). task_ids = set() + # Store parent ids of matching task ids matching_parent_ids = set() + # Store all entity ids of all entities to be able query hierarchical + # values. whole_hierarchy_ids = set() + # Store parent id of each entity id parent_id_by_entity_id = {} for entity_info in task_parent_changes: - parents = entity_info.get("parents") or [] # Ignore entities with less parents than 2 # NOTE entity itself is also part of "parents" value + parents = entity_info.get("parents") or [] if len(parents) < 2: continue parent_info = parents[1] + # Check if parent has entity type we care about. if parent_info["entity_type"] not in interest_entity_types: continue task_ids.add(entity_info["entityId"]) matching_parent_ids.add(parent_info["entityId"]) + # Store whole hierarchi of task entity prev_id = None for item in parents: item_id = item["entityId"] @@ -200,9 +218,12 @@ class PushFrameValuesToTaskEvent(BaseEvent): break prev_id = item_id + # Just skip if nothing is interesting for our settings if not matching_parent_ids: return + # Query object type ids of parent ids for custom attribute + # definitions query entities = session.query( "select object_type_id from TypedContext where id in ({})".format( self.join_query_keys(matching_parent_ids) @@ -211,6 +232,8 @@ class PushFrameValuesToTaskEvent(BaseEvent): # Prepare task object id task_object_id = object_types_by_name["task"]["id"] + + # All object ids for which we're querying custom attribute definitions object_type_ids = set() object_type_ids.add(task_object_id) for entity in entities: @@ -220,10 +243,13 @@ class PushFrameValuesToTaskEvent(BaseEvent): session, object_type_ids, interest_attributes ) + # Skip if all task attributes are not available task_attrs = attrs_by_obj_id.get(task_object_id) if not task_attrs: return + # Skip attributes that is not in both hierarchical and nonhierarchical + # TODO be able to push values if hierarchical is available for key in interest_attributes: if key not in hier_attrs: task_attrs.pop(key, None) @@ -231,9 +257,11 @@ class PushFrameValuesToTaskEvent(BaseEvent): elif key not in task_attrs: hier_attrs.pop(key) + # Skip if nothing remained if not task_attrs: return + # Do some preparations for custom attribute values query attr_key_by_id = {} nonhier_id_by_key = {} hier_attr_ids = [] @@ -247,13 +275,21 @@ class PushFrameValuesToTaskEvent(BaseEvent): nonhier_id_by_key[key] = attr_id conf_ids.append(attr_id) + # Query custom attribute values + # - result does not contain values for all entities only result of + # query callback to ftrack server result = query_custom_attributes( session, conf_ids, whole_hierarchy_ids ) + + # Prepare variables where result will be stored + # - hierachical values should not contain attribute with value by + # default hier_values_by_entity_id = { entity_id: {} for entity_id in whole_hierarchy_ids } + # - real values of custom attributes values_by_entity_id = { entity_id: { attr_id: None @@ -271,6 +307,10 @@ class PushFrameValuesToTaskEvent(BaseEvent): if attr_id in hier_attr_ids and value is not None: hier_values_by_entity_id[entity_id][attr_id] = value + # Prepare values for all task entities + # - going through all parents and storing first value value + # - store None to those that are already known that do not have set + # value at all for task_id in tuple(task_ids): for attr_id in hier_attr_ids: entity_ids = [] @@ -293,6 +333,7 @@ class PushFrameValuesToTaskEvent(BaseEvent): for entity_id in entity_ids: hier_values_by_entity_id[entity_id][attr_id] = value + # Prepare changes to commit changes = [] for task_id in tuple(task_ids): parent_id = parent_id_by_entity_id[task_id] From 1be4c4fc7f899f2aecec50530f609072bd12edc2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:45:33 +0200 Subject: [PATCH 145/180] added some more comments --- .../event_handlers_server/event_push_frame_values_to_task.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index d1393796ff..1d64174188 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -151,6 +151,9 @@ class PushFrameValuesToTaskEvent(BaseEvent): name_low = object_type["name"].lower() object_types_by_name[name_low] = object_type + # NOTE it would be nice to check if `interesting_data` do not contain + # value changs of tasks that were created or moved + # - it is a complex way how to find out if interesting_data: self.process_attribute_changes( session, object_types_by_name, From 5cda53abffe2eed91c2bf5c09d8b3a5e559ab702 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:58:30 +0200 Subject: [PATCH 146/180] keep refresh button available even if not in dev mode --- openpype/tools/settings/settings/categories.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 34ab4c464a..392c749211 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -183,6 +183,12 @@ class SettingsCategoryWidget(QtWidgets.QWidget): footer_widget = QtWidgets.QWidget(configurations_widget) footer_layout = QtWidgets.QHBoxLayout(footer_widget) + refresh_icon = qtawesome.icon("fa.refresh", color="white") + refresh_btn = QtWidgets.QPushButton(footer_widget) + refresh_btn.setIcon(refresh_icon) + + footer_layout.addWidget(refresh_btn, 0) + if self.user_role == "developer": self._add_developer_ui(footer_layout) @@ -205,8 +211,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget): main_layout.addWidget(configurations_widget, 1) save_btn.clicked.connect(self._save) + refresh_btn.clicked.connect(self._on_refresh) self.save_btn = save_btn + self.refresh_btn = refresh_btn self.require_restart_label = require_restart_label self.scroll_widget = scroll_widget self.content_layout = content_layout @@ -220,10 +228,6 @@ class SettingsCategoryWidget(QtWidgets.QWidget): return def _add_developer_ui(self, footer_layout): - refresh_icon = qtawesome.icon("fa.refresh", color="white") - refresh_button = QtWidgets.QPushButton() - refresh_button.setIcon(refresh_icon) - modify_defaults_widget = QtWidgets.QWidget() modify_defaults_checkbox = QtWidgets.QCheckBox(modify_defaults_widget) modify_defaults_checkbox.setChecked(self._hide_studio_overrides) @@ -235,10 +239,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget): modify_defaults_layout.addWidget(label_widget) modify_defaults_layout.addWidget(modify_defaults_checkbox) - footer_layout.addWidget(refresh_button, 0) footer_layout.addWidget(modify_defaults_widget, 0) - refresh_button.clicked.connect(self._on_refresh) modify_defaults_checkbox.stateChanged.connect( self._on_modify_defaults ) From d64e1d249d4eb3042dc89890553d0e536c06c2de Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 11:20:55 +0200 Subject: [PATCH 147/180] width of workfile toos widget have better sizes to display content --- openpype/tools/workfiles/app.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index c79e55a143..d567e26d74 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -944,10 +944,8 @@ class Window(QtWidgets.QMainWindow): split_widget.addWidget(tasks_widget) split_widget.addWidget(files_widget) split_widget.addWidget(side_panel) - split_widget.setStretchFactor(0, 1) - split_widget.setStretchFactor(1, 1) - split_widget.setStretchFactor(2, 3) - split_widget.setStretchFactor(3, 1) + split_widget.setSizes([255, 160, 455, 175]) + body_layout.addWidget(split_widget) # Add top margin for tasks to align it visually with files as @@ -976,7 +974,7 @@ class Window(QtWidgets.QMainWindow): # Force focus on the open button by default, required for Houdini. files_widget.btn_open.setFocus() - self.resize(1000, 600) + self.resize(1200, 600) def keyPressEvent(self, event): """Custom keyPressEvent. From 7b273f15497b0980c6a9cf1717882b6ea933188e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 12:37:26 +0200 Subject: [PATCH 148/180] fix project specific environment variables to work as expected --- openpype/lib/applications.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index a7dcb6dd55..9866400928 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1159,6 +1159,9 @@ def prepare_host_environments(data, implementation_envs=True): def apply_project_environments_value(project_name, env, project_settings=None): """Apply project specific environments on passed environments. + The enviornments are applied on passed `env` argument value so it is not + required to apply changes back. + Args: project_name (str): Name of project for which environemnts should be received. @@ -1167,6 +1170,9 @@ def apply_project_environments_value(project_name, env, project_settings=None): project_settings (dict): Project settings for passed project name. Optional if project settings are already prepared. + Returns: + dict: Passed env values with applied project environments. + Raises: KeyError: If project settings do not contain keys for project specific environments. @@ -1177,10 +1183,9 @@ def apply_project_environments_value(project_name, env, project_settings=None): project_settings = get_project_settings(project_name) env_value = project_settings["global"]["project_environments"] - if not env_value: - return env - parsed = acre.parse(env_value) - return _merge_env(parsed, env) + if env_value: + env.update(_merge_env(acre.parse(env_value), env)) + return env def prepare_context_environments(data): @@ -1209,9 +1214,8 @@ def prepare_context_environments(data): # Load project specific environments project_name = project_doc["name"] - data["env"] = apply_project_environments_value( - project_name, data["env"] - ) + # Apply project specific environments on current env value + apply_project_environments_value(project_name, data["env"]) app = data["app"] workdir_data = get_workdir_data( From 251d3add0162aa3f883daa53fe366158103e843a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:51:47 +0200 Subject: [PATCH 149/180] added root_key as abstract property to BaseEntity --- openpype/settings/entities/base_entity.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index c6bff1ff47..147bd613d1 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -279,6 +279,11 @@ class BaseItemEntity(BaseEntity): self, "Dynamic entity can't require restart." ) + @abstractproperty + def root_key(self): + """Root is represented as this dictionary key.""" + pass + @abstractmethod def set_override_state(self, state): """Set override state and trigger it on children. From ac36919e2642e27db2fce2042aed61b27f04a061 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:52:03 +0200 Subject: [PATCH 150/180] added root_key to both root entities --- openpype/settings/entities/root_entities.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 401d3980c9..c637da8f76 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -491,6 +491,8 @@ class SystemSettings(RootEntity): schema_data (dict): Pass schema data to entity. This is for development and debugging purposes. """ + root_key = SYSTEM_SETTINGS_KEY + def __init__( self, set_studio_state=True, reset=True, schema_data=None ): @@ -600,6 +602,8 @@ class ProjectSettings(RootEntity): schema_data (dict): Pass schema data to entity. This is for development and debugging purposes. """ + root_key = PROJECT_SETTINGS_KEY + def __init__( self, project_name=None, From 81bebe03c675c1a4274a963b5d6653e47667560f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:52:16 +0200 Subject: [PATCH 151/180] implemented root_key propery for rest of entities --- openpype/settings/entities/base_entity.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 147bd613d1..1b0dd372fa 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -871,6 +871,10 @@ class ItemEntity(BaseItemEntity): """Call save on root item.""" self.root_item.save() + @property + def root_key(self): + return self.root_item.root_key + def schema_validations(self): if not self.label and self.use_label_wrap: reason = ( From 5c9016c676364097fc3d2de8450bbe95eb45b3d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:52:31 +0200 Subject: [PATCH 152/180] implemented get_entity_from_path for system settings --- openpype/settings/entities/root_entities.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index c637da8f76..5ed78fd401 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -189,11 +189,10 @@ class RootEntity(BaseItemEntity): if not KEY_REGEX.match(key): raise InvalidKeySymbols(self.path, key) + @abstractmethod def get_entity_from_path(self, path): - """Return system settings entity.""" - raise NotImplementedError(( - "Method `get_entity_from_path` not available for \"{}\"" - ).format(self.__class__.__name__)) + """Return entity matching passed path.""" + pass def create_schema_object(self, schema_data, *args, **kwargs): """Create entity by entered schema data. @@ -505,6 +504,18 @@ class SystemSettings(RootEntity): if set_studio_state: self.set_studio_state() + def get_entity_from_path(self, path): + """Return system settings entity.""" + path_parts = path.split("/") + first_part = path_parts[0] + output = self + if first_part == self.root_key: + path_parts.pop(0) + + for path_part in path_parts: + output = output[path_part] + return output + def _reset_values(self): default_value = get_default_settings()[SYSTEM_SETTINGS_KEY] for key, child_obj in self.non_gui_children.items(): From ee72605f5855737b6d7905dd1ec80a393b7caf48 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:53:30 +0200 Subject: [PATCH 153/180] implemented copy action in settings --- openpype/tools/settings/settings/base.py | 55 ++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 03f920b7dc..c0ef968247 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -1,3 +1,5 @@ +import json + from Qt import QtWidgets, QtGui, QtCore from openpype.tools.settings import CHILD_OFFSET from .widgets import ExpandingWidget @@ -125,6 +127,58 @@ class BaseWidget(QtWidgets.QWidget): actions_mapping[action] = remove_from_project_override menu.addAction(action) + def _copy_value_action(self, menu, actions_mapping): + def copy_value(): + mime_data = QtCore.QMimeData() + + if self.entity.is_dynamic_item or self.entity.is_in_dynamic_item: + entity_path = None + else: + entity_path = "/".join( + [self.entity.root_key, self.entity.path] + ) + + value = self.entity.value + # Copy for settings tool + settings_data = { + "root_key": self.entity.root_key, + "value": value, + "path": entity_path + } + settings_encoded_data = QtCore.QByteArray() + settings_stream = QtCore.QDataStream( + settings_encoded_data, QtCore.QIODevice.WriteOnly + ) + settings_stream.writeQString(json.dumps(settings_data)) + mime_data.setData( + "application/copy_settings_value", settings_encoded_data + ) + + # Copy as json + json_encoded_data = None + if isinstance(value, (dict, list)): + json_encoded_data = QtCore.QByteArray() + json_stream = QtCore.QDataStream( + json_encoded_data, QtCore.QIODevice.WriteOnly + ) + json_stream.writeQString(json.dumps(value)) + + mime_data.setData("application/json", json_encoded_data) + + # Copy as text + if json_encoded_data is None: + # Store value as string + mime_data.setText(str(value)) + else: + # Store data as json string + mime_data.setText(json.dumps(value, indent=4)) + + QtWidgets.QApplication.clipboard().setMimeData(mime_data) + + action = QtWidgets.QAction("Copy") + actions_mapping[action] = copy_value + menu.addAction(action) + def show_actions_menu(self, event=None): if event and event.button() != QtCore.Qt.RightButton: return @@ -143,6 +197,7 @@ class BaseWidget(QtWidgets.QWidget): self._remove_from_studio_default_action(menu, actions_mapping) self._add_to_project_override_action(menu, actions_mapping) self._remove_from_project_override_action(menu, actions_mapping) + self._copy_value_action(menu, actions_mapping) if not actions_mapping: action = QtWidgets.QAction("< No action >") From 8eeeda11e7b4058063bd14ab2789e4b9bfad08d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:53:42 +0200 Subject: [PATCH 154/180] implemented base of paste value actions --- openpype/tools/settings/settings/base.py | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index c0ef968247..8882cc0c46 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -179,6 +179,46 @@ class BaseWidget(QtWidgets.QWidget): actions_mapping[action] = copy_value menu.addAction(action) + def _paste_value_action(self, menu, actions_mapping): + mime_data = QtWidgets.QApplication.clipboard().mimeData() + mime_value = mime_data.data("application/copy_settings_value") + if not mime_value: + return + + settings_stream = QtCore.QDataStream( + mime_value, QtCore.QIODevice.ReadOnly + ) + mime_data_value_str = settings_stream.readQString() + mime_data_value = json.loads(mime_data_value_str) + + value = mime_data_value["value"] + path = mime_data_value["path"] + root_key = mime_data_value["root_key"] + + def paste_value(): + try: + self.entity.set(value) + except Exception: + # TODO show dialog + print("Failed") + import sys + import traceback + + traceback.print_exception(*sys.exc_info()) + + def paste_value_to_path(): + entity = self.entity.get_entity_from_path(path) + entity.set(value) + + if path and root_key == self.entity.root_key: + action = QtWidgets.QAction("Paste to same entity") + actions_mapping[action] = paste_value_to_path + menu.addAction(action) + + action = QtWidgets.QAction("Paste") + actions_mapping[action] = paste_value + menu.addAction(action) + def show_actions_menu(self, event=None): if event and event.button() != QtCore.Qt.RightButton: return @@ -198,6 +238,7 @@ class BaseWidget(QtWidgets.QWidget): self._add_to_project_override_action(menu, actions_mapping) self._remove_from_project_override_action(menu, actions_mapping) self._copy_value_action(menu, actions_mapping) + self._paste_value_action(menu, actions_mapping) if not actions_mapping: action = QtWidgets.QAction("< No action >") From 2e8df3e2d0339cd1ebf6469af800e26d004527db Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:08:29 +0200 Subject: [PATCH 155/180] show dialog if paste crashes --- openpype/tools/settings/settings/base.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 8882cc0c46..09a36cd99b 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -199,12 +199,14 @@ class BaseWidget(QtWidgets.QWidget): try: self.entity.set(value) except Exception: - # TODO show dialog - print("Failed") - import sys - import traceback - - traceback.print_exception(*sys.exc_info()) + dialog = QtWidgets.QMessageBox(self) + dialog.setWindowTitle("Value does not match settings schema") + dialog.setIcon(QtWidgets.QMessageBox.Warning) + dialog.setText(( + "Pasted value does not seem to match schema of destination" + " settings entity." + )) + dialog.exec_() def paste_value_to_path(): entity = self.entity.get_entity_from_path(path) From 455acfaae9cee59b6a864b1c7fae0732ba55faef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:10:43 +0200 Subject: [PATCH 156/180] paste_value_to_path is simplified --- openpype/tools/settings/settings/base.py | 29 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 09a36cd99b..40a6562fd0 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -195,6 +195,26 @@ class BaseWidget(QtWidgets.QWidget): path = mime_data_value["path"] root_key = mime_data_value["root_key"] + # Try to find matching entity to be able paste values to same spot + # - entity can't by dynamic or in dynamic item + # - must be in same root entity as source copy + # Can't copy system settings <-> project settings + matching_entity = None + if path and root_key == self.entity.root_key: + try: + matching_entity = self.entity.get_entity_from_path(path) + except Exception: + pass + + # Paste value to matchin entity + def paste_value_to_path(): + matching_entity.set(value) + + if matching_entity is not None: + action = QtWidgets.QAction("Paste to same entity", menu) + actions_mapping[action] = paste_value_to_path + menu.addAction(action) + def paste_value(): try: self.entity.set(value) @@ -208,15 +228,6 @@ class BaseWidget(QtWidgets.QWidget): )) dialog.exec_() - def paste_value_to_path(): - entity = self.entity.get_entity_from_path(path) - entity.set(value) - - if path and root_key == self.entity.root_key: - action = QtWidgets.QAction("Paste to same entity") - actions_mapping[action] = paste_value_to_path - menu.addAction(action) - action = QtWidgets.QAction("Paste") actions_mapping[action] = paste_value menu.addAction(action) From 302c6b44b107085616f7522ab91c94fac5da249f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:11:18 +0200 Subject: [PATCH 157/180] copy/paste actions are separated with separator in menu --- openpype/tools/settings/settings/base.py | 31 +++++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 40a6562fd0..8986780f31 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -127,7 +127,7 @@ class BaseWidget(QtWidgets.QWidget): actions_mapping[action] = remove_from_project_override menu.addAction(action) - def _copy_value_action(self, menu, actions_mapping): + def _copy_value_actions(self, menu): def copy_value(): mime_data = QtCore.QMimeData() @@ -175,15 +175,15 @@ class BaseWidget(QtWidgets.QWidget): QtWidgets.QApplication.clipboard().setMimeData(mime_data) - action = QtWidgets.QAction("Copy") - actions_mapping[action] = copy_value - menu.addAction(action) + action = QtWidgets.QAction("Copy", menu) + return [(action, copy_value)] - def _paste_value_action(self, menu, actions_mapping): + def _paste_value_actions(self, menu): + output = [] mime_data = QtWidgets.QApplication.clipboard().mimeData() mime_value = mime_data.data("application/copy_settings_value") if not mime_value: - return + return output settings_stream = QtCore.QDataStream( mime_value, QtCore.QIODevice.ReadOnly @@ -212,8 +212,7 @@ class BaseWidget(QtWidgets.QWidget): if matching_entity is not None: action = QtWidgets.QAction("Paste to same entity", menu) - actions_mapping[action] = paste_value_to_path - menu.addAction(action) + output.append((action, paste_value_to_path)) def paste_value(): try: @@ -229,8 +228,9 @@ class BaseWidget(QtWidgets.QWidget): dialog.exec_() action = QtWidgets.QAction("Paste") - actions_mapping[action] = paste_value - menu.addAction(action) + output.append((action, paste_value)) + + return output def show_actions_menu(self, event=None): if event and event.button() != QtCore.Qt.RightButton: @@ -250,8 +250,15 @@ class BaseWidget(QtWidgets.QWidget): self._remove_from_studio_default_action(menu, actions_mapping) self._add_to_project_override_action(menu, actions_mapping) self._remove_from_project_override_action(menu, actions_mapping) - self._copy_value_action(menu, actions_mapping) - self._paste_value_action(menu, actions_mapping) + + ui_actions = [] + ui_actions.extend(self._copy_value_actions(menu)) + ui_actions.extend(self._paste_value_actions(menu)) + if ui_actions: + menu.addSeparator() + for action, callback in ui_actions: + menu.addAction(action) + actions_mapping[action] = callback if not actions_mapping: action = QtWidgets.QAction("< No action >") From daf12bb9736ca9c7fae575033c756deb14121b8a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:11:23 +0200 Subject: [PATCH 158/180] added comment --- openpype/tools/settings/settings/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 8986780f31..ac040f9e25 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -214,6 +214,7 @@ class BaseWidget(QtWidgets.QWidget): action = QtWidgets.QAction("Paste to same entity", menu) output.append((action, paste_value_to_path)) + # Simple paste value method def paste_value(): try: self.entity.set(value) From 9c02ecb35aadb1ecae8fb12abf183d4491e839c6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:14:56 +0200 Subject: [PATCH 159/180] make both paste secure with dialog poopup --- openpype/tools/settings/settings/base.py | 27 ++++++++++++++---------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index ac040f9e25..620628d1d2 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -180,8 +180,10 @@ class BaseWidget(QtWidgets.QWidget): def _paste_value_actions(self, menu): output = [] + # Allow paste of value only if were copied from this UI mime_data = QtWidgets.QApplication.clipboard().mimeData() mime_value = mime_data.data("application/copy_settings_value") + # Skip if there is nothing to do if not mime_value: return output @@ -206,18 +208,9 @@ class BaseWidget(QtWidgets.QWidget): except Exception: pass - # Paste value to matchin entity - def paste_value_to_path(): - matching_entity.set(value) - - if matching_entity is not None: - action = QtWidgets.QAction("Paste to same entity", menu) - output.append((action, paste_value_to_path)) - - # Simple paste value method - def paste_value(): + def _set_entity_value(_entity, _value): try: - self.entity.set(value) + _entity.set(_value) except Exception: dialog = QtWidgets.QMessageBox(self) dialog.setWindowTitle("Value does not match settings schema") @@ -228,6 +221,18 @@ class BaseWidget(QtWidgets.QWidget): )) dialog.exec_() + # Paste value to matchin entity + def paste_value_to_path(): + _set_entity_value(matching_entity, value) + + if matching_entity is not None: + action = QtWidgets.QAction("Paste to same entity", menu) + output.append((action, paste_value_to_path)) + + # Simple paste value method + def paste_value(): + _set_entity_value(self.entity, value) + action = QtWidgets.QAction("Paste") output.append((action, paste_value)) From 82ac355a3823cd6be24da8e42411748a7c2ee52f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:23:39 +0200 Subject: [PATCH 160/180] moved simple Paste before special Paste --- openpype/tools/settings/settings/base.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 620628d1d2..543551b6a2 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -221,6 +221,13 @@ class BaseWidget(QtWidgets.QWidget): )) dialog.exec_() + # Simple paste value method + def paste_value(): + _set_entity_value(self.entity, value) + + action = QtWidgets.QAction("Paste", menu) + output.append((action, paste_value)) + # Paste value to matchin entity def paste_value_to_path(): _set_entity_value(matching_entity, value) @@ -229,13 +236,6 @@ class BaseWidget(QtWidgets.QWidget): action = QtWidgets.QAction("Paste to same entity", menu) output.append((action, paste_value_to_path)) - # Simple paste value method - def paste_value(): - _set_entity_value(self.entity, value) - - action = QtWidgets.QAction("Paste") - output.append((action, paste_value)) - return output def show_actions_menu(self, event=None): From c86ef80326af322ff9a83f4eb5718497d1fdc471 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:23:54 +0200 Subject: [PATCH 161/180] changed label to "Paste to same place" --- openpype/tools/settings/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 543551b6a2..eb5f82ab9a 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -233,7 +233,7 @@ class BaseWidget(QtWidgets.QWidget): _set_entity_value(matching_entity, value) if matching_entity is not None: - action = QtWidgets.QAction("Paste to same entity", menu) + action = QtWidgets.QAction("Paste to same place", menu) output.append((action, paste_value_to_path)) return output From 9dfbd8d7f2d75ad78201e045c9300eaadc647c79 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 26 Jun 2021 03:40:30 +0000 Subject: [PATCH 162/180] [Automated] Bump version --- CHANGELOG.md | 10 ++++++++-- openpype/version.py | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fe2ce33cb..96b90cd53e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # Changelog -## [3.2.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD) **🚀 Enhancements** +- Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757) - Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739) - Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) - PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726) @@ -31,9 +32,12 @@ - Default subset template for TVPaint review and workfile families [\#1716](https://github.com/pypeclub/OpenPype/pull/1716) - Maya: Extract review hotfix [\#1714](https://github.com/pypeclub/OpenPype/pull/1714) - Settings: Imageio improving granularity [\#1711](https://github.com/pypeclub/OpenPype/pull/1711) -- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Application without executables [\#1679](https://github.com/pypeclub/OpenPype/pull/1679) +**Merged pull requests:** + +- TVPaint ftrack family [\#1755](https://github.com/pypeclub/OpenPype/pull/1755) + ## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...2.18.4) @@ -98,6 +102,7 @@ - Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) - Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) +- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) - Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671) - Mac launch arguments fix [\#1660](https://github.com/pypeclub/OpenPype/pull/1660) @@ -111,6 +116,7 @@ - update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697) - Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686) +- Use poetry to build / publish OpenPype wheel [\#1636](https://github.com/pypeclub/OpenPype/pull/1636) # Changelog diff --git a/openpype/version.py b/openpype/version.py index ce6cfec003..fcd3b2afca 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.3" +__version__ = "3.2.0-nightly.4" From 02def9660b4c30937b70127e7cdd725fca767944 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 28 Jun 2021 09:56:08 +0200 Subject: [PATCH 163/180] Fix - single file files are str only, cast it to list to count properly --- .../plugins/publish/validate_frame_ranges.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py index e3086fb638..943cb73b98 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py @@ -43,7 +43,10 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): self.log.warning("Cannot check for extension {}".format(ext)) return - frames = len(instance.data.get("representations", [None])[0]["files"]) + files = instance.data.get("representations", [None])[0]["files"] + if isinstance(files, str): + files = [files] + frames = len(files) err_msg = "Frame duration from DB:'{}' ". format(int(duration)) +\ " doesn't match number of files:'{}'".format(frames) +\ From 50cf8f96143eb44612723b9f0e3057be290df9ba Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 14:16:38 +0200 Subject: [PATCH 164/180] fix object attributes error --- .../event_handlers_server/event_push_frame_values_to_task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 1d64174188..81719258e1 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -124,8 +124,8 @@ class PushFrameValuesToTaskEvent(BaseEvent): )) return - interest_attributes = set(self.interest_attributes) - interest_entity_types = set(self.interest_entity_types) + interest_attributes = set(interest_attributes) + interest_entity_types = set(interest_entity_types) # Separate value changes and task parent changes _entities_info = [] From 2f6f259c64da037574a7858f4ef3ec9efb35cb60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jun 2021 20:51:56 +0000 Subject: [PATCH 165/180] Bump prismjs from 1.23.0 to 1.24.0 in /website Bumps [prismjs](https://github.com/PrismJS/prism) from 1.23.0 to 1.24.0. - [Release notes](https://github.com/PrismJS/prism/releases) - [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md) - [Commits](https://github.com/PrismJS/prism/compare/v1.23.0...v1.24.0) --- updated-dependencies: - dependency-name: prismjs dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/yarn.lock | 39 +++------------------------------------ 1 file changed, 3 insertions(+), 36 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index 2d5ec103d4..a63bf37731 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -2667,15 +2667,6 @@ cli-boxes@^2.2.1: resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== -clipboard@^2.0.0: - version "2.0.8" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.8.tgz#ffc6c103dd2967a83005f3f61976aa4655a4cdba" - integrity sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ== - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -3310,11 +3301,6 @@ del@^6.0.0: rimraf "^3.0.2" slash "^3.0.0" -delegate@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" - integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== - depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -4224,13 +4210,6 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" -good-listener@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" - integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA= - dependencies: - delegate "^3.1.2" - got@^9.6.0: version "9.6.0" resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" @@ -6615,11 +6594,9 @@ prism-react-renderer@^1.1.1: integrity sha512-GHqzxLYImx1iKN1jJURcuRoA/0ygCcNhfGw1IT8nPIMzarmKQ3Nc+JcG0gi8JXQzuh0C5ShE4npMIoqNin40hg== prismjs@^1.23.0: - version "1.23.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" - integrity sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA== - optionalDependencies: - clipboard "^2.0.0" + version "1.24.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.0.tgz#0409c30068a6c52c89ef7f1089b3ca4de56be2ac" + integrity sha512-SqV5GRsNqnzCL8k5dfAjCNhUrF3pR0A9lTDSCUZeh/LIshheXJEaP0hwLz2t4XHivd2J/v2HR+gRnigzeKe3cQ== process-nextick-args@~2.0.0: version "2.0.1" @@ -7390,11 +7367,6 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -select@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" - integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= - selfsigned@^1.10.8: version "1.10.8" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30" @@ -8016,11 +7988,6 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= -tiny-emitter@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - tiny-invariant@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" From 42fbc1f633ebec2e04a3243b07a9341e55672424 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 11:33:44 +0200 Subject: [PATCH 166/180] decode ffprobe output before logging --- openpype/lib/vendor_bin_utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index 3b923cb608..a8c75c20da 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -89,8 +89,13 @@ def ffprobe_streams(path_to_file, logger=None): popen_stdout, popen_stderr = popen.communicate() if popen_stdout: - logger.debug("ffprobe stdout: {}".format(popen_stdout)) + logger.debug("FFprobe stdout:\n{}".format( + popen_stdout.decode("utf-8") + )) if popen_stderr: - logger.debug("ffprobe stderr: {}".format(popen_stderr)) + logger.warning("FFprobe stderr:\n{}".format( + popen_stderr.decode("utf-8") + )) + return json.loads(popen_stdout)["streams"] From 404a659b40411bcb174b114a10012d89b20ed222 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 11:48:47 +0200 Subject: [PATCH 167/180] find first stream with resolution when reading ffprobe streams --- openpype/plugins/publish/extract_review.py | 30 +++++++++++++++---- .../plugins/publish/extract_review_slate.py | 20 +++++++++++-- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 42fb2a8f93..de54b554e3 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -975,11 +975,31 @@ class ExtractReview(pyblish.api.InstancePlugin): # NOTE Skipped using instance's resolution full_input_path_single_file = temp_data["full_input_path_single_file"] - input_data = ffprobe_streams( - full_input_path_single_file, self.log - )[0] - input_width = int(input_data["width"]) - input_height = int(input_data["height"]) + try: + streams = ffprobe_streams( + full_input_path_single_file, self.log + ) + except Exception: + raise AssertionError(( + "FFprobe couldn't read information about input file: \"{}\"" + ).format(full_input_path_single_file)) + + # Try to find first stream with defined 'width' and 'height' + # - this is to avoid order of streams where audio can be as first + # - there may be a better way (checking `codec_type`?) + input_width = None + input_height = None + for stream in streams: + if "width" in stream and "height" in stream: + input_width = int(stream["width"]) + input_height = int(stream["height"]) + break + + # Raise exception of any stream didn't define input resolution + if input_width is None: + raise AssertionError(( + "FFprobe couldn't read resolution from input file: \"{}\"" + ).format(full_input_path_single_file)) # NOTE Setting only one of `width` or `heigth` is not allowed # - settings value can't have None but has value of 0 diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index fb36a930fb..6908f044d1 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -26,9 +26,23 @@ class ExtractReviewSlate(openpype.api.Extractor): slate_path = inst_data.get("slateFrame") ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") - slate_stream = openpype.lib.ffprobe_streams(slate_path, self.log)[0] - slate_width = slate_stream["width"] - slate_height = slate_stream["height"] + slate_streams = openpype.lib.ffprobe_streams(slate_path, self.log) + # Try to find first stream with defined 'width' and 'height' + # - this is to avoid order of streams where audio can be as first + # - there may be a better way (checking `codec_type`?)+ + slate_width = None + slate_height = None + for slate_stream in slate_streams: + if "width" in slate_stream and "height" in slate_stream: + slate_width = int(slate_stream["width"]) + slate_height = int(slate_stream["height"]) + break + + # Raise exception of any stream didn't define input resolution + if slate_width is None: + raise AssertionError(( + "FFprobe couldn't read resolution from input file: \"{}\"" + ).format(slate_path)) if "reviewToWidth" in inst_data: use_legacy_code = True From 6d571b2e17eaeeea65d301b56b317881e08e1ab8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 11:49:22 +0200 Subject: [PATCH 168/180] find first stream that is not an audio when defying profile and pix_fmt --- .../plugins/publish/extract_review_slate.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 6908f044d1..2b07d7db74 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -323,16 +323,29 @@ class ExtractReviewSlate(openpype.api.Extractor): ) return codec_args - codec_name = streams[0].get("codec_name") + # Try to find first stream that is not an audio + no_audio_stream = None + for stream in streams: + if stream.get("codec_type") != "audio": + no_audio_stream = stream + break + + if no_audio_stream is None: + self.log.warning(( + "Couldn't find stream that is not an audio from file \"{}\"" + ).format(full_input_path)) + return codec_args + + codec_name = no_audio_stream.get("codec_name") if codec_name: codec_args.append("-codec:v {}".format(codec_name)) - profile_name = streams[0].get("profile") + profile_name = no_audio_stream.get("profile") if profile_name: profile_name = profile_name.replace(" ", "_").lower() codec_args.append("-profile:v {}".format(profile_name)) - pix_fmt = streams[0].get("pix_fmt") + pix_fmt = no_audio_stream.get("pix_fmt") if pix_fmt: codec_args.append("-pix_fmt {}".format(pix_fmt)) return codec_args From 13aec9cb572ece21c79548a9d7748cf3002cef3c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 13:47:37 +0200 Subject: [PATCH 169/180] "use_python_2" is optional in application settings --- openpype/lib/applications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 9866400928..1eac7ea776 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -179,7 +179,7 @@ class Application: if group.enabled: enabled = data.get("enabled", True) self.enabled = enabled - self.use_python_2 = data["use_python_2"] + self.use_python_2 = data.get("use_python_2", False) self.label = data.get("variant_label") or name self.full_name = "/".join((group.name, name)) From 7daf1d3d0b41b9525aca9bbda39cd75923dc8898 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:04:37 +0200 Subject: [PATCH 170/180] do replacement only if replacement is still string --- openpype/settings/entities/lib.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 05f4ea64f8..cf0da29978 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -145,7 +145,10 @@ def _fill_schema_template_data( # Only replace the key in string template = template.replace(replacement_string, value) - output = template.replace("__dbcb__", "{").replace("__decb__", "}") + if isinstance(template, STRING_TYPE): + output = template.replace("__dbcb__", "{").replace("__decb__", "}") + else: + output = template else: output = template From cc85f669b8d4b9737959295cb23ee75913a0bfbc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:04:59 +0200 Subject: [PATCH 171/180] removed use_python_2 from blender --- openpype/settings/defaults/system_settings/applications.json | 3 --- .../schemas/system_schema/host_settings/schema_blender.json | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 72cd010cf2..583597df32 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -807,7 +807,6 @@ "environment": {}, "variants": { "2-83": { - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\Blender Foundation\\Blender 2.83\\blender.exe" @@ -829,7 +828,6 @@ "environment": {} }, "2-90": { - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\Blender Foundation\\Blender 2.90\\blender.exe" @@ -851,7 +849,6 @@ "environment": {} }, "2-91": { - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\Blender Foundation\\Blender 2.91\\blender.exe" diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_blender.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_blender.json index 0a6c8ca035..27ead6e6da 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_blender.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_blender.json @@ -30,7 +30,8 @@ "children": [ { "type": "schema_template", - "name": "template_host_variant_items" + "name": "template_host_variant_items", + "skip_paths": ["use_python_2"] } ] } From 2fd7bc4c1305ac4e1968a54391418615749004cf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:05:37 +0200 Subject: [PATCH 172/180] template_host_variant have ability to modify skip_paths on template_host_variant_items --- .../host_settings/template_host_variant.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant.json b/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant.json index 33cde3d216..96a936c27b 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant.json @@ -1,4 +1,9 @@ [ + { + "__default_values__": { + "variant_skip_paths": null + } + }, { "type": "dict", "key": "{app_variant}", @@ -19,7 +24,8 @@ }, { "type": "schema_template", - "name": "template_host_variant_items" + "name": "template_host_variant_items", + "skip_paths": "{variant_skip_paths}" } ] } From 5bcff7230e5b786e1a4989ab934759fcc8447e72 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:05:54 +0200 Subject: [PATCH 173/180] removed use_python_2 from harmony --- .../settings/defaults/system_settings/applications.json | 2 -- .../schemas/system_schema/host_settings/schema_harmony.json | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 583597df32..b7eece8a6a 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -888,7 +888,6 @@ "20": { "enabled": true, "variant_label": "20", - "use_python_2": false, "executables": { "windows": [], "darwin": [], @@ -904,7 +903,6 @@ "17": { "enabled": true, "variant_label": "17", - "use_python_2": false, "executables": { "windows": [], "darwin": [ diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_harmony.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_harmony.json index 083885a53b..c122b8930b 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_harmony.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_harmony.json @@ -29,11 +29,13 @@ "template_data": [ { "app_variant_label": "20", - "app_variant": "20" + "app_variant": "20", + "variant_skip_paths": ["use_python_2"] }, { "app_variant_label": "17", - "app_variant": "17" + "app_variant": "17", + "variant_skip_paths": ["use_python_2"] } ] } From e98d1a99ef40f384b90ede51d0221b996b84f247 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:06:08 +0200 Subject: [PATCH 174/180] remove use_python_2 from tvpaint --- openpype/settings/defaults/system_settings/applications.json | 2 -- .../schemas/system_schema/host_settings/schema_tvpaint.json | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index b7eece8a6a..ed09ec2815 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -927,7 +927,6 @@ "environment": {}, "variants": { "animation_11-64bits": { - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\TVPaint Developpement\\TVPaint Animation 11 (64bits)\\TVPaint Animation 11 (64bits).exe" @@ -943,7 +942,6 @@ "environment": {} }, "animation_11-32bits": { - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files (x86)\\TVPaint Developpement\\TVPaint Animation 11 (32bits)\\TVPaint Animation 11 (32bits).exe" diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json index c39e6f7a30..ff57d767c4 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json @@ -30,7 +30,8 @@ "children": [ { "type": "schema_template", - "name": "template_host_variant_items" + "name": "template_host_variant_items", + "skip_paths": ["use_python_2"] } ] } From a95d0ac3ffc3ef0167b6d5d32ca8e413798fedfd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:06:30 +0200 Subject: [PATCH 175/180] remove use_python_2 from photoshop --- .../settings/defaults/system_settings/applications.json | 2 -- .../system_schema/host_settings/schema_photoshop.json | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index ed09ec2815..d8c9e171fc 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -975,7 +975,6 @@ "2020": { "enabled": true, "variant_label": "2020", - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\Adobe\\Adobe Photoshop 2020\\Photoshop.exe" @@ -993,7 +992,6 @@ "2021": { "enabled": true, "variant_label": "2021", - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\Adobe\\Adobe Photoshop 2021\\Photoshop.exe" diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_photoshop.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_photoshop.json index 9c21166b63..7bcd89c650 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_photoshop.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_photoshop.json @@ -29,11 +29,13 @@ "template_data": [ { "app_variant_label": "2020", - "app_variant": "2020" + "app_variant": "2020", + "variant_skip_paths": ["use_python_2"] }, { "app_variant_label": "2021", - "app_variant": "2021" + "app_variant": "2021", + "variant_skip_paths": ["use_python_2"] } ] } From 5b60e7f172899639b2c286d6bc99aa6835174523 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:06:40 +0200 Subject: [PATCH 176/180] remove use_python_2 from aftereffects --- .../settings/defaults/system_settings/applications.json | 2 -- .../system_schema/host_settings/schema_aftereffects.json | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index d8c9e171fc..224f9dc318 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1021,7 +1021,6 @@ "2020": { "enabled": true, "variant_label": "2020", - "use_python_2": false, "executables": { "windows": [ "" @@ -1039,7 +1038,6 @@ "2021": { "enabled": true, "variant_label": "2021", - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\Adobe\\Adobe After Effects 2021\\Support Files\\AfterFX.exe" diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json index afadf48173..6c36a9bb8a 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json @@ -29,11 +29,13 @@ "template_data": [ { "app_variant_label": "2020", - "app_variant": "2020" + "app_variant": "2020", + "variant_skip_paths": ["use_python_2"] }, { "app_variant_label": "2021", - "app_variant": "2021" + "app_variant": "2021", + "variant_skip_paths": ["use_python_2"] } ] } From cd1bf6d302e2f369b2a3cca0a21b64a85c8d9a79 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:10:23 +0200 Subject: [PATCH 177/180] added better condition for full replacements --- openpype/settings/entities/lib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index cf0da29978..92510e04d5 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -129,6 +129,7 @@ def _fill_schema_template_data( elif isinstance(template, STRING_TYPE): # TODO find much better way how to handle filling template data template = template.replace("{{", "__dbcb__").replace("}}", "__decb__") + full_replacement = False for replacement_string in template_key_pattern.findall(template): key = str(replacement_string[1:-1]) required_keys.add(key) @@ -141,11 +142,12 @@ def _fill_schema_template_data( # Replace the value with value from templates data # - with this is possible to set value with different type template = value + full_replacement = True else: # Only replace the key in string template = template.replace(replacement_string, value) - if isinstance(template, STRING_TYPE): + if not full_replacement: output = template.replace("__dbcb__", "{").replace("__decb__", "}") else: output = template From d91f47084860a8c40b89f37506689a457bc99e2a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:11:26 +0200 Subject: [PATCH 178/180] handle full value replacement in template --- openpype/settings/entities/lib.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 31071a2d30..d747c3e85e 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -497,6 +497,7 @@ class SchemasHub: .replace("{{", "__dbcb__") .replace("}}", "__decb__") ) + full_replacement = False for replacement_string in template_key_pattern.findall(template): key = str(replacement_string[1:-1]) required_keys.add(key) @@ -509,11 +510,19 @@ class SchemasHub: # Replace the value with value from templates data # - with this is possible to set value with different type template = value + full_replacement = True else: # Only replace the key in string template = template.replace(replacement_string, value) - output = template.replace("__dbcb__", "{").replace("__decb__", "}") + if not full_replacement: + output = ( + template + .replace("__dbcb__", "{") + .replace("__decb__", "}") + ) + else: + output = template else: output = template From f2fb9885db8359e070fe882e3ca1888a0de33d3b Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 30 Jun 2021 03:42:24 +0000 Subject: [PATCH 179/180] [Automated] Bump version --- CHANGELOG.md | 17 +++++++++++------ openpype/version.py | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96b90cd53e..0b69a8e2d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,14 @@ # Changelog -## [3.2.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD) **🚀 Enhancements** +- Settings UI copy/paste [\#1769](https://github.com/pypeclub/OpenPype/pull/1769) +- Workfile tool widths [\#1766](https://github.com/pypeclub/OpenPype/pull/1766) +- Push hierarchical attributes care about task parent changes [\#1763](https://github.com/pypeclub/OpenPype/pull/1763) - Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757) - Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739) - Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) @@ -19,6 +22,11 @@ **🐛 Bug fixes** +- FFprobe streams order [\#1775](https://github.com/pypeclub/OpenPype/pull/1775) +- Project specific environments [\#1767](https://github.com/pypeclub/OpenPype/pull/1767) +- Settings UI with refresh button [\#1764](https://github.com/pypeclub/OpenPype/pull/1764) +- Standalone publisher thumbnail extractor fix [\#1761](https://github.com/pypeclub/OpenPype/pull/1761) +- Anatomy others templates don't cause crash [\#1758](https://github.com/pypeclub/OpenPype/pull/1758) - Backend acre module commit update [\#1745](https://github.com/pypeclub/OpenPype/pull/1745) - hiero: precollect instances failing when audio selected [\#1743](https://github.com/pypeclub/OpenPype/pull/1743) - Hiero: creator instance error [\#1742](https://github.com/pypeclub/OpenPype/pull/1742) @@ -36,7 +44,9 @@ **Merged pull requests:** +- Bump prismjs from 1.23.0 to 1.24.0 in /website [\#1773](https://github.com/pypeclub/OpenPype/pull/1773) - TVPaint ftrack family [\#1755](https://github.com/pypeclub/OpenPype/pull/1755) +- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) ## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24) @@ -55,10 +65,6 @@ - Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727) -**Merged pull requests:** - -- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) - ## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.1.0...2.18.2) @@ -116,7 +122,6 @@ - update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697) - Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686) -- Use poetry to build / publish OpenPype wheel [\#1636](https://github.com/pypeclub/OpenPype/pull/1636) # Changelog diff --git a/openpype/version.py b/openpype/version.py index fcd3b2afca..0371d5f4e3 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.4" +__version__ = "3.2.0-nightly.5" From b21f827790727433393397c2931d21efb099594a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 30 Jun 2021 11:22:43 +0200 Subject: [PATCH 180/180] added few docstrings --- openpype/settings/entities/lib.py | 53 ++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index d747c3e85e..42a08232b9 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -127,7 +127,16 @@ class SchemasHub: return self._gui_types def get_schema(self, schema_name): - """Get schema definition data by it's name.""" + """Get schema definition data by it's name. + + Returns: + dict: Copy of schema loaded from json files. + + Raises: + KeyError: When schema name is stored in loaded templates or json + file was not possible to parse or when schema name was not + found. + """ if schema_name not in self._loaded_schemas: if schema_name in self._loaded_templates: raise KeyError(( @@ -148,7 +157,16 @@ class SchemasHub: return copy.deepcopy(self._loaded_schemas[schema_name]) def get_template(self, template_name): - """Get template definition data by it's name.""" + """Get template definition data by it's name. + + Returns: + list: Copy of template items loaded from json files. + + Raises: + KeyError: When template name is stored in loaded schemas or json + file was not possible to parse or when template name was not + found. + """ if template_name not in self._loaded_templates: if template_name in self._loaded_schemas: raise KeyError(( @@ -232,7 +250,16 @@ class SchemasHub: return klass(schema_data, *args, **kwargs) def _load_types(self): - """Prepare entity types for cretion of their objects.""" + """Prepare entity types for cretion of their objects. + + Currently all classes in `openpype.settings.entities` that inherited + from `BaseEntity` are stored as loaded types. GUI types are stored to + separated attribute to not mess up api access of entities. + + TODOs: + Add more dynamic way how to add custom types from anywhere and + better handling of abstract classes. Skipping them is dangerous. + """ from openpype.settings import entities @@ -400,7 +427,25 @@ class SchemasHub: required_keys=None, missing_keys=None ): - """Fill template values with data from schema data.""" + """Fill template values with data from schema data. + + Template has more abilities than schemas. It is expected that template + will be used at multiple places (but may not). Schema represents + exactly one entity and it's children but template may represent more + entities. + + Template can have "keys to fill" from their definition. Some key may be + required and some may be optional because template has their default + values defined. + + Template also have ability to "skip paths" which means to skip entities + from it's content. A template can be used across multiple places with + different requirements. + + Raises: + SchemaTemplateMissingKeys: When fill data do not contain all + required keys for template. + """ first = False if required_keys is None: first = True