diff --git a/openpype/hosts/harmony/api/TB_sceneOpened.js b/openpype/hosts/harmony/api/TB_sceneOpened.js index 6a403fa65e..e7cd555332 100644 --- a/openpype/hosts/harmony/api/TB_sceneOpened.js +++ b/openpype/hosts/harmony/api/TB_sceneOpened.js @@ -35,7 +35,11 @@ function Client() { self.pack = function(num) { var ascii=''; for (var i = 3; i >= 0; i--) { - ascii += String.fromCharCode((num >> (8 * i)) & 255); + var hex = ((num >> (8 * i)) & 255).toString(16); + if (hex.length < 2){ + ascii += "0"; + } + ascii += hex; } return ascii; }; @@ -279,19 +283,22 @@ function Client() { }; self._send = function(message) { - var data = new QByteArray(); - var outstr = new QDataStream(data, QIODevice.WriteOnly); - outstr.writeInt(0); - data.append('UTF-8'); - outstr.device().seek(0); - outstr.writeInt(data.size() - 4); - var codec = QTextCodec.codecForUtfText(data); - var msg = codec.fromUnicode(message); - var l = msg.size(); - var coded = new QByteArray('AH').append(self.pack(l)); - coded = coded.append(msg); - self.socket.write(new QByteArray(coded)); - self.logDebug('Sent.'); + /** Harmony 21.1 doesn't have QDataStream anymore. + + This means we aren't able to write bytes into QByteArray so we had + modify how content lenght is sent do the server. + Content lenght is sent as string of 8 char convertible into integer + (instead of 0x00000001[4 bytes] > "000000001"[8 bytes]) */ + var codec_name = new QByteArray().append("UTF-8"); + + var codec = QTextCodec.codecForName(codec_name); + var msg = codec.fromUnicode(message); + var l = msg.size(); + var header = new QByteArray().append('AH').append(self.pack(l)); + var coded = msg.prepend(header); + + self.socket.write(coded); + self.logDebug('Sent.'); }; self.waitForLock = function() { @@ -351,7 +358,14 @@ function start() { app.avalonClient = new Client(); app.avalonClient.socket.connectToHost(host, port); } - var menuBar = QApplication.activeWindow().menuBar(); + var mainWindow = null; + var widgets = QApplication.topLevelWidgets(); + for (var i = 0 ; i < widgets.length; i++) { + if (widgets[i] instanceof QMainWindow){ + mainWindow = widgets[i]; + } + } + var menuBar = mainWindow.menuBar(); var actions = menuBar.actions(); app.avalonMenu = null; diff --git a/openpype/hosts/harmony/api/server.py b/openpype/hosts/harmony/api/server.py index 88cfe54521..0de359ec61 100644 --- a/openpype/hosts/harmony/api/server.py +++ b/openpype/hosts/harmony/api/server.py @@ -88,21 +88,25 @@ class Server(threading.Thread): """ current_time = time.time() while True: - + self.log.info("wait ttt") # Receive the data in small chunks and retransmit it request = None - header = self.connection.recv(6) + header = self.connection.recv(10) if len(header) == 0: # null data received, socket is closing. self.log.info(f"[{self.timestamp()}] Connection closing.") break + if header[0:2] != b"AH": self.log.error("INVALID HEADER") - length = struct.unpack(">I", header[2:])[0] + content_length_str = header[2:].decode() + + length = int(content_length_str, 16) data = self.connection.recv(length) while (len(data) < length): # we didn't received everything in first try, lets wait for # all data. + self.log.info("loop") time.sleep(0.1) if self.connection is None: self.log.error(f"[{self.timestamp()}] " @@ -113,7 +117,7 @@ class Server(threading.Thread): break data += self.connection.recv(length - len(data)) - + self.log.debug("data:: {} {}".format(data, type(data))) self.received += data.decode("utf-8") pretty = self._pretty(self.received) self.log.debug( diff --git a/openpype/hosts/hiero/api/__init__.py b/openpype/hosts/hiero/api/__init__.py index f3c32b268c..fc2d017f04 100644 --- a/openpype/hosts/hiero/api/__init__.py +++ b/openpype/hosts/hiero/api/__init__.py @@ -27,6 +27,7 @@ from .lib import ( get_track_items, get_current_project, get_current_sequence, + get_timeline_selection, get_current_track, get_track_item_pype_tag, set_track_item_pype_tag, @@ -80,6 +81,7 @@ __all__ = [ "get_track_items", "get_current_project", "get_current_sequence", + "get_timeline_selection", "get_current_track", "get_track_item_pype_tag", "set_track_item_pype_tag", diff --git a/openpype/hosts/hiero/api/events.py b/openpype/hosts/hiero/api/events.py index 7fab3edfc8..59fd278a81 100644 --- a/openpype/hosts/hiero/api/events.py +++ b/openpype/hosts/hiero/api/events.py @@ -109,8 +109,9 @@ def register_hiero_events(): # hiero.core.events.registerInterest("kShutdown", shutDown) # hiero.core.events.registerInterest("kStartup", startupCompleted) - hiero.core.events.registerInterest( - ("kSelectionChanged", "kTimeline"), selection_changed_timeline) + # INFO: was disabled because it was slowing down timeline operations + # hiero.core.events.registerInterest( + # ("kSelectionChanged", "kTimeline"), selection_changed_timeline) # workfiles try: diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 2a4cd03b76..d19cefd2da 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -1,6 +1,8 @@ """ Host specific functions where host api is connected """ + +from copy import deepcopy import os import re import sys @@ -89,13 +91,19 @@ def get_current_sequence(name=None, new=False): if not sequence: # if nothing found create new with input name sequence = get_current_sequence(name, True) - elif not name and not new: + else: # if name is none and new is False then return current open sequence sequence = hiero.ui.activeSequence() return sequence +def get_timeline_selection(): + active_sequence = hiero.ui.activeSequence() + timeline_editor = hiero.ui.getTimelineEditor(active_sequence) + return list(timeline_editor.selection()) + + def get_current_track(sequence, name, audio=False): """ Get current track in context of active project. @@ -118,7 +126,7 @@ def get_current_track(sequence, name, audio=False): # get track by name track = None for _track in tracks: - if _track.name() in name: + if _track.name() == name: track = _track if not track: @@ -126,13 +134,14 @@ def get_current_track(sequence, name, audio=False): track = hiero.core.VideoTrack(name) else: track = hiero.core.AudioTrack(name) + sequence.addTrack(track) return track def get_track_items( - selected=False, + selection=False, sequence_name=None, track_item_name=None, track_name=None, @@ -143,7 +152,7 @@ def get_track_items( """Get all available current timeline track items. Attribute: - selected (bool)[optional]: return only selected items on timeline + selection (list)[optional]: list of selected track items sequence_name (str)[optional]: return only clips from input sequence track_item_name (str)[optional]: return only item with input name track_name (str)[optional]: return only items from track name @@ -155,32 +164,34 @@ def get_track_items( Return: list or hiero.core.TrackItem: list of track items or single track item """ - return_list = list() - track_items = list() + track_type = track_type or "video" + selection = selection or [] + return_list = [] # get selected track items or all in active sequence - if selected: + if selection: try: - selected_items = list(hiero.selection) - for item in selected_items: - if track_name and track_name in item.parent().name(): - # filter only items fitting input track name - track_items.append(item) - elif not track_name: - # or add all if no track_name was defined - track_items.append(item) + for track_item in selection: + log.info("___ track_item: {}".format(track_item)) + # make sure only trackitems are selected + if not isinstance(track_item, hiero.core.TrackItem): + continue + + if _validate_all_atrributes( + track_item, + track_item_name, + track_name, + track_type, + check_enabled, + check_tagged + ): + log.info("___ valid trackitem: {}".format(track_item)) + return_list.append(track_item) except AttributeError: pass - # check if any collected track items are - # `core.Hiero.Python.TrackItem` instance - if track_items: - any_track_item = track_items[0] - if not isinstance(any_track_item, hiero.core.TrackItem): - selected_items = [] - # collect all available active sequence track items - if not track_items: + if not return_list: sequence = get_current_sequence(name=sequence_name) # get all available tracks from sequence tracks = list(sequence.audioTracks()) + list(sequence.videoTracks()) @@ -191,42 +202,76 @@ def get_track_items( if check_enabled and not track.isEnabled(): continue # and all items in track - for item in track.items(): - if check_tagged and not item.tags(): + for track_item in track.items(): + # make sure no subtrackitem is also track items + if not isinstance(track_item, hiero.core.TrackItem): continue - # check if track item is enabled - if check_enabled: - if not item.isEnabled(): - continue - if track_item_name: - if track_item_name in item.name(): - return item - # make sure only track items with correct track names are added - if track_name and track_name in track.name(): - # filter out only defined track_name items - track_items.append(item) - elif not track_name: - # or add all if no track_name is defined - track_items.append(item) + if _validate_all_atrributes( + track_item, + track_item_name, + track_name, + track_type, + check_enabled, + check_tagged + ): + return_list.append(track_item) - # filter out only track items with defined track_type - for track_item in track_items: - if track_type and track_type == "video" and isinstance( + return return_list + + +def _validate_all_atrributes( + track_item, + track_item_name, + track_name, + track_type, + check_enabled, + check_tagged +): + def _validate_correct_name_track_item(): + if track_item_name and track_item_name in track_item.name(): + return True + elif not track_item_name: + return True + + def _validate_tagged_track_item(): + if check_tagged and track_item.tags(): + return True + elif not check_tagged: + return True + + def _validate_enabled_track_item(): + if check_enabled and track_item.isEnabled(): + return True + elif not check_enabled: + return True + + def _validate_parent_track_item(): + if track_name and track_name in track_item.parent().name(): + # filter only items fitting input track name + return True + elif not track_name: + # or add all if no track_name was defined + return True + + def _validate_type_track_item(): + if track_type == "video" and isinstance( track_item.parent(), hiero.core.VideoTrack): # only video track items are allowed - return_list.append(track_item) - elif track_type and track_type == "audio" and isinstance( + return True + elif track_type == "audio" and isinstance( track_item.parent(), hiero.core.AudioTrack): # only audio track items are allowed - return_list.append(track_item) - elif not track_type: - # add all if no track_type is defined - return_list.append(track_item) + return True - # return output list but make sure all items are TrackItems - return [_i for _i in return_list - if type(_i) == hiero.core.TrackItem] + # check if track item is enabled + return all([ + _validate_enabled_track_item(), + _validate_type_track_item(), + _validate_tagged_track_item(), + _validate_parent_track_item(), + _validate_correct_name_track_item() + ]) def get_track_item_pype_tag(track_item): @@ -245,7 +290,7 @@ def get_track_item_pype_tag(track_item): return None for tag in _tags: # return only correct tag defined by global name - if tag.name() in self.pype_tag_name: + if tag.name() == self.pype_tag_name: return tag @@ -266,7 +311,7 @@ def set_track_item_pype_tag(track_item, data=None): "editable": "0", "note": "OpenPype data container", "icon": "openpype_icon.png", - "metadata": {k: v for k, v in data.items()} + "metadata": dict(data.items()) } # get available pype tag if any _tag = get_track_item_pype_tag(track_item) @@ -301,9 +346,9 @@ def get_track_item_pype_data(track_item): return None # get tag metadata attribute - tag_data = tag.metadata() + tag_data = deepcopy(dict(tag.metadata())) # convert tag metadata to normal keys names and values to correct types - for k, v in dict(tag_data).items(): + for k, v in tag_data.items(): key = k.replace("tag.", "") try: @@ -324,7 +369,7 @@ def get_track_item_pype_data(track_item): log.warning(msg) value = v - data.update({key: value}) + data[key] = value return data @@ -497,7 +542,7 @@ class PyblishSubmission(hiero.exporters.FnSubmission.Submission): from . import publish # Add submission to Hiero module for retrieval in plugins. hiero.submission = self - publish() + publish(hiero.ui.mainWindow()) def add_submission(): @@ -527,7 +572,7 @@ class PublishAction(QtWidgets.QAction): # from getting picked up when not using the "Export" dialog. if hasattr(hiero, "submission"): del hiero.submission - publish() + publish(hiero.ui.mainWindow()) def eventHandler(self, event): # Add the Menu to the right-click menu @@ -893,32 +938,32 @@ def apply_colorspace_clips(): def is_overlapping(ti_test, ti_original, strict=False): - covering_exp = bool( + covering_exp = ( (ti_test.timelineIn() <= ti_original.timelineIn()) and (ti_test.timelineOut() >= ti_original.timelineOut()) ) - inside_exp = bool( + inside_exp = ( (ti_test.timelineIn() >= ti_original.timelineIn()) and (ti_test.timelineOut() <= ti_original.timelineOut()) ) - overlaying_right_exp = bool( + overlaying_right_exp = ( (ti_test.timelineIn() < ti_original.timelineOut()) and (ti_test.timelineOut() >= ti_original.timelineOut()) ) - overlaying_left_exp = bool( + overlaying_left_exp = ( (ti_test.timelineOut() > ti_original.timelineIn()) and (ti_test.timelineIn() <= ti_original.timelineIn()) ) - if not strict: + if strict: + return covering_exp + else: return any(( covering_exp, inside_exp, overlaying_right_exp, overlaying_left_exp )) - else: - return covering_exp def get_sequence_pattern_and_padding(file): @@ -936,17 +981,13 @@ def get_sequence_pattern_and_padding(file): """ foundall = re.findall( r"(#+)|(%\d+d)|(?<=[^a-zA-Z0-9])(\d+)(?=\.\w+$)", file) - if foundall: - found = sorted(list(set(foundall[0])))[-1] - - if "%" in found: - padding = int(re.findall(r"\d+", found)[-1]) - else: - padding = len(found) - - return found, padding - else: + if not foundall: return None, None + found = sorted(list(set(foundall[0])))[-1] + + padding = int( + re.findall(r"\d+", found)[-1]) if "%" in found else len(found) + return found, padding def sync_clip_name_to_data_asset(track_items_list): @@ -982,7 +1023,7 @@ def sync_clip_name_to_data_asset(track_items_list): print("asset was changed in clip: {}".format(ti_name)) -def check_inventory_versions(): +def check_inventory_versions(track_items=None): """ Actual version color idetifier of Loaded containers @@ -993,14 +1034,14 @@ def check_inventory_versions(): """ from . import parse_container + track_item = track_items or get_track_items() # presets clip_color_last = "green" clip_color = "red" # get all track items from current timeline - for track_item in get_track_items(): + for track_item in track_item: container = parse_container(track_item) - if container: # get representation from io representation = legacy_io.find_one({ @@ -1038,29 +1079,31 @@ def selection_changed_timeline(event): timeline_editor = event.sender selection = timeline_editor.selection() - selection = [ti for ti in selection - if isinstance(ti, hiero.core.TrackItem)] + track_items = get_track_items( + selection=selection, + track_type="video", + check_enabled=True, + check_locked=True, + check_tagged=True + ) # run checking function - sync_clip_name_to_data_asset(selection) - - # also mark old versions of loaded containers - check_inventory_versions() + sync_clip_name_to_data_asset(track_items) def before_project_save(event): track_items = get_track_items( - selected=False, track_type="video", check_enabled=True, check_locked=True, - check_tagged=True) + check_tagged=True + ) # run checking function sync_clip_name_to_data_asset(track_items) # also mark old versions of loaded containers - check_inventory_versions() + check_inventory_versions(track_items) def get_main_window(): diff --git a/openpype/hosts/hiero/api/otio/hiero_export.py b/openpype/hosts/hiero/api/otio/hiero_export.py index 64fb81aed4..1e4088d9c0 100644 --- a/openpype/hosts/hiero/api/otio/hiero_export.py +++ b/openpype/hosts/hiero/api/otio/hiero_export.py @@ -151,7 +151,7 @@ def create_otio_reference(clip): padding = media_source.filenamePadding() file_head = media_source.filenameHead() is_sequence = not media_source.singleFile() - frame_duration = media_source.duration() - 1 + frame_duration = media_source.duration() fps = utils.get_rate(clip) or self.project_fps extension = os.path.splitext(path)[-1] diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index 8025ebff05..9b628ec70b 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -143,6 +143,11 @@ def parse_container(track_item, validate=True): """ # convert tag metadata to normal keys names data = lib.get_track_item_pype_data(track_item) + if ( + not data + or data.get("id") != "pyblish.avalon.container" + ): + return if validate and data and data.get("schema"): schema.validate(data) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 54e66bf99a..add416d04e 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -1,4 +1,5 @@ import os +from pprint import pformat import re from copy import deepcopy @@ -400,7 +401,8 @@ class ClipLoader: # inject asset data to representation dict self._get_asset_data() - log.debug("__init__ self.data: `{}`".format(self.data)) + log.info("__init__ self.data: `{}`".format(pformat(self.data))) + log.info("__init__ options: `{}`".format(pformat(options))) # add active components to class if self.new_sequence: @@ -482,7 +484,9 @@ class ClipLoader: """ asset_name = self.context["representation"]["context"]["asset"] - self.data["assetData"] = openpype.get_asset(asset_name)["data"] + asset_doc = openpype.get_asset(asset_name) + log.debug("__ asset_doc: {}".format(pformat(asset_doc))) + self.data["assetData"] = asset_doc["data"] def _make_track_item(self, source_bin_item, audio=False): """ Create track item with """ @@ -500,7 +504,7 @@ class ClipLoader: track_item.setSource(clip) track_item.setSourceIn(self.handle_start) track_item.setTimelineIn(self.timeline_in) - track_item.setSourceOut(self.media_duration - self.handle_end) + track_item.setSourceOut((self.media_duration) - self.handle_end) track_item.setTimelineOut(self.timeline_out) track_item.setPlaybackSpeed(1) self.active_track.addTrackItem(track_item) @@ -520,14 +524,18 @@ class ClipLoader: self.handle_start = self.data["versionData"].get("handleStart") self.handle_end = self.data["versionData"].get("handleEnd") if self.handle_start is None: - self.handle_start = int(self.data["assetData"]["handleStart"]) + self.handle_start = self.data["assetData"]["handleStart"] if self.handle_end is None: - self.handle_end = int(self.data["assetData"]["handleEnd"]) + self.handle_end = self.data["assetData"]["handleEnd"] + + self.handle_start = int(self.handle_start) + self.handle_end = int(self.handle_end) if self.sequencial_load: last_track_item = lib.get_track_items( sequence_name=self.active_sequence.name(), - track_name=self.active_track.name()) + track_name=self.active_track.name() + ) if len(last_track_item) == 0: last_timeline_out = 0 else: @@ -541,17 +549,12 @@ class ClipLoader: self.timeline_in = int(self.data["assetData"]["clipIn"]) self.timeline_out = int(self.data["assetData"]["clipOut"]) + log.debug("__ self.timeline_in: {}".format(self.timeline_in)) + log.debug("__ self.timeline_out: {}".format(self.timeline_out)) + # check if slate is included - # either in version data families or by calculating frame diff - slate_on = next( - # check iterate if slate is in families - (f for f in self.context["version"]["data"]["families"] - if "slate" in f), - # if nothing was found then use default None - # so other bool could be used - None) or bool(int( - (self.timeline_out - self.timeline_in + 1) - + self.handle_start + self.handle_end) < self.media_duration) + slate_on = "slate" in self.context["version"]["data"]["families"] + log.debug("__ slate_on: {}".format(slate_on)) # if slate is on then remove the slate frame from beginning if slate_on: @@ -572,7 +575,7 @@ class ClipLoader: # there were some cases were hiero was not creating it source_bin_item = None for item in self.active_bin.items(): - if self.data["clip_name"] in item.name(): + if self.data["clip_name"] == item.name(): source_bin_item = item if not source_bin_item: log.warning("Problem with created Source clip: `{}`".format( @@ -599,8 +602,8 @@ class Creator(LegacyCreator): rename_index = None def __init__(self, *args, **kwargs): - import openpype.hosts.hiero.api as phiero super(Creator, self).__init__(*args, **kwargs) + import openpype.hosts.hiero.api as phiero self.presets = openpype.get_current_project_settings()[ "hiero"]["create"].get(self.__class__.__name__, {}) @@ -609,7 +612,10 @@ class Creator(LegacyCreator): self.sequence = phiero.get_current_sequence() if (self.options or {}).get("useSelection"): - self.selected = phiero.get_track_items(selected=True) + timeline_selection = phiero.get_timeline_selection() + self.selected = phiero.get_track_items( + selection=timeline_selection + ) else: self.selected = phiero.get_track_items() @@ -716,6 +722,10 @@ class PublishClip: else: self.tag_data.update({"reviewTrack": None}) + log.debug("___ self.tag_data: {}".format( + pformat(self.tag_data) + )) + # create pype tag on track_item and add data lib.imprint(self.track_item, self.tag_data) diff --git a/openpype/hosts/hiero/api/tags.py b/openpype/hosts/hiero/api/tags.py index 8877b92b9d..8c6ff2a77b 100644 --- a/openpype/hosts/hiero/api/tags.py +++ b/openpype/hosts/hiero/api/tags.py @@ -86,7 +86,7 @@ def update_tag(tag, data): # due to hiero bug we have to make sure keys which are not existent in # data are cleared of value by `None` - for _mk in mtd.keys(): + for _mk in mtd.dict().keys(): if _mk.replace("tag.", "") not in data_mtd.keys(): mtd.setValue(_mk, str(None)) diff --git a/openpype/hosts/hiero/plugins/load/load_clip.py b/openpype/hosts/hiero/plugins/load/load_clip.py index da4326c8c1..a3365253b3 100644 --- a/openpype/hosts/hiero/plugins/load/load_clip.py +++ b/openpype/hosts/hiero/plugins/load/load_clip.py @@ -3,10 +3,6 @@ from openpype.pipeline import ( get_representation_path, ) import openpype.hosts.hiero.api as phiero -# from openpype.hosts.hiero.api import plugin, lib -# reload(lib) -# reload(plugin) -# reload(phiero) class LoadClip(phiero.SequenceLoader): @@ -106,7 +102,7 @@ class LoadClip(phiero.SequenceLoader): name = container['name'] namespace = container['namespace'] track_item = phiero.get_track_items( - track_item_name=namespace) + track_item_name=namespace).pop() version = legacy_io.find_one({ "type": "version", "_id": representation["parent"] @@ -157,7 +153,7 @@ class LoadClip(phiero.SequenceLoader): # load clip to timeline and get main variables namespace = container['namespace'] track_item = phiero.get_track_items( - track_item_name=namespace) + track_item_name=namespace).pop() track = track_item.parent() # remove track item from track diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 46f0b2440e..e54d050f0d 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -19,9 +19,12 @@ class PrecollectInstances(pyblish.api.ContextPlugin): def process(self, context): self.otio_timeline = context.data["otioTimeline"] - + timeline_selection = phiero.get_timeline_selection() selected_timeline_items = phiero.get_track_items( - selected=True, check_tagged=True, check_enabled=True) + selection=timeline_selection, + check_tagged=True, + check_enabled=True + ) # only return enabled track items if not selected_timeline_items: @@ -292,9 +295,9 @@ class PrecollectInstances(pyblish.api.ContextPlugin): for otio_clip in self.otio_timeline.each_clip(): track_name = otio_clip.parent().name parent_range = otio_clip.range_in_parent() - if ti_track_name not in track_name: + if ti_track_name != track_name: continue - if otio_clip.name not in track_item.name(): + if otio_clip.name != track_item.name(): continue self.log.debug("__ parent_range: {}".format(parent_range)) self.log.debug("__ timeline_range: {}".format(timeline_range)) @@ -314,7 +317,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): speed = track_item.playbackSpeed() timeline = phiero.get_current_sequence() frame_start = int(track_item.timelineIn()) - frame_duration = int(track_item.sourceDuration() / speed) + frame_duration = int((track_item.duration() - 1) / speed) fps = timeline.framerate().toFloat() return hiero_export.create_otio_time_range( diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index b9f58c15f6..c9bfb86810 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -16,7 +16,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): """Inject the current working file into context""" label = "Precollect Workfile" - order = pyblish.api.CollectorOrder - 0.5 + order = pyblish.api.CollectorOrder - 0.491 def process(self, context): @@ -84,6 +84,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): "colorspace": self.get_colorspace(project), "fps": fps } + self.log.debug("__ context_data: {}".format(pformat(context_data))) context.data.update(context_data) self.log.info("Creating instance: {}".format(instance)) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index ff04fa7aa2..2d3bda5245 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1093,6 +1093,11 @@ class RenderProductsRenderman(ARenderProducts): if not enabled: continue + # Skip display types not producing any file output. + # Is there a better way to do it? + if not display_types.get(display["driverNode"]["type"]): + continue + aov_name = name if aov_name == "rmanDefaultDisplay": aov_name = "beauty" diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_camera.py b/openpype/hosts/maya/plugins/publish/collect_fbx_camera.py new file mode 100644 index 0000000000..bfa5bccbb9 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_camera.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from maya import cmds # noqa +import pyblish.api + + +class CollectFbxCamera(pyblish.api.InstancePlugin): + """Collect Camera for FBX export.""" + + order = pyblish.api.CollectorOrder + 0.2 + label = "Collect Camera for FBX export" + families = ["camera"] + + def process(self, instance): + if not instance.data.get("families"): + instance.data["families"] = [] + + if "fbx" not in instance.data["families"]: + instance.data["families"].append("fbx") + + instance.data["cameras"] = True diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index b6a76f1e21..d295492f9a 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -22,10 +22,46 @@ RENDERER_NODE_TYPES = [ # redshift "RedshiftMeshParameters" ] - SHAPE_ATTRS = set(SHAPE_ATTRS) +def get_pxr_multitexture_file_attrs(node): + attrs = [] + for i in range(9): + if cmds.attributeQuery("filename{}".format(i), node): + file = cmds.getAttr("{}.filename{}".format(node, i)) + if file: + attrs.append("filename{}".format(i)) + return attrs + + +FILE_NODES = { + "file": "fileTextureName", + + "aiImage": "filename", + + "RedshiftNormalMap": "text0", + + "PxrBump": "filename", + "PxrNormalMap": "filename", + "PxrMultiTexture": get_pxr_multitexture_file_attrs, + "PxrPtexture": "filename", + "PxrTexture": "filename" +} + + +def get_attributes(dictionary, attr): + # type: (dict, str) -> list + if callable(dictionary[attr]): + val = dictionary[attr]() + else: + val = dictionary.get(attr, []) + + if not isinstance(val, list): + return [val] + return val + + def get_look_attrs(node): """Returns attributes of a node that are important for the look. @@ -51,15 +87,14 @@ def get_look_attrs(node): if cmds.objectType(node, isAType="shape"): attrs = cmds.listAttr(node, changedSinceFileOpen=True) or [] for attr in attrs: - if attr in SHAPE_ATTRS: + if attr in SHAPE_ATTRS or \ + attr not in SHAPE_ATTRS and attr.startswith('ai'): result.append(attr) - elif attr.startswith('ai'): - result.append(attr) - return result -def node_uses_image_sequence(node): +def node_uses_image_sequence(node, node_path): + # type: (str) -> bool """Return whether file node uses an image sequence or single image. Determine if a node uses an image sequence or just a single image, @@ -74,12 +109,15 @@ def node_uses_image_sequence(node): """ # useFrameExtension indicates an explicit image sequence - node_path = get_file_node_path(node).lower() - # The following tokens imply a sequence - patterns = ["", "", "", "u_v", "", "", "", + "u_v", ""] + try: + use_frame_extension = cmds.getAttr('%s.useFrameExtension' % node) + except ValueError: + use_frame_extension = False - return (cmds.getAttr('%s.useFrameExtension' % node) or + return (use_frame_extension or any(pattern in node_path for pattern in patterns)) @@ -137,14 +175,15 @@ def seq_to_glob(path): return path -def get_file_node_path(node): +def get_file_node_paths(node): + # type: (str) -> list """Get the file path used by a Maya file node. Args: node (str): Name of the Maya file node Returns: - str: the file path in use + list: the file paths in use """ # if the path appears to be sequence, use computedFileTextureNamePattern, @@ -163,15 +202,19 @@ def get_file_node_path(node): ""] lower = texture_pattern.lower() if any(pattern in lower for pattern in patterns): - return texture_pattern + return [texture_pattern] - if cmds.nodeType(node) == 'aiImage': - return cmds.getAttr('{0}.filename'.format(node)) - if cmds.nodeType(node) == 'RedshiftNormalMap': - return cmds.getAttr('{}.tex0'.format(node)) + try: + file_attributes = get_attributes(FILE_NODES, cmds.nodeType(node)) + except AttributeError: + file_attributes = "fileTextureName" - # otherwise use fileTextureName - return cmds.getAttr('{0}.fileTextureName'.format(node)) + files = [] + for file_attr in file_attributes: + if cmds.attributeQuery(file_attr, node=node, exists=True): + files.append(cmds.getAttr("{}.{}".format(node, file_attr))) + + return files def get_file_node_files(node): @@ -185,16 +228,21 @@ def get_file_node_files(node): list: List of full file paths. """ + paths = get_file_node_paths(node) + sequences = [] + replaces = [] + for index, path in enumerate(paths): + if node_uses_image_sequence(node, path): + glob_pattern = seq_to_glob(path) + sequences.extend(glob.glob(glob_pattern)) + replaces.append(index) - path = get_file_node_path(node) - path = cmds.workspace(expandName=path) - if node_uses_image_sequence(node): - glob_pattern = seq_to_glob(path) - return glob.glob(glob_pattern) - elif os.path.exists(path): - return [path] - else: - return [] + for index in replaces: + paths.pop(index) + + paths.extend(sequences) + + return [p for p in paths if os.path.exists(p)] class CollectLook(pyblish.api.InstancePlugin): @@ -238,13 +286,13 @@ class CollectLook(pyblish.api.InstancePlugin): "for %s" % instance.data['name']) # Discover related object sets - self.log.info("Gathering sets..") + self.log.info("Gathering sets ...") sets = self.collect_sets(instance) # Lookup set (optimization) instance_lookup = set(cmds.ls(instance, long=True)) - self.log.info("Gathering set relations..") + self.log.info("Gathering set relations ...") # Ensure iteration happen in a list so we can remove keys from the # dict within the loop @@ -326,7 +374,10 @@ class CollectLook(pyblish.api.InstancePlugin): "volumeShader", "displacementShader", "aiSurfaceShader", - "aiVolumeShader"] + "aiVolumeShader", + "rman__surface", + "rman__displacement" + ] if look_sets: materials = [] @@ -374,9 +425,10 @@ class CollectLook(pyblish.api.InstancePlugin): or [] ) - files = cmds.ls(history, type="file", long=True) - files.extend(cmds.ls(history, type="aiImage", long=True)) - files.extend(cmds.ls(history, type="RedshiftNormalMap", long=True)) + all_supported_nodes = FILE_NODES.keys() + files = [] + for node_type in all_supported_nodes: + files.extend(cmds.ls(history, type=node_type, long=True)) self.log.info("Collected file nodes:\n{}".format(files)) # Collect textures if any file nodes are found @@ -510,27 +562,24 @@ class CollectLook(pyblish.api.InstancePlugin): Returns: dict """ - self.log.debug("processing: {}".format(node)) - if cmds.nodeType(node) not in ["file", "aiImage", "RedshiftNormalMap"]: + all_supported_nodes = FILE_NODES.keys() + if cmds.nodeType(node) not in all_supported_nodes: self.log.error( "Unsupported file node: {}".format(cmds.nodeType(node))) raise AssertionError("Unsupported file node") - if cmds.nodeType(node) == 'file': - self.log.debug(" - file node") - attribute = "{}.fileTextureName".format(node) - computed_attribute = "{}.computedFileTextureNamePattern".format(node) - elif cmds.nodeType(node) == 'aiImage': - self.log.debug("aiImage node") - attribute = "{}.filename".format(node) - computed_attribute = attribute - elif cmds.nodeType(node) == 'RedshiftNormalMap': - self.log.debug("RedshiftNormalMap node") - attribute = "{}.tex0".format(node) - computed_attribute = attribute + self.log.debug(" - got {}".format(cmds.nodeType(node))) + + attribute = FILE_NODES.get(cmds.nodeType(node)) + source = cmds.getAttr("{}.{}".format( + node, + attribute + )) + computed_attribute = "{}.{}".format(node, attribute) + if attribute == "fileTextureName": + computed_attribute = node + ".computedFileTextureNamePattern" - source = cmds.getAttr(attribute) self.log.info(" - file source: {}".format(source)) color_space_attr = "{}.colorSpace".format(node) try: @@ -567,11 +616,15 @@ class CollectLook(pyblish.api.InstancePlugin): self.log.info(" - color space: {}".format(color_space)) # Define the resource - return {"node": node, - "attribute": attribute, - "source": source, # required for resources - "files": files, - "color_space": color_space} # required for resources + return { + "node": node, + # here we are passing not only attribute, but with node again + # this should be simplified and changed extractor. + "attribute": "{}.{}".format(node, attribute), + "source": source, # required for resources + "files": files, + "color_space": color_space + } # required for resources class CollectModelRenderSets(CollectLook): diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index e66983780e..fbd2e81279 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -339,9 +339,15 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "source": filepath, "expectedFiles": full_exp_files, "publishRenderMetadataFolder": common_publish_meta_path, - "resolutionWidth": cmds.getAttr("defaultResolution.width"), - "resolutionHeight": cmds.getAttr("defaultResolution.height"), - "pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"), + "resolutionWidth": lib.get_attr_in_layer( + "defaultResolution.height", layer=layer_name + ), + "resolutionHeight": lib.get_attr_in_layer( + "defaultResolution.width", layer=layer_name + ), + "pixelAspect": lib.get_attr_in_layer( + "defaultResolution.pixelAspect", layer=layer_name + ), "tileRendering": render_instance.data.get("tileRendering") or False, # noqa: E501 "tilesX": render_instance.data.get("tilesX") or 2, "tilesY": render_instance.data.get("tilesY") or 2, diff --git a/openpype/hosts/maya/plugins/publish/collect_vrayscene.py b/openpype/hosts/maya/plugins/publish/collect_vrayscene.py index afdb570cbc..0bae9656f3 100644 --- a/openpype/hosts/maya/plugins/publish/collect_vrayscene.py +++ b/openpype/hosts/maya/plugins/publish/collect_vrayscene.py @@ -124,9 +124,15 @@ class CollectVrayScene(pyblish.api.InstancePlugin): # Add source to allow tracing back to the scene from # which was submitted originally "source": context.data["currentFile"].replace("\\", "/"), - "resolutionWidth": cmds.getAttr("defaultResolution.width"), - "resolutionHeight": cmds.getAttr("defaultResolution.height"), - "pixelAspect": cmds.getAttr("defaultResolution.pixelAspect"), + "resolutionWidth": lib.get_attr_in_layer( + "defaultResolution.height", layer=layer_name + ), + "resolutionHeight": lib.get_attr_in_layer( + "defaultResolution.width", layer=layer_name + ), + "pixelAspect": lib.get_attr_in_layer( + "defaultResolution.pixelAspect", layer=layer_name + ), "priority": instance.data.get("priority"), "useMultipleSceneFiles": instance.data.get( "vraySceneMultipleFiles") diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 881705b92c..81d7c31ae7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -372,10 +372,12 @@ class ExtractLook(openpype.api.Extractor): if mode == COPY: transfers.append((source, destination)) - self.log.info('copying') + self.log.info('file will be copied {} -> {}'.format( + source, destination)) elif mode == HARDLINK: hardlinks.append((source, destination)) - self.log.info('hardlinking') + self.log.info('file will be hardlinked {} -> {}'.format( + source, destination)) # Store the hashes from hash to destination to include in the # database diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index ba8aa7a8db..f40425eefc 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -373,7 +373,7 @@ def add_write_node_legacy(name, **kwarg): Returns: node (obj): nuke write node """ - frame_range = kwarg.get("use_range_limit", None) + use_range_limit = kwarg.get("use_range_limit", None) w = nuke.createNode( "Write", @@ -391,10 +391,10 @@ def add_write_node_legacy(name, **kwarg): log.debug(e) continue - if frame_range: + if use_range_limit: w["use_limit"].setValue(True) - w["first"].setValue(frame_range[0]) - w["last"].setValue(frame_range[1]) + w["first"].setValue(kwarg["frame_range"][0]) + w["last"].setValue(kwarg["frame_range"][1]) return w @@ -409,7 +409,7 @@ def add_write_node(name, file_path, knobs, **kwarg): Returns: node (obj): nuke write node """ - frame_range = kwarg.get("use_range_limit", None) + use_range_limit = kwarg.get("use_range_limit", None) w = nuke.createNode( "Write", @@ -420,10 +420,10 @@ def add_write_node(name, file_path, knobs, **kwarg): # finally add knob overrides set_node_knobs_from_settings(w, knobs, **kwarg) - if frame_range: + if use_range_limit: w["use_limit"].setValue(True) - w["first"].setValue(frame_range[0]) - w["last"].setValue(frame_range[1]) + w["first"].setValue(kwarg["frame_range"][0]) + w["last"].setValue(kwarg["frame_range"][1]) return w diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index 32ee1fd86f..fec97167fb 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -27,6 +27,10 @@ class CreateWritePrerender(plugin.AbstractWriteRender): # add fpath_template write_data["fpath_template"] = self.fpath_template write_data["use_range_limit"] = self.use_range_limit + write_data["frame_range"] = ( + nuke.root()["first_frame"].value(), + nuke.root()["last_frame"].value() + ) if not self.is_legacy(): return create_write_node( diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index 9ed43b2110..49edb22a89 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -1,4 +1,5 @@ import nuke +import os from openpype.api import Logger from openpype.pipeline import install_host @@ -9,6 +10,7 @@ from openpype.hosts.nuke.api.lib import ( WorkfileSettings, dirmap_file_name_filter ) +from openpype.settings import get_project_settings log = Logger.get_logger(__name__) @@ -28,3 +30,32 @@ nuke.addOnScriptLoad(WorkfileSettings().set_context_settings) nuke.addFilenameFilter(dirmap_file_name_filter) log.info('Automatic syncing of write file knob to script version') + + +def add_scripts_menu(): + try: + from scriptsmenu import launchfornuke + except ImportError: + log.warning( + "Skipping studio.menu install, because " + "'scriptsmenu' module seems unavailable." + ) + return + + # load configuration of custom menu + project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + config = project_settings["nuke"]["scriptsmenu"]["definition"] + _menu = project_settings["nuke"]["scriptsmenu"]["name"] + + if not config: + log.warning("Skipping studio menu, no definition found.") + return + + # run the launcher for Maya menu + studio_menu = launchfornuke.main(title=_menu.title()) + + # apply configuration + studio_menu.build_from_configuration(studio_menu, config) + + +add_scripts_menu() diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index 1ee21deedc..7b2d22f738 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -168,7 +168,7 @@ def make_sequence_collection(path, otio_range, metadata): first, last = otio_range_to_frame_range(otio_range) collection = clique.Collection( head=head, tail=tail, padding=metadata["padding"]) - collection.indexes.update([i for i in range(first, (last + 1))]) + collection.indexes.update([i for i in range(first, last)]) return dir_path, collection @@ -218,6 +218,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): "name": name } tw_node.update(metadata) + tw_node["lookup"] = list(lookup) # get first and last frame offsets offset_in += lookup[0] @@ -254,7 +255,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): media_in + source_in + offset_in) media_out_trimmed = ( media_in + source_in + ( - (source_range.duration.value * abs( + ((source_range.duration.value - 1) * abs( time_scalar)) + offset_out)) # calculate available handles diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py index 124787e467..e4ba651bfd 100644 --- a/openpype/modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/ftrack/lib/avalon_sync.py @@ -143,14 +143,17 @@ def create_chunks(iterable, chunk_size=None): list: Chunked items. """ chunks = [] - if not iterable: - return chunks tupled_iterable = tuple(iterable) + if not tupled_iterable: + return chunks iterable_size = len(tupled_iterable) if chunk_size is None: chunk_size = 200 + if chunk_size < 1: + chunk_size = 1 + for idx in range(0, iterable_size, chunk_size): chunks.append(tupled_iterable[idx:idx + chunk_size]) return chunks diff --git a/openpype/plugins/publish/collect_hierarchy.py b/openpype/plugins/publish/collect_hierarchy.py index a96d444be6..8398a2815a 100644 --- a/openpype/plugins/publish/collect_hierarchy.py +++ b/openpype/plugins/publish/collect_hierarchy.py @@ -62,7 +62,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): "frameEnd": instance.data["frameEnd"], "clipIn": instance.data["clipIn"], "clipOut": instance.data["clipOut"], - 'fps': instance.context.data["fps"], + "fps": instance.data["fps"], "resolutionWidth": instance.data["resolutionWidth"], "resolutionHeight": instance.data["resolutionHeight"], "pixelAspect": instance.data["pixelAspect"] diff --git a/openpype/plugins/publish/collect_otio_frame_ranges.py b/openpype/plugins/publish/collect_otio_frame_ranges.py index ee7b7957ad..8eaf9d6f29 100644 --- a/openpype/plugins/publish/collect_otio_frame_ranges.py +++ b/openpype/plugins/publish/collect_otio_frame_ranges.py @@ -55,13 +55,13 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin): "frameStart": frame_start, "frameEnd": frame_end, "clipIn": tl_start, - "clipOut": tl_end, + "clipOut": tl_end - 1, "clipInH": tl_start_h, - "clipOutH": tl_end_h, + "clipOutH": tl_end_h - 1, "sourceStart": src_starting_from + src_start, - "sourceEnd": src_starting_from + src_end, + "sourceEnd": src_starting_from + src_end - 1, "sourceStartH": src_starting_from + src_start_h, - "sourceEndH": src_starting_from + src_end_h, + "sourceEndH": src_starting_from + src_end_h - 1, } instance.data.update(data) self.log.debug( diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index 7c11462ef0..b89a076a44 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -66,7 +66,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): # create trimmed otio time range trimmed_media_range_h = editorial.range_from_frames( - a_frame_start_h, (a_frame_end_h - a_frame_start_h + 1), + a_frame_start_h, (a_frame_end_h - a_frame_start_h) + 1, media_fps ) trimmed_duration = trimmed_media_range_h.duration.value diff --git a/openpype/plugins/publish/extract_otio_trimming_video.py b/openpype/plugins/publish/extract_otio_trimming_video.py index 30b57e2c69..e8e2994f36 100644 --- a/openpype/plugins/publish/extract_otio_trimming_video.py +++ b/openpype/plugins/publish/extract_otio_trimming_video.py @@ -80,7 +80,7 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor): video_path = input_file_path frame_start = otio_range.start_time.value input_fps = otio_range.start_time.rate - frame_duration = (otio_range.duration.value + 1) + frame_duration = otio_range.duration.value - 1 sec_start = openpype.lib.frames_to_secons(frame_start, input_fps) sec_duration = openpype.lib.frames_to_secons(frame_duration, input_fps) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index bf13a4050e..353314fff2 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -113,7 +113,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "usdOverride", "simpleUnrealTexture" ] - exclude_families = ["clip", "render.farm"] + exclude_families = ["render.farm"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", "family", "hierarchy", "task", "username" diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 4cdfe1ca5d..e03bdcecc3 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -165,6 +165,9 @@ "CollectMayaRender": { "sync_workfile_version": false }, + "CollectFbxCamera": { + "enabled": false + }, "ValidateInstanceInContext": { "enabled": true, "optional": true, diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 33ddc2f251..dc8ffcebff 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -15,6 +15,18 @@ "destination-path": [] } }, + "scriptsmenu": { + "name": "OpenPype Tools", + "definition": [ + { + "type": "action", + "sourcetype": "python", + "title": "OpenPype Docs", + "command": "import webbrowser;webbrowser.open(url='https://openpype.io/docs/artist_hosts_nuke_tut')", + "tooltip": "Open the OpenPype Nuke user doc page" + } + ] + }, "create": { "CreateWriteRender": { "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index cc70516c72..0c7943447b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -49,7 +49,7 @@ }, { "type": "schema", - "name": "schema_maya_scriptsmenu" + "name": "schema_scriptsmenu" }, { "type": "schema", 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 bc572cbdc8..1ae4efd8ea 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -79,6 +79,10 @@ } ] }, + { + "type": "schema", + "name": "schema_scriptsmenu" + }, { "type": "dict", "collapsible": true, 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 2e5bc64e1c..9877b5ff0d 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 @@ -21,6 +21,20 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "CollectFbxCamera", + "label": "Collect Camera for FBX export", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + }, { "type": "splitter" }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_scriptsmenu.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_scriptsmenu.json similarity index 100% rename from openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_scriptsmenu.json rename to openpype/settings/entities/schemas/projects_schema/schemas/schema_scriptsmenu.json diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index 7d9f1a7323..4b5bc36aeb 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -139,6 +139,7 @@ class HierarchyView(QtWidgets.QTreeView): self.setAlternatingRowColors(True) self.setSelectionMode(HierarchyView.ExtendedSelection) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.setEditTriggers(HierarchyView.AllEditTriggers) column_delegates = {} column_key_to_index = {} @@ -301,16 +302,6 @@ class HierarchyView(QtWidgets.QTreeView): def rowsInserted(self, parent_index, start, end): super(HierarchyView, self).rowsInserted(parent_index, start, end) - for row in range(start, end + 1): - for key, column in self._column_key_to_index.items(): - if key not in self.persistent_columns: - continue - col_index = self._source_model.index(row, column, parent_index) - if bool( - self._source_model.flags(col_index) - & QtCore.Qt.ItemIsEditable - ): - self.openPersistentEditor(col_index) # Expand parent on insert if not self.isExpanded(parent_index): diff --git a/openpype/vendor/python/common/scriptsmenu/action.py b/openpype/vendor/python/common/scriptsmenu/action.py index dc4d775f6a..5e68628406 100644 --- a/openpype/vendor/python/common/scriptsmenu/action.py +++ b/openpype/vendor/python/common/scriptsmenu/action.py @@ -119,7 +119,8 @@ module.{module_name}()""" """ # get the current application and its linked keyboard modifiers - modifiers = QtWidgets.QApplication.keyboardModifiers() + app = QtWidgets.QApplication.instance() + modifiers = app.keyboardModifiers() # If the menu has a callback registered for the current modifier # we run the callback instead of the action itself. diff --git a/openpype/vendor/python/common/scriptsmenu/launchfornuke.py b/openpype/vendor/python/common/scriptsmenu/launchfornuke.py index 23e4ed1b4d..72302a79a6 100644 --- a/openpype/vendor/python/common/scriptsmenu/launchfornuke.py +++ b/openpype/vendor/python/common/scriptsmenu/launchfornuke.py @@ -8,7 +8,7 @@ def _nuke_main_window(): if (obj.inherits('QMainWindow') and obj.metaObject().className() == 'Foundry::UI::DockMainWindow'): return obj - raise RuntimeError('Could not find Nuke MainWindow instance') + raise RuntimeError('Could not find Nuke MainWindow instance') def _nuke_main_menubar(): @@ -22,9 +22,6 @@ def _nuke_main_menubar(): def main(title="Scripts"): - # Register control + shift callback to add to shelf (Nuke behavior) - # modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier - # menu.register_callback(modifiers, to_shelf) nuke_main_bar = _nuke_main_menubar() for nuke_bar in nuke_main_bar.children(): if isinstance(nuke_bar, scriptsmenu.ScriptsMenu): @@ -33,4 +30,4 @@ def main(title="Scripts"): return menu menu = scriptsmenu.ScriptsMenu(title=title, parent=nuke_main_bar) - return menu \ No newline at end of file + return menu diff --git a/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py b/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py index e2b7ff96c7..9e7c094902 100644 --- a/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py +++ b/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py @@ -264,8 +264,7 @@ class ScriptsMenu(QtWidgets.QMenu): action.setVisible(True) else: for action in self._script_actions: - if not action.has_tag(search.lower()): - action.setVisible(False) + action.setVisible(action.has_tag(search.lower())) # Set visibility for all submenus for action in self.actions(): diff --git a/openpype/vendor/python/common/scriptsmenu/version.py b/openpype/vendor/python/common/scriptsmenu/version.py index 73f9426c2d..52ec49c845 100644 --- a/openpype/vendor/python/common/scriptsmenu/version.py +++ b/openpype/vendor/python/common/scriptsmenu/version.py @@ -1,6 +1,6 @@ VERSION_MAJOR = 1 VERSION_MINOR = 5 -VERSION_PATCH = 1 +VERSION_PATCH = 2 version = '{}.{}.{}'.format(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) diff --git a/website/docs/admin_hosts_nuke.md b/website/docs/admin_hosts_nuke.md new file mode 100644 index 0000000000..46f596a2dc --- /dev/null +++ b/website/docs/admin_hosts_nuke.md @@ -0,0 +1,14 @@ +--- +id: admin_hosts_nuke +title: Nuke +sidebar_label: Nuke +--- + +## Custom Menu +You can add your custom tools menu into Nuke by extending definitions in **Nuke -> Scripts Menu Definition**. +![Custom menu definition](assets/nuke-admin_scriptsmenu.png) + +:::note Work in progress +This is still work in progress. Menu definition will be handled more friendly with widgets and not +raw json. +::: diff --git a/website/docs/assets/nuke-admin_scriptsmenu.png b/website/docs/assets/nuke-admin_scriptsmenu.png new file mode 100644 index 0000000000..cad2a4411d Binary files /dev/null and b/website/docs/assets/nuke-admin_scriptsmenu.png differ diff --git a/website/sidebars.js b/website/sidebars.js index 105afc30eb..0e33bed949 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -88,6 +88,7 @@ module.exports = { items: [ "admin_hosts_blender", "admin_hosts_maya", + "admin_hosts_nuke", "admin_hosts_resolve", "admin_hosts_harmony", "admin_hosts_aftereffects",