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(): 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 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) 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__) 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: diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index e1e24af3ea..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 @@ -1158,9 +1170,10 @@ class ExtractReview(pyblish.api.InstancePlugin): "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" ]) @@ -1707,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 @@ -1767,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 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" } ] } diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 4351f18a60..037fa63a29 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -56,6 +56,12 @@ ] }, "overscan_crop": "", + "overscan_color": [ + 0, + 0, + 0, + 255 + ], "width": 0, "height": 0, "bg_color": [ @@ -226,6 +232,17 @@ ], "tasks": [], "template": "{family}{Task}_{Render_layer}_{Render_pass}" + }, + { + "families": [ + "review", + "workfile" + ], + "hosts": [ + "tvpaint" + ], + "tasks": [], + "template": "{family}{Task}" } ] }, 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/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 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" } ] } 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", 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..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." 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"] ): diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 22b08d7d15..0e8caeb278 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,16 @@ 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 - - # Initialize - self.refresh() + self.refresh_timer = refresh_timer # 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 +67,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() @@ -69,27 +88,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): diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index af749814b7..a6d34bbe9d 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) @@ -106,21 +108,40 @@ class ProjectsPanel(QtWidgets.QWidget): flick.activateOn(view) model = ProjectModel(self.dbcon) model.hide_invisible = True - model.refresh() view.setModel(model) 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""" @@ -276,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) @@ -344,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 @@ -353,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) @@ -367,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() @@ -402,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 @@ -412,7 +451,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): 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_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index eda8c618f1..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) @@ -80,7 +51,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 +106,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() 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): diff --git a/openpype/widgets/color_widgets/color_triangle.py b/openpype/widgets/color_widgets/color_triangle.py index d4db175d84..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) @@ -724,27 +740,37 @@ 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 + # 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))) + r += rdelta + 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()) 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) diff --git a/website/docs/admin_hosts_tvpaint.md b/website/docs/admin_hosts_tvpaint.md new file mode 100644 index 0000000000..a99cd19010 --- /dev/null +++ b/website/docs/admin_hosts_tvpaint.md @@ -0,0 +1,30 @@ +--- +id: admin_hosts_tvpaint +title: TVPaint +sidebar_label: TVPaint +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## 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). + +### [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). + +:::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](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. 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. -