From 759dc59132bc32ce07b6f204c8da7df8ae9f2715 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 29 Sep 2023 17:33:27 +0200 Subject: [PATCH 01/45] add clip to timeline in correct place --- openpype/hosts/resolve/api/lib.py | 9 ++++++++- openpype/hosts/resolve/api/plugin.py | 10 +++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index eaee3bb9ba..4e8b3a4107 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -246,7 +246,8 @@ def get_media_pool_item(fpath, root: object = None) -> object: def create_timeline_item(media_pool_item: object, timeline: object = None, source_start: int = None, - source_end: int = None) -> object: + source_end: int = None, + timeline_in: int = None) -> object: """ Add media pool item to current or defined timeline. @@ -278,6 +279,12 @@ def create_timeline_item(media_pool_item: object, clip_data.update({"endFrame": source_end}) print(clip_data) + + if timeline_in: + timeline_start = timeline.GetStartFrame() + # Create a clipInfo dictionary with the necessary information + clip_data["recordFrame"] = int(timeline_start + timeline_in) + # add to timeline media_pool.AppendToTimeline([clip_data]) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index e2bd76ffa2..c679aa062d 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -402,6 +402,9 @@ class ClipLoader: if handle_end is None: handle_end = int(self.data["assetData"]["handleEnd"]) + self.timeline_in = int(self.data["assetData"]["clipIn"]) + + source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) @@ -416,7 +419,12 @@ class ClipLoader: # make track item from source in bin as item timeline_item = lib.create_timeline_item( - media_pool_item, self.active_timeline, source_in, source_out) + media_pool_item, + self.active_timeline, + source_in, + source_out, + self.timeline_in + ) print("Loading clips: `{}`".format(self.data["clip_name"])) return timeline_item From 7cd8be0afa68005bc615523fd4a0ee55f73e1a30 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Oct 2023 11:49:51 +0200 Subject: [PATCH 02/45] resolve: add option for adding clips sequentially - or to asset define place - also create track with a name --- openpype/hosts/resolve/api/lib.py | 12 +++--- openpype/hosts/resolve/api/plugin.py | 55 +++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 4e8b3a4107..8f7eba8a90 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -274,17 +274,15 @@ def create_timeline_item(media_pool_item: object, # add source time range if input was given if source_start is not None: - clip_data.update({"startFrame": source_start}) + clip_data["startFrame"] = source_start if source_end is not None: - clip_data.update({"endFrame": source_end}) + clip_data["endFrame"] = source_end + + # Create a clipInfo dictionary with the necessary information + clip_data["recordFrame"] = timeline_in print(clip_data) - if timeline_in: - timeline_start = timeline.GetStartFrame() - # Create a clipInfo dictionary with the necessary information - clip_data["recordFrame"] = int(timeline_start + timeline_in) - # add to timeline media_pool.AppendToTimeline([clip_data]) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index c679aa062d..b1bde212fe 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -312,6 +312,9 @@ class ClipLoader: # try to get value from options or evaluate key value for `load_to` self.new_timeline = options.get("newTimeline") or bool( "New timeline" in options.get("load_to", "")) + # try to get value from options or evaluate key value for `load_how` + self.sequential_load = options.get("sequentially") or bool( + "Sequentially in order" in options.get("load_how", "")) assert self._populate_data(), str( "Cannot Load selected data, look into database " @@ -352,6 +355,7 @@ class ClipLoader: asset = str(repr_cntx["asset"]) subset = str(repr_cntx["subset"]) representation = str(repr_cntx["representation"]) + self.data["track_name"] = "{}_{}".format(asset, representation) self.data["clip_name"] = "_".join([asset, subset, representation]) self.data["versionData"] = self.context["version"]["data"] # gets file path @@ -383,6 +387,33 @@ class ClipLoader: asset_name = self.context["representation"]["context"]["asset"] self.data["assetData"] = get_current_project_asset(asset_name)["data"] + def _set_active_track(self): + """ Set active track to `track` """ + track_type = "video" + track_name = self.data["track_name"] + track_exists = False + + # get total track count + track_count = self.active_timeline.GetTrackCount(track_type) + # loop all tracks by track indexes + for track_index in range(1, int(track_count) + 1): + # get current track name + _track_name = self.active_timeline.GetTrackName( + track_type, track_index) + if track_name != _track_name: + continue + track_exists = True + break + + if not track_exists: + self.active_timeline.AddTrack(track_type) + self.active_timeline.SetTrackName( + track_type, + track_index + 1, + track_name + ) + + def load(self): # create project bin for the media to be imported into self.active_bin = lib.create_bin(self.data["binPath"]) @@ -402,8 +433,18 @@ class ClipLoader: if handle_end is None: handle_end = int(self.data["assetData"]["handleEnd"]) - self.timeline_in = int(self.data["assetData"]["clipIn"]) + # handle timeline tracks + self._set_active_track() + # get timeline in + timeline_start = self.active_timeline.GetStartFrame() + if self.sequential_load: + # set timeline start frame + timeline_in = int(timeline_start) + else: + # set timeline start frame + original clip in frame + timeline_in = int( + timeline_start + self.data["assetData"]["clipIn"]) source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) @@ -423,7 +464,7 @@ class ClipLoader: self.active_timeline, source_in, source_out, - self.timeline_in + timeline_in ) print("Loading clips: `{}`".format(self.data["clip_name"])) @@ -478,6 +519,16 @@ class TimelineItemLoader(LoaderPlugin): ], default=0, help="Where do you want clips to be loaded?" + ), + qargparse.Choice( + "load_how", + label="How to load clips", + items=[ + "Original timing", + "Sequentially in order" + ], + default="Original timing", + help="Would you like to place it at original timing?" ) ] From ec893d45e3e47ac95ec6f9d3ba16513c70009a78 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Oct 2023 11:50:10 +0200 Subject: [PATCH 03/45] updating docs readme to latest python api --- ...0.4.txt => RESOLVE_API_v18.5.1-build6.txt} | 81 +++++++++++++++---- 1 file changed, 65 insertions(+), 16 deletions(-) rename openpype/hosts/resolve/{RESOLVE_API_v18.0.4.txt => RESOLVE_API_v18.5.1-build6.txt} (89%) diff --git a/openpype/hosts/resolve/RESOLVE_API_v18.0.4.txt b/openpype/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt similarity index 89% rename from openpype/hosts/resolve/RESOLVE_API_v18.0.4.txt rename to openpype/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt index 98597a12cb..7d1d6edf61 100644 --- a/openpype/hosts/resolve/RESOLVE_API_v18.0.4.txt +++ b/openpype/hosts/resolve/RESOLVE_API_v18.5.1-build6.txt @@ -1,4 +1,4 @@ -Updated as of 9 May 2022 +Updated as of 26 May 2023 ---------------------------- In this package, you will find a brief introduction to the Scripting API for DaVinci Resolve Studio. Apart from this README.txt file, this package contains folders containing the basic import modules for scripting access (DaVinciResolve.py) and some representative examples. @@ -19,7 +19,7 @@ DaVinci Resolve scripting requires one of the following to be installed (for all Lua 5.1 Python 2.7 64-bit - Python 3.6 64-bit + Python >= 3.6 64-bit Using a script @@ -171,6 +171,10 @@ Project GetRenderResolutions(format, codec) --> [{Resolution}] # Returns list of resolutions applicable for the given render format (string) and render codec (string). Returns full list of resolutions if no argument is provided. Each element in the list is a dictionary with 2 keys "Width" and "Height". RefreshLUTList() --> Bool # Refreshes LUT List GetUniqueId() --> string # Returns a unique ID for the project item + InsertAudioToCurrentTrackAtPlayhead(mediaPath, --> Bool # Inserts the media specified by mediaPath (string) with startOffsetInSamples (int) and durationInSamples (int) at the playhead on a selected track on the Fairlight page. Returns True if successful, otherwise False. + startOffsetInSamples, durationInSamples) + LoadBurnInPreset(presetName) --> Bool # Loads user defined data burn in preset for project when supplied presetName (string). Returns true if successful. + ExportCurrentFrameAsStill(filePath) --> Bool # Exports current frame as still to supplied filePath. filePath must end in valid export file format. Returns True if succssful, False otherwise. MediaStorage GetMountedVolumeList() --> [paths...] # Returns list of folder paths corresponding to mounted volumes displayed in Resolve’s Media Storage. @@ -179,6 +183,7 @@ MediaStorage RevealInStorage(path) --> Bool # Expands and displays given file/folder path in Resolve’s Media Storage. AddItemListToMediaPool(item1, item2, ...) --> [clips...] # Adds specified file/folder paths from Media Storage into current Media Pool folder. Input is one or more file/folder paths. Returns a list of the MediaPoolItems created. AddItemListToMediaPool([items...]) --> [clips...] # Adds specified file/folder paths from Media Storage into current Media Pool folder. Input is an array of file/folder paths. Returns a list of the MediaPoolItems created. + AddItemListToMediaPool([{itemInfo}, ...]) --> [clips...] # Adds list of itemInfos specified as dict of "media", "startFrame" (int), "endFrame" (int) from Media Storage into current Media Pool folder. Returns a list of the MediaPoolItems created. AddClipMattesToMediaPool(MediaPoolItem, [paths], stereoEye) --> Bool # Adds specified media files as mattes for the specified MediaPoolItem. StereoEye is an optional argument for specifying which eye to add the matte to for stereo clips ("left" or "right"). Returns True if successful. AddTimelineMattesToMediaPool([paths]) --> [MediaPoolItems] # Adds specified media files as timeline mattes in current media pool folder. Returns a list of created MediaPoolItems. @@ -189,20 +194,22 @@ MediaPool CreateEmptyTimeline(name) --> Timeline # Adds new timeline with given name. AppendToTimeline(clip1, clip2, ...) --> [TimelineItem] # Appends specified MediaPoolItem objects in the current timeline. Returns the list of appended timelineItems. AppendToTimeline([clips]) --> [TimelineItem] # Appends specified MediaPoolItem objects in the current timeline. Returns the list of appended timelineItems. - AppendToTimeline([{clipInfo}, ...]) --> [TimelineItem] # Appends list of clipInfos specified as dict of "mediaPoolItem", "startFrame" (int), "endFrame" (int), (optional) "mediaType" (int; 1 - Video only, 2 - Audio only). Returns the list of appended timelineItems. + AppendToTimeline([{clipInfo}, ...]) --> [TimelineItem] # Appends list of clipInfos specified as dict of "mediaPoolItem", "startFrame" (int), "endFrame" (int), (optional) "mediaType" (int; 1 - Video only, 2 - Audio only), "trackIndex" (int) and "recordFrame" (int). Returns the list of appended timelineItems. CreateTimelineFromClips(name, clip1, clip2,...) --> Timeline # Creates new timeline with specified name, and appends the specified MediaPoolItem objects. CreateTimelineFromClips(name, [clips]) --> Timeline # Creates new timeline with specified name, and appends the specified MediaPoolItem objects. - CreateTimelineFromClips(name, [{clipInfo}]) --> Timeline # Creates new timeline with specified name, appending the list of clipInfos specified as a dict of "mediaPoolItem", "startFrame" (int), "endFrame" (int). - ImportTimelineFromFile(filePath, {importOptions}) --> Timeline # Creates timeline based on parameters within given file and optional importOptions dict, with support for the keys: - # "timelineName": string, specifies the name of the timeline to be created - # "importSourceClips": Bool, specifies whether source clips should be imported, True by default + CreateTimelineFromClips(name, [{clipInfo}]) --> Timeline # Creates new timeline with specified name, appending the list of clipInfos specified as a dict of "mediaPoolItem", "startFrame" (int), "endFrame" (int), "recordFrame" (int). + ImportTimelineFromFile(filePath, {importOptions}) --> Timeline # Creates timeline based on parameters within given file (AAF/EDL/XML/FCPXML/DRT/ADL) and optional importOptions dict, with support for the keys: + # "timelineName": string, specifies the name of the timeline to be created. Not valid for DRT import + # "importSourceClips": Bool, specifies whether source clips should be imported, True by default. Not valid for DRT import # "sourceClipsPath": string, specifies a filesystem path to search for source clips if the media is inaccessible in their original path and if "importSourceClips" is True - # "sourceClipsFolders": List of Media Pool folder objects to search for source clips if the media is not present in current folder and if "importSourceClips" is False + # "sourceClipsFolders": List of Media Pool folder objects to search for source clips if the media is not present in current folder and if "importSourceClips" is False. Not valid for DRT import # "interlaceProcessing": Bool, specifies whether to enable interlace processing on the imported timeline being created. valid only for AAF import DeleteTimelines([timeline]) --> Bool # Deletes specified timelines in the media pool. GetCurrentFolder() --> Folder # Returns currently selected Folder. SetCurrentFolder(Folder) --> Bool # Sets current folder by given Folder. DeleteClips([clips]) --> Bool # Deletes specified clips or timeline mattes in the media pool + ImportFolderFromFile(filePath, sourceClipsPath="") --> Bool # Returns true if import from given DRB filePath is successful, false otherwise + # sourceClipsPath is a string that specifies a filesystem path to search for source clips if the media is inaccessible in their original path, empty by default DeleteFolders([subfolders]) --> Bool # Deletes specified subfolders in the media pool MoveClips([clips], targetFolder) --> Bool # Moves specified clips to target folder. MoveFolders([folders], targetFolder) --> Bool # Moves specified folders to target folder. @@ -225,6 +232,7 @@ Folder GetSubFolderList() --> [folders...] # Returns a list of subfolders in the folder. GetIsFolderStale() --> bool # Returns true if folder is stale in collaboration mode, false otherwise GetUniqueId() --> string # Returns a unique ID for the media pool folder + Export(filePath) --> bool # Returns true if export of DRB folder to filePath is successful, false otherwise MediaPoolItem GetName() --> string # Returns the clip name. @@ -257,6 +265,8 @@ MediaPoolItem UnlinkProxyMedia() --> Bool # Unlinks any proxy media associated with clip. ReplaceClip(filePath) --> Bool # Replaces the underlying asset and metadata of MediaPoolItem with the specified absolute clip path. GetUniqueId() --> string # Returns a unique ID for the media pool item + TranscribeAudio() --> Bool # Transcribes audio of the MediaPoolItem. Returns True if successful; False otherwise + ClearTranscription() --> Bool # Clears audio transcription of the MediaPoolItem. Returns True if successful; False otherwise. Timeline GetName() --> string # Returns the timeline name. @@ -266,6 +276,23 @@ Timeline SetStartTimecode(timecode) --> Bool # Set the start timecode of the timeline to the string 'timecode'. Returns true when the change is successful, false otherwise. GetStartTimecode() --> string # Returns the start timecode for the timeline. GetTrackCount(trackType) --> int # Returns the number of tracks for the given track type ("audio", "video" or "subtitle"). + AddTrack(trackType, optionalSubTrackType) --> Bool # Adds track of trackType ("video", "subtitle", "audio"). Second argument optionalSubTrackType is required for "audio" + # optionalSubTrackType can be one of {"mono", "stereo", "5.1", "5.1film", "7.1", "7.1film", "adaptive1", ... , "adaptive24"} + DeleteTrack(trackType, trackIndex) --> Bool # Deletes track of trackType ("video", "subtitle", "audio") and given trackIndex. 1 <= trackIndex <= GetTrackCount(trackType). + SetTrackEnable(trackType, trackIndex, Bool) --> Bool # Enables/Disables track with given trackType and trackIndex + # trackType is one of {"audio", "video", "subtitle"} + # 1 <= trackIndex <= GetTrackCount(trackType). + GetIsTrackEnabled(trackType, trackIndex) --> Bool # Returns True if track with given trackType and trackIndex is enabled and False otherwise. + # trackType is one of {"audio", "video", "subtitle"} + # 1 <= trackIndex <= GetTrackCount(trackType). + SetTrackLock(trackType, trackIndex, Bool) --> Bool # Locks/Unlocks track with given trackType and trackIndex + # trackType is one of {"audio", "video", "subtitle"} + # 1 <= trackIndex <= GetTrackCount(trackType). + GetIsTrackLocked(trackType, trackIndex) --> Bool # Returns True if track with given trackType and trackIndex is locked and False otherwise. + # trackType is one of {"audio", "video", "subtitle"} + # 1 <= trackIndex <= GetTrackCount(trackType). + DeleteClips([timelineItems], Bool) --> Bool # Deletes specified TimelineItems from the timeline, performing ripple delete if the second argument is True. Second argument is optional (The default for this is False) + SetClipsLinked([timelineItems], Bool) --> Bool # Links or unlinks the specified TimelineItems depending on second argument. GetItemListInTrack(trackType, index) --> [items...] # Returns a list of timeline items on that track (based on trackType and index). 1 <= index <= GetTrackCount(trackType). AddMarker(frameId, color, name, note, duration, --> Bool # Creates a new marker at given frameId position and with given marker information. 'customData' is optional and helps to attach user specific data to the marker. customData) @@ -301,7 +328,7 @@ Timeline # "sourceClipsFolders": string, list of Media Pool folder objects to search for source clips if the media is not present in current folder Export(fileName, exportType, exportSubtype) --> Bool # Exports timeline to 'fileName' as per input exportType & exportSubtype format. - # Refer to section "Looking up timeline exports properties" for information on the parameters. + # Refer to section "Looking up timeline export properties" for information on the parameters. GetSetting(settingName) --> string # Returns value of timeline setting (indicated by settingName : string). Check the section below for more information. SetSetting(settingName, settingValue) --> Bool # Sets timeline setting (indicated by settingName : string) to the value (settingValue : string). Check the section below for more information. InsertGeneratorIntoTimeline(generatorName) --> TimelineItem # Inserts a generator (indicated by generatorName : string) into the timeline. @@ -313,6 +340,8 @@ Timeline GrabStill() --> galleryStill # Grabs still from the current video clip. Returns a GalleryStill object. GrabAllStills(stillFrameSource) --> [galleryStill] # Grabs stills from all the clips of the timeline at 'stillFrameSource' (1 - First frame, 2 - Middle frame). Returns the list of GalleryStill objects. GetUniqueId() --> string # Returns a unique ID for the timeline + CreateSubtitlesFromAudio() --> Bool # Creates subtitles from audio for the timeline. Returns True on success, False otherwise. + DetectSceneCuts() --> Bool # Detects and makes scene cuts along the timeline. Returns True if successful, False otherwise. TimelineItem GetName() --> string # Returns the item name. @@ -362,6 +391,7 @@ TimelineItem GetStereoLeftFloatingWindowParams() --> {keyframes...} # For the LEFT eye -> returns a dict (offset -> dict) of keyframe offsets and respective floating window params. Value at particular offset includes the left, right, top and bottom floating window values. GetStereoRightFloatingWindowParams() --> {keyframes...} # For the RIGHT eye -> returns a dict (offset -> dict) of keyframe offsets and respective floating window params. Value at particular offset includes the left, right, top and bottom floating window values. GetNumNodes() --> int # Returns the number of nodes in the current graph for the timeline item + ApplyArriCdlLut() --> Bool # Applies ARRI CDL and LUT. Returns True if successful, False otherwise. SetLUT(nodeIndex, lutPath) --> Bool # Sets LUT on the node mapping the node index provided, 1 <= nodeIndex <= total number of nodes. # The lutPath can be an absolute path, or a relative path (based off custom LUT paths or the master LUT path). # The operation is successful for valid lut paths that Resolve has already discovered (see Project.RefreshLUTList). @@ -376,8 +406,16 @@ TimelineItem SelectTakeByIndex(idx) --> Bool # Selects a take by index, 1 <= idx <= number of takes. FinalizeTake() --> Bool # Finalizes take selection. CopyGrades([tgtTimelineItems]) --> Bool # Copies the current grade to all the items in tgtTimelineItems list. Returns True on success and False if any error occurred. + SetClipEnabled(Bool) --> Bool # Sets clip enabled based on argument. + GetClipEnabled() --> Bool # Gets clip enabled status. UpdateSidecar() --> Bool # Updates sidecar file for BRAW clips or RMD file for R3D clips. GetUniqueId() --> string # Returns a unique ID for the timeline item + LoadBurnInPreset(presetName) --> Bool # Loads user defined data burn in preset for clip when supplied presetName (string). Returns true if successful. + GetNodeLabel(nodeIndex) --> string # Returns the label of the node at nodeIndex. + CreateMagicMask(mode) --> Bool # Returns True if magic mask was created successfully, False otherwise. mode can "F" (forward), "B" (backward), or "BI" (bidirection) + RegenerateMagicMask() --> Bool # Returns True if magic mask was regenerated successfully, False otherwise. + Stabilize() --> Bool # Returns True if stabilization was successful, False otherwise + SmartReframe() --> Bool # Performs Smart Reframe. Returns True if successful, False otherwise. Gallery GetAlbumName(galleryStillAlbum) --> string # Returns the name of the GalleryStillAlbum object 'galleryStillAlbum'. @@ -422,9 +460,11 @@ Invoke "Project:SetSetting", "Timeline:SetSetting" or "MediaPoolItem:SetClipProp ensure the success of the operation. You can troubleshoot the validity of keys and values by setting the desired result from the UI and checking property snapshots before and after the change. The following Project properties have specifically enumerated values: -"superScale" - the property value is an enumerated integer between 0 and 3 with these meanings: 0=Auto, 1=no scaling, and 2, 3 and 4 represent the Super Scale multipliers 2x, 3x and 4x. +"superScale" - the property value is an enumerated integer between 0 and 4 with these meanings: 0=Auto, 1=no scaling, and 2, 3 and 4 represent the Super Scale multipliers 2x, 3x and 4x. + for super scale multiplier '2x Enhanced', exactly 4 arguments must be passed as outlined below. If less than 4 arguments are passed, it will default to 2x. Affects: • x = Project:GetSetting('superScale') and Project:SetSetting('superScale', x) +• for '2x Enhanced' --> Project:SetSetting('superScale', 2, sharpnessValue, noiseReductionValue), where sharpnessValue is a float in the range [0.0, 1.0] and noiseReductionValue is a float in the range [0.0, 1.0] "timelineFrameRate" - the property value is one of the frame rates available to the user in project settings under "Timeline frame rate" option. Drop Frame can be configured for supported frame rates by appending the frame rate with "DF", e.g. "29.97 DF" will enable drop frame and "29.97" will disable drop frame @@ -432,9 +472,11 @@ Affects: • x = Project:GetSetting('timelineFrameRate') and Project:SetSetting('timelineFrameRate', x) The following Clip properties have specifically enumerated values: -"superScale" - the property value is an enumerated integer between 1 and 3 with these meanings: 1=no scaling, and 2, 3 and 4 represent the Super Scale multipliers 2x, 3x and 4x. +"Super Scale" - the property value is an enumerated integer between 1 and 4 with these meanings: 1=no scaling, and 2, 3 and 4 represent the Super Scale multipliers 2x, 3x and 4x. + for super scale multiplier '2x Enhanced', exactly 4 arguments must be passed as outlined below. If less than 4 arguments are passed, it will default to 2x. Affects: • x = MediaPoolItem:GetClipProperty('Super Scale') and MediaPoolItem:SetClipProperty('Super Scale', x) +• for '2x Enhanced' --> MediaPoolItem:SetClipProperty('Super Scale', 2, sharpnessValue, noiseReductionValue), where sharpnessValue is a float in the range [0.0, 1.0] and noiseReductionValue is a float in the range [0.0, 1.0] Looking up Render Settings @@ -478,11 +520,6 @@ exportType can be one of the following constants: - resolve.EXPORT_DRT - resolve.EXPORT_EDL - resolve.EXPORT_FCP_7_XML - - resolve.EXPORT_FCPXML_1_3 - - resolve.EXPORT_FCPXML_1_4 - - resolve.EXPORT_FCPXML_1_5 - - resolve.EXPORT_FCPXML_1_6 - - resolve.EXPORT_FCPXML_1_7 - resolve.EXPORT_FCPXML_1_8 - resolve.EXPORT_FCPXML_1_9 - resolve.EXPORT_FCPXML_1_10 @@ -492,6 +529,8 @@ exportType can be one of the following constants: - resolve.EXPORT_TEXT_TAB - resolve.EXPORT_DOLBY_VISION_VER_2_9 - resolve.EXPORT_DOLBY_VISION_VER_4_0 + - resolve.EXPORT_DOLBY_VISION_VER_5_1 + - resolve.EXPORT_OTIO exportSubtype can be one of the following enums: - resolve.EXPORT_NONE - resolve.EXPORT_AAF_NEW @@ -504,6 +543,16 @@ When exportType is resolve.EXPORT_AAF, valid exportSubtype values are resolve.EX When exportType is resolve.EXPORT_EDL, valid exportSubtype values are resolve.EXPORT_CDL, resolve.EXPORT_SDL, resolve.EXPORT_MISSING_CLIPS and resolve.EXPORT_NONE. Note: Replace 'resolve.' when using the constants above, if a different Resolve class instance name is used. +Unsupported exportType types +--------------------------------- +Starting with DaVinci Resolve 18.1, the following export types are not supported: + - resolve.EXPORT_FCPXML_1_3 + - resolve.EXPORT_FCPXML_1_4 + - resolve.EXPORT_FCPXML_1_5 + - resolve.EXPORT_FCPXML_1_6 + - resolve.EXPORT_FCPXML_1_7 + + Looking up Timeline item properties ----------------------------------- This section covers additional notes for the function "TimelineItem:SetProperty" and "TimelineItem:GetProperty". These functions are used to get and set properties mentioned. From 8c508b4b00082e7f0427fa929a5d2e0d8be19d99 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Oct 2023 15:45:38 +0200 Subject: [PATCH 04/45] removing testing scripts --- .../utility_scripts/tests/test_otio_as_edl.py | 49 ------------- .../testing_create_timeline_item_from_path.py | 73 ------------------- .../tests/testing_load_media_pool_item.py | 24 ------ .../tests/testing_startup_script.py | 5 -- .../tests/testing_timeline_op.py | 13 ---- 5 files changed, 164 deletions(-) delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py delete mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py diff --git a/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py b/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py deleted file mode 100644 index 92f2e43a72..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py +++ /dev/null @@ -1,49 +0,0 @@ -#! python3 -import os -import sys - -import opentimelineio as otio - -from openpype.pipeline import install_host - -import openpype.hosts.resolve.api as bmdvr -from openpype.hosts.resolve.api.testing_utils import TestGUI -from openpype.hosts.resolve.otio import davinci_export as otio_export - - -class ThisTestGUI(TestGUI): - extensions = [".exr", ".jpg", ".mov", ".png", ".mp4", ".ari", ".arx"] - - def __init__(self): - super(ThisTestGUI, self).__init__() - # activate resolve from openpype - install_host(bmdvr) - - def _open_dir_button_pressed(self, event): - # selected_path = self.fu.RequestFile(os.path.expanduser("~")) - selected_path = self.fu.RequestDir(os.path.expanduser("~")) - self._widgets["inputTestSourcesFolder"].Text = selected_path - - # main function - def process(self, event): - self.input_dir_path = self._widgets["inputTestSourcesFolder"].Text - project = bmdvr.get_current_project() - otio_timeline = otio_export.create_otio_timeline(project) - print(f"_ otio_timeline: `{otio_timeline}`") - edl_path = os.path.join(self.input_dir_path, "this_file_name.edl") - print(f"_ edl_path: `{edl_path}`") - # xml_string = otio_adapters.fcpx_xml.write_to_string(otio_timeline) - # print(f"_ xml_string: `{xml_string}`") - otio.adapters.write_to_file( - otio_timeline, edl_path, adapter_name="cmx_3600") - project = bmdvr.get_current_project() - media_pool = project.GetMediaPool() - timeline = media_pool.ImportTimelineFromFile(edl_path) - # at the end close the window - self._close_window(None) - - -if __name__ == "__main__": - test_gui = ThisTestGUI() - test_gui.show_gui() - sys.exit(not bool(True)) diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py b/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py deleted file mode 100644 index 91a361ec08..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py +++ /dev/null @@ -1,73 +0,0 @@ -#! python3 -import os -import sys - -import clique - -from openpype.pipeline import install_host -from openpype.hosts.resolve.api.testing_utils import TestGUI -import openpype.hosts.resolve.api as bmdvr -from openpype.hosts.resolve.api.lib import ( - create_media_pool_item, - create_timeline_item, -) - - -class ThisTestGUI(TestGUI): - extensions = [".exr", ".jpg", ".mov", ".png", ".mp4", ".ari", ".arx"] - - def __init__(self): - super(ThisTestGUI, self).__init__() - # activate resolve from openpype - install_host(bmdvr) - - def _open_dir_button_pressed(self, event): - # selected_path = self.fu.RequestFile(os.path.expanduser("~")) - selected_path = self.fu.RequestDir(os.path.expanduser("~")) - self._widgets["inputTestSourcesFolder"].Text = selected_path - - # main function - def process(self, event): - self.input_dir_path = self._widgets["inputTestSourcesFolder"].Text - - self.dir_processing(self.input_dir_path) - - # at the end close the window - self._close_window(None) - - def dir_processing(self, dir_path): - collections, reminders = clique.assemble(os.listdir(dir_path)) - - # process reminders - for _rem in reminders: - _rem_path = os.path.join(dir_path, _rem) - - # go deeper if directory - if os.path.isdir(_rem_path): - print(_rem_path) - self.dir_processing(_rem_path) - else: - self.file_processing(_rem_path) - - # process collections - for _coll in collections: - _coll_path = os.path.join(dir_path, list(_coll).pop()) - self.file_processing(_coll_path) - - def file_processing(self, fpath): - print(f"_ fpath: `{fpath}`") - _base, ext = os.path.splitext(fpath) - # skip if unwanted extension - if ext not in self.extensions: - return - media_pool_item = create_media_pool_item(fpath) - print(media_pool_item) - - track_item = create_timeline_item(media_pool_item) - print(track_item) - - -if __name__ == "__main__": - test_gui = ThisTestGUI() - test_gui.show_gui() - sys.exit(not bool(True)) diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py b/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py deleted file mode 100644 index 2e83188bde..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py +++ /dev/null @@ -1,24 +0,0 @@ -#! python3 -from openpype.pipeline import install_host -from openpype.hosts.resolve import api as bmdvr -from openpype.hosts.resolve.api.lib import ( - create_media_pool_item, - create_timeline_item, -) - - -def file_processing(fpath): - media_pool_item = create_media_pool_item(fpath) - print(media_pool_item) - - track_item = create_timeline_item(media_pool_item) - print(track_item) - - -if __name__ == "__main__": - path = "C:/CODE/__openpype_projects/jtest03dev/shots/sq01/mainsq01sh030/publish/plate/plateMain/v006/jt3d_mainsq01sh030_plateMain_v006.0996.exr" - - # activate resolve from openpype - install_host(bmdvr) - - file_processing(path) diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py b/openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py deleted file mode 100644 index b64714ab16..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_startup_script.py +++ /dev/null @@ -1,5 +0,0 @@ -#! python3 -from openpype.hosts.resolve.startup import main - -if __name__ == "__main__": - main() diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py b/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py deleted file mode 100644 index 8270496f64..0000000000 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py +++ /dev/null @@ -1,13 +0,0 @@ -#! python3 -from openpype.pipeline import install_host -from openpype.hosts.resolve import api as bmdvr -from openpype.hosts.resolve.api.lib import get_current_project - -if __name__ == "__main__": - install_host(bmdvr) - project = get_current_project() - timeline_count = project.GetTimelineCount() - print(f"Timeline count: {timeline_count}") - timeline = project.GetTimelineByIndex(timeline_count) - print(f"Timeline name: {timeline.GetName()}") - print(timeline.GetTrackCount("video")) From 88a1f97ad595000b6a440643b61cbcd3b8f659c9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 2 Oct 2023 15:48:27 +0200 Subject: [PATCH 05/45] resolve: improving loading --- openpype/hosts/resolve/api/lib.py | 54 +++++++++++++++++----------- openpype/hosts/resolve/api/plugin.py | 43 ++++------------------ 2 files changed, 40 insertions(+), 57 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 8f7eba8a90..22be929412 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -6,7 +6,10 @@ import contextlib from opentimelineio import opentime from openpype.lib import Logger -from openpype.pipeline.editorial import is_overlapping_otio_ranges +from openpype.pipeline.editorial import ( + is_overlapping_otio_ranges, + frames_to_timecode +) from ..otio import davinci_export as otio_export @@ -243,11 +246,13 @@ def get_media_pool_item(fpath, root: object = None) -> object: return None -def create_timeline_item(media_pool_item: object, - timeline: object = None, - source_start: int = None, - source_end: int = None, - timeline_in: int = None) -> object: +def create_timeline_item( + media_pool_item: object, + source_start: int, + source_end: int, + timeline_in: int, + timeline: object = None +) -> object: """ Add media pool item to current or defined timeline. @@ -267,20 +272,24 @@ def create_timeline_item(media_pool_item: object, clip_name = _clip_property("File Name") timeline = timeline or get_current_timeline() + # timing variables + fps = project.GetSetting("timelineFrameRate") + duration = source_end - source_start + timecode_in = frames_to_timecode(timeline_in, fps) + timecode_out = frames_to_timecode(timeline_in + duration, fps) + # if timeline was used then switch it to current timeline with maintain_current_timeline(timeline): # Add input mediaPoolItem to clip data - clip_data = {"mediaPoolItem": media_pool_item} - - # add source time range if input was given - if source_start is not None: - clip_data["startFrame"] = source_start - if source_end is not None: - clip_data["endFrame"] = source_end - - # Create a clipInfo dictionary with the necessary information - clip_data["recordFrame"] = timeline_in + clip_data = { + "mediaPoolItem": media_pool_item, + "startFrame": source_start, + "endFrame": source_end, + "recordFrame": timeline_in, + } + print("clip_data", "_" * 50) + print(media_pool_item.GetName()) print(clip_data) # add to timeline @@ -289,10 +298,15 @@ def create_timeline_item(media_pool_item: object, output_timeline_item = get_timeline_item( media_pool_item, timeline) - assert output_timeline_item, AssertionError( - "Track Item with name `{}` doesn't exist on the timeline: `{}`".format( - clip_name, timeline.GetName() - )) + assert output_timeline_item, AssertionError(( + "Clip name '{}' was't created on the timeline: '{}' \n\n" + "Please check if the clip is in the media pool or if the timeline \n" + "is having activated correct track name, or if it is not already \n" + "having any clip add place on the timeline in: '{}' out: '{}'. \n\n" + "Clip data: {}" + ).format( + clip_name, timeline.GetName(), timecode_in, timecode_out, clip_data + )) return output_timeline_item diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index b1bde212fe..1c817d8e0d 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -355,7 +355,6 @@ class ClipLoader: asset = str(repr_cntx["asset"]) subset = str(repr_cntx["subset"]) representation = str(repr_cntx["representation"]) - self.data["track_name"] = "{}_{}".format(asset, representation) self.data["clip_name"] = "_".join([asset, subset, representation]) self.data["versionData"] = self.context["version"]["data"] # gets file path @@ -387,32 +386,6 @@ class ClipLoader: asset_name = self.context["representation"]["context"]["asset"] self.data["assetData"] = get_current_project_asset(asset_name)["data"] - def _set_active_track(self): - """ Set active track to `track` """ - track_type = "video" - track_name = self.data["track_name"] - track_exists = False - - # get total track count - track_count = self.active_timeline.GetTrackCount(track_type) - # loop all tracks by track indexes - for track_index in range(1, int(track_count) + 1): - # get current track name - _track_name = self.active_timeline.GetTrackName( - track_type, track_index) - if track_name != _track_name: - continue - track_exists = True - break - - if not track_exists: - self.active_timeline.AddTrack(track_type) - self.active_timeline.SetTrackName( - track_type, - track_index + 1, - track_name - ) - def load(self): # create project bin for the media to be imported into @@ -420,7 +393,6 @@ class ClipLoader: # create mediaItem in active project bin # create clip media - media_pool_item = lib.create_media_pool_item( self.data["path"], self.active_bin) _clip_property = media_pool_item.GetClipProperty @@ -433,9 +405,6 @@ class ClipLoader: if handle_end is None: handle_end = int(self.data["assetData"]["handleEnd"]) - # handle timeline tracks - self._set_active_track() - # get timeline in timeline_start = self.active_timeline.GetStartFrame() if self.sequential_load: @@ -454,17 +423,17 @@ class ClipLoader: source_out -= handle_end # include handles - if self.with_handles: - source_in -= handle_start - source_out += handle_end + if not self.with_handles: + source_in += handle_start + source_out -= handle_end # make track item from source in bin as item timeline_item = lib.create_timeline_item( media_pool_item, - self.active_timeline, source_in, source_out, - timeline_in + timeline_in, + self.active_timeline, ) print("Loading clips: `{}`".format(self.data["clip_name"])) @@ -504,7 +473,7 @@ class TimelineItemLoader(LoaderPlugin): """ options = [ - qargparse.Toggle( + qargparse.Boolean( "handles", label="Include handles", default=0, From b1ea0b099dd13e13515d3a4f32b2830702496488 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 17 Oct 2023 15:32:42 +0200 Subject: [PATCH 06/45] merging if conditions for handles exclusion also updating docstring --- openpype/hosts/resolve/api/lib.py | 5 +++-- openpype/hosts/resolve/api/plugin.py | 9 ++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 22be929412..2d91609679 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -258,9 +258,10 @@ def create_timeline_item( Args: media_pool_item (resolve.MediaPoolItem): resolve's object + source_start (int): media source input frame (sequence frame) + source_end (int): media source output frame (sequence frame) + timeline_in (int): timeline input frame (sequence frame) timeline (resolve.Timeline)[optional]: resolve's object - source_start (int)[optional]: media source input frame (sequence frame) - source_end (int)[optional]: media source output frame (sequence frame) Returns: object: resolve.TimelineItem diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 1c817d8e0d..314d066890 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -418,12 +418,11 @@ class ClipLoader: source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) - if _clip_property("Type") == "Video": - source_in += handle_start - source_out -= handle_end - # include handles - if not self.with_handles: + if ( + not self.with_handles + or _clip_property("Type") == "Video" + ): source_in += handle_start source_out -= handle_end From 9e61d7e371d54a500cc1c67dd3c560d8cfcf98c1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 17 Oct 2023 17:39:10 +0200 Subject: [PATCH 07/45] removing debug printing --- openpype/hosts/resolve/api/lib.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index ca79dd6e87..dfe4608a46 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -292,10 +292,6 @@ def create_timeline_item( "recordFrame": timeline_in, } - print("clip_data", "_" * 50) - print(media_pool_item.GetName()) - print(clip_data) - # add to timeline media_pool.AppendToTimeline([clip_data]) @@ -511,7 +507,7 @@ def imprint(timeline_item, data=None): Arguments: timeline_item (hiero.core.TrackItem): hiero track item object - data (dict): Any data which needst to be imprinted + data (dict): Any data which needs to be imprinted Examples: data = { From 3bbc3d0489db4ea0d785416e6459c79fee34701e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 17 Oct 2023 17:42:46 +0200 Subject: [PATCH 08/45] Update openpype/hosts/resolve/api/lib.py Co-authored-by: Roy Nieterau --- openpype/hosts/resolve/api/lib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index dfe4608a46..20636f299b 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -251,10 +251,10 @@ def get_media_pool_item(filepath, root: object = None) -> object: def create_timeline_item( media_pool_item: object, - source_start: int, - source_end: int, - timeline_in: int, - timeline: object = None + timeline: object = None, + timeline_in: int = None, + source_start: int = None, + source_end: int = None, ) -> object: """ Add media pool item to current or defined timeline. From 435408dc142cb934424333257f8092db37974b01 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 17 Oct 2023 17:47:23 +0200 Subject: [PATCH 09/45] create_timeline_item with backward comapatibility --- openpype/hosts/resolve/api/lib.py | 30 ++++++++++++++++++---------- openpype/hosts/resolve/api/plugin.py | 4 ++-- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 20636f299b..6f84d921e0 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -261,10 +261,10 @@ def create_timeline_item( Args: media_pool_item (resolve.MediaPoolItem): resolve's object - source_start (int): media source input frame (sequence frame) - source_end (int): media source output frame (sequence frame) - timeline_in (int): timeline input frame (sequence frame) - timeline (resolve.Timeline)[optional]: resolve's object + timeline (Optional[resolve.Timeline]): resolve's object + timeline_in (Optional[int]): timeline input frame (sequence frame) + source_start (Optional[int]): media source input frame (sequence frame) + source_end (Optional[int]): media source output frame (sequence frame) Returns: object: resolve.TimelineItem @@ -277,21 +277,29 @@ def create_timeline_item( timeline = timeline or get_current_timeline() # timing variables - fps = project.GetSetting("timelineFrameRate") - duration = source_end - source_start - timecode_in = frames_to_timecode(timeline_in, fps) - timecode_out = frames_to_timecode(timeline_in + duration, fps) + if all([timeline_in, source_start, source_end]): + fps = project.GetSetting("timelineFrameRate") + duration = source_end - source_start + timecode_in = frames_to_timecode(timeline_in, fps) + timecode_out = frames_to_timecode(timeline_in + duration, fps) + else: + timecode_in = None + timecode_out = None # if timeline was used then switch it to current timeline with maintain_current_timeline(timeline): # Add input mediaPoolItem to clip data clip_data = { "mediaPoolItem": media_pool_item, - "startFrame": source_start, - "endFrame": source_end, - "recordFrame": timeline_in, } + if source_start: + clip_data["startFrame"] = source_start + if source_end: + clip_data["endFrame"] = source_end + if timecode_in: + clip_data["recordFrame"] = timecode_in + # add to timeline media_pool.AppendToTimeline([clip_data]) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 95f2fb2281..c88ed762ab 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -434,10 +434,10 @@ class ClipLoader: # make track item from source in bin as item timeline_item = lib.create_timeline_item( media_pool_item, + self.active_timeline, + timeline_in, source_in, source_out, - timeline_in, - self.active_timeline, ) print("Loading clips: `{}`".format(self.data["clip_name"])) From a810dfe78a97eb1467d3745ee786f2743f6faac2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Oct 2023 12:30:57 +0200 Subject: [PATCH 10/45] fixing assert message --- openpype/hosts/resolve/api/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 6f84d921e0..798c40a864 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -308,9 +308,9 @@ def create_timeline_item( assert output_timeline_item, AssertionError(( "Clip name '{}' was't created on the timeline: '{}' \n\n" - "Please check if the clip is in the media pool or if the timeline \n" - "is having activated correct track name, or if it is not already \n" - "having any clip add place on the timeline in: '{}' out: '{}'. \n\n" + "Please check if correct track position is activated, \n" + "or if a clip is not already at the timeline in \n" + "position: '{}' out: '{}'. \n\n" "Clip data: {}" ).format( clip_name, timeline.GetName(), timecode_in, timecode_out, clip_data From a11467883b5d1cec302f43361e7d939ff502ac07 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Oct 2023 12:34:12 +0200 Subject: [PATCH 11/45] getting fps from timeline instead of project --- openpype/hosts/resolve/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 798c40a864..aef9caca78 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -278,7 +278,7 @@ def create_timeline_item( # timing variables if all([timeline_in, source_start, source_end]): - fps = project.GetSetting("timelineFrameRate") + fps = timeline.GetSetting("timelineFrameRate") duration = source_end - source_start timecode_in = frames_to_timecode(timeline_in, fps) timecode_out = frames_to_timecode(timeline_in + duration, fps) From fbc24f565415cc6cbb88547c9b333bf41a52a3e0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Oct 2023 12:48:50 +0200 Subject: [PATCH 12/45] make sure handles on timeline are included only if available or demanded --- openpype/hosts/resolve/api/plugin.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index c88ed762ab..9a09685bee 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -410,6 +410,18 @@ class ClipLoader: if handle_end is None: handle_end = int(self.data["assetData"]["handleEnd"]) + # check frame duration from versionData or assetData + frame_start = self.data["versionData"].get("frameStart") + if frame_start is None: + frame_start = self.data["assetData"]["frameStart"] + + # check frame duration from versionData or assetData + frame_end = self.data["versionData"].get("frameEnd") + if frame_end is None: + frame_end = self.data["assetData"]["frameEnd"] + + db_frame_duration = int(frame_end) - int(frame_start) + 1 + # get timeline in timeline_start = self.active_timeline.GetStartFrame() if self.sequential_load: @@ -423,10 +435,17 @@ class ClipLoader: source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) - # include handles + # check if source duration is shorter than db frame duration + source_with_handles = True + source_duration = source_out - source_in + 1 + if source_duration < db_frame_duration: + source_with_handles = False + + # only exclude handles if source has no handles or + # if user wants to load without handles if ( not self.with_handles - or _clip_property("Type") == "Video" + or not source_with_handles ): source_in += handle_start source_out -= handle_end From ed2756aa52372b296b5e0307e763339dd5890250 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Oct 2023 13:05:23 +0200 Subject: [PATCH 13/45] improving code readability --- openpype/hosts/resolve/api/plugin.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 9a09685bee..63da14b1c2 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -306,14 +306,18 @@ class ClipLoader: self.active_project = lib.get_current_project() # try to get value from options or evaluate key value for `handles` - self.with_handles = options.get("handles") or bool( - options.get("handles") is True) + self.with_handles = options.get("handles") is True + # try to get value from options or evaluate key value for `load_to` - self.new_timeline = options.get("newTimeline") or bool( - "New timeline" in options.get("load_to", "")) + self.new_timeline = ( + options.get("newTimeline") or + options.get("load_to") == "New timeline" + ) # try to get value from options or evaluate key value for `load_how` - self.sequential_load = options.get("sequentially") or bool( - "Sequentially in order" in options.get("load_how", "")) + self.sequential_load = ( + options.get("sequentially") or + options.get("load_how") == "Sequentially in order" + ) assert self._populate_data(), str( "Cannot Load selected data, look into database " From aa74d48836c990a66413e821bd09628e5480a00f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 18 Oct 2023 14:00:57 +0200 Subject: [PATCH 14/45] clip duration from Frames clip attributes --- openpype/hosts/resolve/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 63da14b1c2..5c4a92df89 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -438,10 +438,10 @@ class ClipLoader: source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) + source_duration = int(_clip_property("Frames")) # check if source duration is shorter than db frame duration source_with_handles = True - source_duration = source_out - source_in + 1 if source_duration < db_frame_duration: source_with_handles = False From 9868b09c9bbd546d98148c7a80c087b87f84a766 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 10 Oct 2023 15:58:34 +0200 Subject: [PATCH 15/45] prepared context dialog using AYON calls --- openpype/tools/context_dialog/_ayon_window.py | 783 ++++++++++++++++++ .../tools/context_dialog/_openpype_window.py | 396 +++++++++ openpype/tools/context_dialog/window.py | 402 +-------- 3 files changed, 1188 insertions(+), 393 deletions(-) create mode 100644 openpype/tools/context_dialog/_ayon_window.py create mode 100644 openpype/tools/context_dialog/_openpype_window.py diff --git a/openpype/tools/context_dialog/_ayon_window.py b/openpype/tools/context_dialog/_ayon_window.py new file mode 100644 index 0000000000..6514780236 --- /dev/null +++ b/openpype/tools/context_dialog/_ayon_window.py @@ -0,0 +1,783 @@ +import os +import json + +import ayon_api +from qtpy import QtWidgets, QtCore, QtGui + +from openpype import style +from openpype.lib.events import QueuedEventSystem +from openpype.tools.ayon_utils.models import ( + ProjectsModel, + HierarchyModel, +) +from openpype.tools.ayon_utils.widgets import ( + ProjectsCombobox, + FoldersWidget, + TasksWidget, +) +from openpype.tools.utils.lib import ( + center_window, + get_openpype_qt_app, +) + + +class SelectionModel(object): + """Model handling selection changes. + + Triggering events: + - "selection.project.changed" + - "selection.folder.changed" + - "selection.task.changed" + """ + + event_source = "selection.model" + + def __init__(self, controller): + self._controller = controller + + self._project_name = None + self._folder_id = None + self._task_id = None + self._task_name = None + + def get_selected_project_name(self): + return self._project_name + + def set_selected_project(self, project_name): + self._project_name = project_name + self._controller.emit_event( + "selection.project.changed", + {"project_name": project_name}, + self.event_source + ) + + def get_selected_folder_id(self): + return self._folder_id + + def set_selected_folder(self, folder_id): + if folder_id == self._folder_id: + return + self._folder_id = folder_id + self._controller.emit_event( + "selection.folder.changed", + { + "project_name": self._project_name, + "folder_id": folder_id, + }, + self.event_source + ) + + def get_selected_task_name(self): + return self._task_name + + def get_selected_task_id(self): + return self._task_id + + def set_selected_task(self, task_id, task_name): + if task_id == self._task_id: + return + + self._task_name = task_name + self._task_id = task_id + self._controller.emit_event( + "selection.task.changed", + { + "project_name": self._project_name, + "folder_id": self._folder_id, + "task_name": task_name, + "task_id": task_id, + }, + self.event_source + ) + + +class ExpectedSelection: + def __init__(self, controller): + self._project_name = None + self._folder_id = None + + self._project_selected = True + self._folder_selected = True + + self._controller = controller + + def _emit_change(self): + self._controller.emit_event( + "expected_selection_changed", + self.get_expected_selection_data(), + ) + + def set_expected_selection(self, project_name, folder_id): + self._project_name = project_name + self._folder_id = folder_id + + self._project_selected = False + self._folder_selected = False + self._emit_change() + + def get_expected_selection_data(self): + project_current = False + folder_current = False + if not self._project_selected: + project_current = True + elif not self._folder_selected: + folder_current = True + return { + "project": { + "name": self._project_name, + "current": project_current, + "selected": self._project_selected, + }, + "folder": { + "id": self._folder_id, + "current": folder_current, + "selected": self._folder_selected, + }, + } + + def is_expected_project_selected(self, project_name): + return project_name == self._project_name and self._project_selected + + def is_expected_folder_selected(self, folder_id): + return folder_id == self._folder_id and self._folder_selected + + def expected_project_selected(self, project_name): + if project_name != self._project_name: + return False + self._project_selected = True + self._emit_change() + return True + + def expected_folder_selected(self, folder_id): + if folder_id != self._folder_id: + return False + self._folder_selected = True + self._emit_change() + return True + + +class ContextDialogController: + def __init__(self): + self._event_system = None + + self._projects_model = ProjectsModel(self) + self._hierarchy_model = HierarchyModel(self) + self._selection_model = SelectionModel(self) + self._expected_selection = ExpectedSelection(self) + + self._confirmed = False + self._is_strict = False + self._output_path = None + + self._initial_project_name = None + self._initial_folder_id = None + self._initial_folder_label = None + self._initial_project_found = True + self._initial_folder_found = True + self._initial_tasks_found = True + + def reset(self): + self._emit_event("controller.reset.started") + + self._confirmed = False + self._output_path = None + + self._initial_project_name = None + self._initial_folder_id = None + self._initial_folder_label = None + self._initial_project_found = True + self._initial_folder_found = True + self._initial_tasks_found = True + + self._projects_model.reset() + self._hierarchy_model.reset() + + self._emit_event("controller.reset.finished") + + def refresh(self): + self._emit_event("controller.refresh.started") + + self._projects_model.reset() + self._hierarchy_model.reset() + + self._emit_event("controller.refresh.finished") + + # Event handling + def emit_event(self, topic, data=None, source=None): + """Use implemented event system to trigger event.""" + + if data is None: + data = {} + self._get_event_system().emit(topic, data, source) + + def register_event_callback(self, topic, callback): + self._get_event_system().add_callback(topic, callback) + + def set_output_json_path(self, output_path): + self._output_path = output_path + + def is_strict(self): + return self._is_strict + + def set_strict(self, enabled): + if self._is_strict is enabled: + return + self._is_strict = enabled + self._emit_event("strict.changed", {"strict": enabled}) + + # Data model functions + def get_project_items(self, sender=None): + return self._projects_model.get_project_items(sender) + + def get_folder_items(self, project_name, sender=None): + return self._hierarchy_model.get_folder_items(project_name, sender) + + def get_task_items(self, project_name, folder_id, sender=None): + return self._hierarchy_model.get_task_items( + project_name, folder_id, sender + ) + + # Expected selection helpers + def set_expected_selection(self, project_name, folder_id): + return self._expected_selection.set_expected_selection( + project_name, folder_id + ) + + def get_expected_selection_data(self): + return self._expected_selection.get_expected_selection_data() + + def expected_project_selected(self, project_name): + self._expected_selection.expected_project_selected(project_name) + + def expected_folder_selected(self, folder_id): + self._expected_selection.expected_folder_selected(folder_id) + + # Selection handling + def get_selected_project_name(self): + return self._selection_model.get_selected_project_name() + + def set_selected_project(self, project_name): + self._selection_model.set_selected_project(project_name) + + def get_selected_folder_id(self): + return self._selection_model.get_selected_folder_id() + + def set_selected_folder(self, folder_id): + self._selection_model.set_selected_folder(folder_id) + + def get_selected_task_name(self): + return self._selection_model.get_selected_task_name() + + def get_selected_task_id(self): + return self._selection_model.get_selected_task_id() + + def set_selected_task(self, task_id, task_name): + self._selection_model.set_selected_task(task_id, task_name) + + def is_initial_context_valid(self): + return self._initial_folder_found and self._initial_project_found + + def set_initial_context( + self, project_name=None, asset_name=None, folder_path=None + ): + if project_name is None: + project_found = True + asset_name = None + folder_path = None + + else: + project = ayon_api.get_project(project_name) + project_found = project is not None + + folder_id = None + folder_found = True + folder_label = None + if folder_path: + folder_label = folder_path + folder = ayon_api.get_folder_by_path(project_name, folder_path) + if folder: + folder_id = folder["id"] + else: + folder_found = False + elif asset_name: + folder_label = asset_name + for folder in ayon_api.get_folders( + project_name, folder_names=[asset_name] + ): + folder_id = folder["id"] + break + if not folder_id: + folder_found = False + + tasks_found = True + if folder_found and (folder_path or asset_name): + tasks = list(ayon_api.get_tasks( + project_name, folder_ids=[folder_id], fields=["id"] + )) + if not tasks: + tasks_found = False + + self._initial_project_name = project_name + self._initial_folder_id = folder_id + self._initial_folder_label = folder_label + self._initial_folder_found = project_found + self._initial_folder_found = folder_found + self._initial_tasks_found = tasks_found + self._emit_event( + "initial.context.changed", + self.get_initial_context() + ) + + def get_initial_context(self): + return { + "project_name": self._initial_project_name, + "folder_id": self._initial_folder_id, + "folder_label": self._initial_folder_label, + "project_found": self._initial_project_found, + "folder_found": self._initial_folder_found, + "tasks_found": self._initial_tasks_found, + "valid": ( + self._initial_project_found + and self._initial_folder_found + and self._initial_tasks_found + ) + } + + # Result of this tool + def get_selected_context(self): + return { + "project": None, + "project_name": None, + "asset": None, + "folder_id": None, + "folder_path": None, + "task": None, + "task_id": None, + "task_name": None, + } + + def window_closed(self): + if not self._confirmed and not self._is_strict: + return + + self._store_output() + + def confirm_selection(self): + self._confirmed = True + self._emit_event( + "selection.confirmed", + {"confirmed": True} + ) + + def _store_output(self): + if not self._output_path: + return + + dirpath = os.path.dirname(self._output_path) + os.makedirs(dirpath, exist_ok=True) + with open(self._output_path, "w") as stream: + json.dump(self.get_selected_context(), stream) + + def _get_event_system(self): + """Inner event system for workfiles tool controller. + + Is used for communication with UI. Event system is created on demand. + + Returns: + QueuedEventSystem: Event system which can trigger callbacks + for topics. + """ + + if self._event_system is None: + self._event_system = QueuedEventSystem() + return self._event_system + + def _emit_event(self, topic, data=None): + self.emit_event(topic, data, "controller") + + +class InvalidContextOverlay(QtWidgets.QFrame): + confirmed = QtCore.Signal() + + def __init__(self, parent): + super(InvalidContextOverlay, self).__init__(parent) + self.setObjectName("OverlayFrame") + + mid_widget = QtWidgets.QWidget(self) + label_widget = QtWidgets.QLabel( + "Requested context was not found...", + mid_widget + ) + + confirm_btn = QtWidgets.QPushButton("Close", mid_widget) + + mid_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + mid_layout = QtWidgets.QVBoxLayout(mid_widget) + mid_layout.setContentsMargins(0, 0, 0, 0) + mid_layout.addWidget(label_widget, 0) + mid_layout.addSpacing(30) + mid_layout.addWidget(confirm_btn, 0) + + main_layout = QtWidgets.QGridLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(mid_widget, 1, 1) + main_layout.setRowStretch(0, 1) + main_layout.setRowStretch(1, 0) + main_layout.setRowStretch(2, 1) + main_layout.setColumnStretch(0, 1) + main_layout.setColumnStretch(1, 0) + main_layout.setColumnStretch(2, 1) + + confirm_btn.clicked.connect(self.confirmed) + + self._label_widget = label_widget + self._confirm_btn = confirm_btn + + def set_context( + self, + project_name, + folder_label, + project_found, + folder_found, + tasks_found, + ): + lines = [] + if not project_found: + lines.extend([ + "Requested project {} was not found...".format(project_name), + ]) + + elif not folder_found: + lines.extend([ + "Requested folder was not found...", + "", + "Project: {}".format(project_name), + "Folder: {}".format(folder_label), + ]) + elif not tasks_found: + lines.extend([ + "Requested folder does not have any tasks...", + "", + "Project: {}".format(project_name), + "Folder: {}".format(folder_label), + ]) + else: + lines.append("Requested context was not found...") + self._label_widget.setText("
".join(lines)) + + +class ContextDialog(QtWidgets.QDialog): + """Dialog to select a context. + + Context has 3 parts: + - Project + - Asset + - Task + + It is possible to predefine project and asset. In that case their widgets + will have passed preselected values and will be disabled. + """ + def __init__(self, controller=None, parent=None): + super(ContextDialog, self).__init__(parent) + + self.setWindowTitle("Select Context") + self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) + + if controller is None: + controller = ContextDialogController() + + # Enable minimize and maximize for app + window_flags = QtCore.Qt.Window + if not parent: + window_flags |= QtCore.Qt.WindowStaysOnTopHint + self.setWindowFlags(window_flags) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + # UI initialization + main_splitter = QtWidgets.QSplitter(self) + + # Left side widget contains project combobox and asset widget + left_side_widget = QtWidgets.QWidget(main_splitter) + + project_combobox = ProjectsCombobox( + controller, + parent=left_side_widget, + handle_expected_selection=True + ) + + # Assets widget + folders_widget = FoldersWidget( + controller, + parent=left_side_widget, + handle_expected_selection=True + ) + + left_side_layout = QtWidgets.QVBoxLayout(left_side_widget) + left_side_layout.setContentsMargins(0, 0, 0, 0) + left_side_layout.addWidget(project_combobox, 0) + left_side_layout.addWidget(folders_widget, 1) + + # Right side of window contains only tasks + tasks_widget = TasksWidget(controller, parent=main_splitter) + + # Add widgets to main splitter + main_splitter.addWidget(left_side_widget) + main_splitter.addWidget(tasks_widget) + + # Set stretch of both sides + main_splitter.setStretchFactor(0, 7) + main_splitter.setStretchFactor(1, 3) + + # Add confimation button to bottom right + ok_btn = QtWidgets.QPushButton("OK", self) + + buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.setContentsMargins(0, 0, 0, 0) + buttons_layout.addStretch(1) + buttons_layout.addWidget(ok_btn, 0) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(main_splitter, 1) + main_layout.addLayout(buttons_layout, 0) + + overlay_widget = InvalidContextOverlay(self) + overlay_widget.setVisible(False) + + ok_btn.clicked.connect(self._on_ok_click) + project_combobox.refreshed.connect(self._on_projects_refresh) + overlay_widget.confirmed.connect(self._on_overlay_confirm) + + controller.register_event_callback( + "selection.project.changed", + self._on_project_selection_change + ) + controller.register_event_callback( + "selection.folder.changed", + self._on_folder_selection_change + ) + controller.register_event_callback( + "selection.task.changed", + self._on_task_selection_change + ) + controller.register_event_callback( + "initial.context.changed", + self._on_init_context_change + ) + controller.register_event_callback( + "strict.changed", + self._on_strict_changed + ) + controller.register_event_callback( + "controller.reset.finished", + self._on_controller_reset + ) + controller.register_event_callback( + "controller.refresh.finished", + self._on_controller_refresh + ) + + # Set stylehseet and resize window on first show + self._first_show = True + self._visible = False + + self._controller = controller + + self._project_combobox = project_combobox + self._folders_widget = folders_widget + self._tasks_widget = tasks_widget + + self._ok_btn = ok_btn + + self._overlay_widget = overlay_widget + + self._apply_strict_changes(self.is_strict()) + + def is_strict(self): + return self._controller.is_strict() + + def showEvent(self, event): + """Override show event to do some callbacks.""" + super(ContextDialog, self).showEvent(event) + self._visible = True + + if self._first_show: + self._first_show = False + # Set stylesheet and resize + self.setStyleSheet(style.load_stylesheet()) + self.resize(600, 700) + center_window(self) + self._controller.refresh() + + initial_context = self._controller.get_initial_context() + self._set_init_context(initial_context) + self._overlay_widget.resize(self.size()) + + def resizeEvent(self, event): + super(ContextDialog, self).resizeEvent(event) + self._overlay_widget.resize(self.size()) + + def closeEvent(self, event): + """Ignore close event if is in strict state and context is not done.""" + if self.is_strict() and not self._ok_btn.isEnabled(): + # Allow to close window when initial context is not valid + if self._controller.is_initial_context_valid(): + event.ignore() + return + + if self.is_strict(): + self._controller.confirm_selection() + self._visible = False + super(ContextDialog, self).closeEvent(event) + + def set_strict(self, enabled): + """Change strictness of dialog.""" + + self._controller.set_strict(enabled) + + def refresh(self): + """Refresh all widget one by one. + + When asset refresh is triggered we have to wait when is done so + this method continues with `_on_asset_widget_refresh_finished`. + """ + + self._controller.reset() + + def get_context(self): + """Result of dialog.""" + return self._controller.get_selected_context() + + def set_context(self, project_name=None, asset_name=None): + """Set context which will be used and locked in dialog.""" + + self._controller.set_initial_context(project_name, asset_name) + + def _on_projects_refresh(self): + initial_context = self._controller.get_initial_context() + self._controller.set_expected_selection( + initial_context["project_name"], + initial_context["folder_id"] + ) + + def _on_overlay_confirm(self): + self.close() + + def _on_ok_click(self): + # Store values to output + self._controller.confirm_selection() + # Close dialog + self.accept() + + def _on_project_selection_change(self, event): + self._on_selection_change( + event["project_name"], + ) + + def _on_folder_selection_change(self, event): + self._on_selection_change( + event["project_name"], + event["folder_id"], + ) + + def _on_task_selection_change(self, event): + self._on_selection_change( + event["project_name"], + event["folder_id"], + event["task_name"], + ) + + def _on_selection_change( + self, project_name, folder_id=None, task_name=None + ): + self._validate_strict(project_name, folder_id, task_name) + + def _on_init_context_change(self, event): + self._set_init_context(event.data) + if self._visible: + self._controller.set_expected_selection( + event["project_name"], event["folder_id"] + ) + + def _set_init_context(self, init_context): + project_name = init_context["project_name"] + if not init_context["valid"]: + self._overlay_widget.setVisible(True) + self._overlay_widget.set_context( + project_name, + init_context["folder_label"], + init_context["project_found"], + init_context["folder_found"], + init_context["tasks_found"] + ) + return + + self._overlay_widget.setVisible(False) + if project_name: + self._project_combobox.setEnabled(False) + if init_context["folder_id"]: + self._folders_widget.setEnabled(False) + else: + self._project_combobox.setEnabled(True) + self._folders_widget.setEnabled(True) + + def _on_strict_changed(self, event): + self._apply_strict_changes(event["strict"]) + + def _on_controller_reset(self): + self._apply_strict_changes(self.is_strict()) + self._project_combobox.refresh() + + def _on_controller_refresh(self): + self._project_combobox.refresh() + + def _apply_strict_changes(self, is_strict): + if not is_strict: + if not self._ok_btn.isEnabled(): + self._ok_btn.setEnabled(True) + return + context = self._controller.get_selected_context() + self._validate_strict( + context["project_name"], + context["folder_id"], + context["task_name"] + ) + + def _validate_strict(self, project_name, folder_id, task_name): + if not self.is_strict(): + return + + enabled = True + if not project_name or not folder_id or not task_name: + enabled = False + self._ok_btn.setEnabled(enabled) + + +def main( + path_to_store, + project_name=None, + asset_name=None, + strict=True +): + # Run Qt application + app = get_openpype_qt_app() + controller = ContextDialogController() + controller.set_strict(strict) + controller.set_initial_context(project_name, asset_name) + controller.set_output_json_path(path_to_store) + window = ContextDialog(controller=controller) + window.show() + app.exec_() + + # Get result from window + data = window.get_context() + + # Make sure json filepath directory exists + file_dir = os.path.dirname(path_to_store) + if not os.path.exists(file_dir): + os.makedirs(file_dir) + + # Store result into json file + with open(path_to_store, "w") as stream: + json.dump(data, stream) diff --git a/openpype/tools/context_dialog/_openpype_window.py b/openpype/tools/context_dialog/_openpype_window.py new file mode 100644 index 0000000000..d370772a7f --- /dev/null +++ b/openpype/tools/context_dialog/_openpype_window.py @@ -0,0 +1,396 @@ +import os +import json + +from qtpy import QtWidgets, QtCore, QtGui + +from openpype import style +from openpype.pipeline import AvalonMongoDB +from openpype.tools.utils.lib import center_window, get_openpype_qt_app +from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget +from openpype.tools.utils.constants import ( + PROJECT_NAME_ROLE +) +from openpype.tools.utils.tasks_widget import TasksWidget +from openpype.tools.utils.models import ( + ProjectModel, + ProjectSortFilterProxy +) + + +class ContextDialog(QtWidgets.QDialog): + """Dialog to select a context. + + Context has 3 parts: + - Project + - Asset + - Task + + It is possible to predefine project and asset. In that case their widgets + will have passed preselected values and will be disabled. + """ + def __init__(self, parent=None): + super(ContextDialog, self).__init__(parent) + + self.setWindowTitle("Select Context") + self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) + + # Enable minimize and maximize for app + window_flags = QtCore.Qt.Window + if not parent: + window_flags |= QtCore.Qt.WindowStaysOnTopHint + self.setWindowFlags(window_flags) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + dbcon = AvalonMongoDB() + + # UI initialization + main_splitter = QtWidgets.QSplitter(self) + + # Left side widget contains project combobox and asset widget + left_side_widget = QtWidgets.QWidget(main_splitter) + + project_combobox = QtWidgets.QComboBox(left_side_widget) + # Styled delegate to propagate stylessheet + project_delegate = QtWidgets.QStyledItemDelegate(project_combobox) + project_combobox.setItemDelegate(project_delegate) + # Project model with only active projects without default item + project_model = ProjectModel( + dbcon, + only_active=True, + add_default_project=False + ) + # Sorting proxy model + project_proxy = ProjectSortFilterProxy() + project_proxy.setSourceModel(project_model) + project_combobox.setModel(project_proxy) + + # Assets widget + assets_widget = SingleSelectAssetsWidget( + dbcon, parent=left_side_widget + ) + + left_side_layout = QtWidgets.QVBoxLayout(left_side_widget) + left_side_layout.setContentsMargins(0, 0, 0, 0) + left_side_layout.addWidget(project_combobox) + left_side_layout.addWidget(assets_widget) + + # Right side of window contains only tasks + tasks_widget = TasksWidget(dbcon, main_splitter) + + # Add widgets to main splitter + main_splitter.addWidget(left_side_widget) + main_splitter.addWidget(tasks_widget) + + # Set stretch of both sides + main_splitter.setStretchFactor(0, 7) + main_splitter.setStretchFactor(1, 3) + + # Add confimation button to bottom right + ok_btn = QtWidgets.QPushButton("OK", self) + + buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.setContentsMargins(0, 0, 0, 0) + buttons_layout.addStretch(1) + buttons_layout.addWidget(ok_btn, 0) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(main_splitter, 1) + main_layout.addLayout(buttons_layout, 0) + + # Timer which will trigger asset refresh + # - this is needed because asset widget triggers + # finished refresh before hides spin box so we need to trigger + # refreshing in small offset if we want re-refresh asset widget + assets_timer = QtCore.QTimer() + assets_timer.setInterval(50) + assets_timer.setSingleShot(True) + + assets_timer.timeout.connect(self._on_asset_refresh_timer) + + project_combobox.currentIndexChanged.connect( + self._on_project_combo_change + ) + assets_widget.selection_changed.connect(self._on_asset_change) + assets_widget.refresh_triggered.connect(self._on_asset_refresh_trigger) + assets_widget.refreshed.connect(self._on_asset_widget_refresh_finished) + tasks_widget.task_changed.connect(self._on_task_change) + ok_btn.clicked.connect(self._on_ok_click) + + self._dbcon = dbcon + + self._project_combobox = project_combobox + self._project_model = project_model + self._project_proxy = project_proxy + self._project_delegate = project_delegate + + self._assets_widget = assets_widget + + self._tasks_widget = tasks_widget + + self._ok_btn = ok_btn + + self._strict = False + + # Values set by `set_context` method + self._set_context_project = None + self._set_context_asset = None + + # Requirements for asset widget refresh + self._assets_timer = assets_timer + self._rerefresh_assets = True + self._assets_refreshing = False + + # Set stylehseet and resize window on first show + self._first_show = True + + # Helper attributes for handling of refresh + self._ignore_value_changes = False + self._refresh_on_next_show = True + + # Output of dialog + self._context_to_store = { + "project": None, + "asset": None, + "task": None + } + + def closeEvent(self, event): + """Ignore close event if is in strict state and context is not done.""" + if self._strict and not self._ok_btn.isEnabled(): + event.ignore() + return + + if self._strict: + self._confirm_values() + super(ContextDialog, self).closeEvent(event) + + def set_strict(self, strict): + """Change strictness of dialog.""" + self._strict = strict + self._validate_strict() + + def _set_refresh_on_next_show(self): + """Refresh will be called on next showEvent. + + If window is already visible then just execute refresh. + """ + self._refresh_on_next_show = True + if self.isVisible(): + self.refresh() + + def _refresh_assets(self): + """Trigger refreshing of asset widget. + + This will set mart to rerefresh asset when current refreshing is done + or do it immidietely if asset widget is not refreshing at the time. + """ + if self._assets_refreshing: + self._rerefresh_assets = True + else: + self._on_asset_refresh_timer() + + def showEvent(self, event): + """Override show event to do some callbacks.""" + super(ContextDialog, self).showEvent(event) + if self._first_show: + self._first_show = False + # Set stylesheet and resize + self.setStyleSheet(style.load_stylesheet()) + self.resize(600, 700) + center_window(self) + + if self._refresh_on_next_show: + self.refresh() + + def refresh(self): + """Refresh all widget one by one. + + When asset refresh is triggered we have to wait when is done so + this method continues with `_on_asset_widget_refresh_finished`. + """ + # Change state of refreshing (no matter how refresh was called) + self._refresh_on_next_show = False + + # Ignore changes of combobox and asset widget + self._ignore_value_changes = True + + # Get current project name to be able set it afterwards + select_project_name = self._dbcon.Session.get("AVALON_PROJECT") + # Trigger project refresh + self._project_model.refresh() + # Sort projects + self._project_proxy.sort(0) + + # Disable combobox if project was passed to `set_context` + if self._set_context_project: + select_project_name = self._set_context_project + self._project_combobox.setEnabled(False) + else: + # Find new project to select + self._project_combobox.setEnabled(True) + if ( + select_project_name is None + and self._project_proxy.rowCount() > 0 + ): + index = self._project_proxy.index(0, 0) + select_project_name = index.data(PROJECT_NAME_ROLE) + + self._ignore_value_changes = False + + idx = self._project_combobox.findText(select_project_name) + if idx >= 0: + self._project_combobox.setCurrentIndex(idx) + self._dbcon.Session["AVALON_PROJECT"] = ( + self._project_combobox.currentText() + ) + + # Trigger asset refresh + self._refresh_assets() + + def _on_asset_refresh_timer(self): + """This is only way how to trigger refresh asset widget. + + Use `_refresh_assets` method to refresh asset widget. + """ + self._assets_widget.refresh() + + def _on_asset_widget_refresh_finished(self): + """Catch when asset widget finished refreshing.""" + # If should refresh again then skip all other callbacks and trigger + # assets timer directly. + self._assets_refreshing = False + if self._rerefresh_assets: + self._rerefresh_assets = False + self._assets_timer.start() + return + + self._ignore_value_changes = True + if self._set_context_asset: + self._dbcon.Session["AVALON_ASSET"] = self._set_context_asset + self._assets_widget.setEnabled(False) + self._assets_widget.select_asset_by_name(self._set_context_asset) + self._set_asset_to_tasks_widget() + else: + self._assets_widget.setEnabled(True) + self._assets_widget.set_current_asset_btn_visibility(False) + + # Refresh tasks + self._tasks_widget.refresh() + + self._ignore_value_changes = False + + self._validate_strict() + + def _on_project_combo_change(self): + if self._ignore_value_changes: + return + project_name = self._project_combobox.currentText() + + if self._dbcon.Session.get("AVALON_PROJECT") == project_name: + return + + self._dbcon.Session["AVALON_PROJECT"] = project_name + + self._refresh_assets() + self._validate_strict() + + def _on_asset_refresh_trigger(self): + self._assets_refreshing = True + self._on_asset_change() + + def _on_asset_change(self): + """Selected assets have changed""" + if self._ignore_value_changes: + return + self._set_asset_to_tasks_widget() + + def _on_task_change(self): + self._validate_strict() + + def _set_asset_to_tasks_widget(self): + asset_id = self._assets_widget.get_selected_asset_id() + + self._tasks_widget.set_asset_id(asset_id) + + def _confirm_values(self): + """Store values to output.""" + self._context_to_store["project"] = self.get_selected_project() + self._context_to_store["asset"] = self.get_selected_asset() + self._context_to_store["task"] = self.get_selected_task() + + def _on_ok_click(self): + # Store values to output + self._confirm_values() + # Close dialog + self.accept() + + def get_selected_project(self): + """Get selected project.""" + return self._project_combobox.currentText() + + def get_selected_asset(self): + """Currently selected asset in asset widget.""" + return self._assets_widget.get_selected_asset_name() + + def get_selected_task(self): + """Currently selected task.""" + return self._tasks_widget.get_selected_task_name() + + def _validate_strict(self): + if not self._strict: + if not self._ok_btn.isEnabled(): + self._ok_btn.setEnabled(True) + return + + enabled = True + if not self._set_context_project and not self.get_selected_project(): + enabled = False + elif not self._set_context_asset and not self.get_selected_asset(): + enabled = False + elif not self.get_selected_task(): + enabled = False + self._ok_btn.setEnabled(enabled) + + def set_context(self, project_name=None, asset_name=None): + """Set context which will be used and locked in dialog.""" + if project_name is None: + asset_name = None + + self._set_context_project = project_name + self._set_context_asset = asset_name + + self._context_to_store["project"] = project_name + self._context_to_store["asset"] = asset_name + + self._set_refresh_on_next_show() + + def get_context(self): + """Result of dialog.""" + return self._context_to_store + + +def main( + path_to_store, + project_name=None, + asset_name=None, + strict=True +): + # Run Qt application + app = get_openpype_qt_app() + window = ContextDialog() + window.set_strict(strict) + window.set_context(project_name, asset_name) + window.show() + app.exec_() + + # Get result from window + data = window.get_context() + + # Make sure json filepath directory exists + file_dir = os.path.dirname(path_to_store) + if not os.path.exists(file_dir): + os.makedirs(file_dir) + + # Store result into json file + with open(path_to_store, "w") as stream: + json.dump(data, stream) diff --git a/openpype/tools/context_dialog/window.py b/openpype/tools/context_dialog/window.py index 4fe41c9949..15b90463da 100644 --- a/openpype/tools/context_dialog/window.py +++ b/openpype/tools/context_dialog/window.py @@ -1,396 +1,12 @@ -import os -import json +from openpype import AYON_SERVER_ENABLED -from qtpy import QtWidgets, QtCore, QtGui +if AYON_SERVER_ENABLED: + from ._ayon_window import ContextDialog, main +else: + from ._openpype_window import ContextDialog, main -from openpype import style -from openpype.pipeline import AvalonMongoDB -from openpype.tools.utils.lib import center_window, get_openpype_qt_app -from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget -from openpype.tools.utils.constants import ( - PROJECT_NAME_ROLE + +__all__ = ( + "ContextDialog", + "main", ) -from openpype.tools.utils.tasks_widget import TasksWidget -from openpype.tools.utils.models import ( - ProjectModel, - ProjectSortFilterProxy -) - - -class ContextDialog(QtWidgets.QDialog): - """Dialog to select a context. - - Context has 3 parts: - - Project - - Aseet - - Task - - It is possible to predefine project and asset. In that case their widgets - will have passed preselected values and will be disabled. - """ - def __init__(self, parent=None): - super(ContextDialog, self).__init__(parent) - - self.setWindowTitle("Select Context") - self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) - - # Enable minimize and maximize for app - window_flags = QtCore.Qt.Window - if not parent: - window_flags |= QtCore.Qt.WindowStaysOnTopHint - self.setWindowFlags(window_flags) - self.setFocusPolicy(QtCore.Qt.StrongFocus) - - dbcon = AvalonMongoDB() - - # UI initialization - main_splitter = QtWidgets.QSplitter(self) - - # Left side widget contains project combobox and asset widget - left_side_widget = QtWidgets.QWidget(main_splitter) - - project_combobox = QtWidgets.QComboBox(left_side_widget) - # Styled delegate to propagate stylessheet - project_delegate = QtWidgets.QStyledItemDelegate(project_combobox) - project_combobox.setItemDelegate(project_delegate) - # Project model with only active projects without default item - project_model = ProjectModel( - dbcon, - only_active=True, - add_default_project=False - ) - # Sorting proxy model - project_proxy = ProjectSortFilterProxy() - project_proxy.setSourceModel(project_model) - project_combobox.setModel(project_proxy) - - # Assets widget - assets_widget = SingleSelectAssetsWidget( - dbcon, parent=left_side_widget - ) - - left_side_layout = QtWidgets.QVBoxLayout(left_side_widget) - left_side_layout.setContentsMargins(0, 0, 0, 0) - left_side_layout.addWidget(project_combobox) - left_side_layout.addWidget(assets_widget) - - # Right side of window contains only tasks - tasks_widget = TasksWidget(dbcon, main_splitter) - - # Add widgets to main splitter - main_splitter.addWidget(left_side_widget) - main_splitter.addWidget(tasks_widget) - - # Set stretch of both sides - main_splitter.setStretchFactor(0, 7) - main_splitter.setStretchFactor(1, 3) - - # Add confimation button to bottom right - ok_btn = QtWidgets.QPushButton("OK", self) - - buttons_layout = QtWidgets.QHBoxLayout() - buttons_layout.setContentsMargins(0, 0, 0, 0) - buttons_layout.addStretch(1) - buttons_layout.addWidget(ok_btn, 0) - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addWidget(main_splitter, 1) - main_layout.addLayout(buttons_layout, 0) - - # Timer which will trigger asset refresh - # - this is needed because asset widget triggers - # finished refresh before hides spin box so we need to trigger - # refreshing in small offset if we want re-refresh asset widget - assets_timer = QtCore.QTimer() - assets_timer.setInterval(50) - assets_timer.setSingleShot(True) - - assets_timer.timeout.connect(self._on_asset_refresh_timer) - - project_combobox.currentIndexChanged.connect( - self._on_project_combo_change - ) - assets_widget.selection_changed.connect(self._on_asset_change) - assets_widget.refresh_triggered.connect(self._on_asset_refresh_trigger) - assets_widget.refreshed.connect(self._on_asset_widget_refresh_finished) - tasks_widget.task_changed.connect(self._on_task_change) - ok_btn.clicked.connect(self._on_ok_click) - - self._dbcon = dbcon - - self._project_combobox = project_combobox - self._project_model = project_model - self._project_proxy = project_proxy - self._project_delegate = project_delegate - - self._assets_widget = assets_widget - - self._tasks_widget = tasks_widget - - self._ok_btn = ok_btn - - self._strict = False - - # Values set by `set_context` method - self._set_context_project = None - self._set_context_asset = None - - # Requirements for asset widget refresh - self._assets_timer = assets_timer - self._rerefresh_assets = True - self._assets_refreshing = False - - # Set stylehseet and resize window on first show - self._first_show = True - - # Helper attributes for handling of refresh - self._ignore_value_changes = False - self._refresh_on_next_show = True - - # Output of dialog - self._context_to_store = { - "project": None, - "asset": None, - "task": None - } - - def closeEvent(self, event): - """Ignore close event if is in strict state and context is not done.""" - if self._strict and not self._ok_btn.isEnabled(): - event.ignore() - return - - if self._strict: - self._confirm_values() - super(ContextDialog, self).closeEvent(event) - - def set_strict(self, strict): - """Change strictness of dialog.""" - self._strict = strict - self._validate_strict() - - def _set_refresh_on_next_show(self): - """Refresh will be called on next showEvent. - - If window is already visible then just execute refresh. - """ - self._refresh_on_next_show = True - if self.isVisible(): - self.refresh() - - def _refresh_assets(self): - """Trigger refreshing of asset widget. - - This will set mart to rerefresh asset when current refreshing is done - or do it immidietely if asset widget is not refreshing at the time. - """ - if self._assets_refreshing: - self._rerefresh_assets = True - else: - self._on_asset_refresh_timer() - - def showEvent(self, event): - """Override show event to do some callbacks.""" - super(ContextDialog, self).showEvent(event) - if self._first_show: - self._first_show = False - # Set stylesheet and resize - self.setStyleSheet(style.load_stylesheet()) - self.resize(600, 700) - center_window(self) - - if self._refresh_on_next_show: - self.refresh() - - def refresh(self): - """Refresh all widget one by one. - - When asset refresh is triggered we have to wait when is done so - this method continues with `_on_asset_widget_refresh_finished`. - """ - # Change state of refreshing (no matter how refresh was called) - self._refresh_on_next_show = False - - # Ignore changes of combobox and asset widget - self._ignore_value_changes = True - - # Get current project name to be able set it afterwards - select_project_name = self._dbcon.Session.get("AVALON_PROJECT") - # Trigger project refresh - self._project_model.refresh() - # Sort projects - self._project_proxy.sort(0) - - # Disable combobox if project was passed to `set_context` - if self._set_context_project: - select_project_name = self._set_context_project - self._project_combobox.setEnabled(False) - else: - # Find new project to select - self._project_combobox.setEnabled(True) - if ( - select_project_name is None - and self._project_proxy.rowCount() > 0 - ): - index = self._project_proxy.index(0, 0) - select_project_name = index.data(PROJECT_NAME_ROLE) - - self._ignore_value_changes = False - - idx = self._project_combobox.findText(select_project_name) - if idx >= 0: - self._project_combobox.setCurrentIndex(idx) - self._dbcon.Session["AVALON_PROJECT"] = ( - self._project_combobox.currentText() - ) - - # Trigger asset refresh - self._refresh_assets() - - def _on_asset_refresh_timer(self): - """This is only way how to trigger refresh asset widget. - - Use `_refresh_assets` method to refresh asset widget. - """ - self._assets_widget.refresh() - - def _on_asset_widget_refresh_finished(self): - """Catch when asset widget finished refreshing.""" - # If should refresh again then skip all other callbacks and trigger - # assets timer directly. - self._assets_refreshing = False - if self._rerefresh_assets: - self._rerefresh_assets = False - self._assets_timer.start() - return - - self._ignore_value_changes = True - if self._set_context_asset: - self._dbcon.Session["AVALON_ASSET"] = self._set_context_asset - self._assets_widget.setEnabled(False) - self._assets_widget.select_assets(self._set_context_asset) - self._set_asset_to_tasks_widget() - else: - self._assets_widget.setEnabled(True) - self._assets_widget.set_current_asset_btn_visibility(False) - - # Refresh tasks - self._tasks_widget.refresh() - - self._ignore_value_changes = False - - self._validate_strict() - - def _on_project_combo_change(self): - if self._ignore_value_changes: - return - project_name = self._project_combobox.currentText() - - if self._dbcon.Session.get("AVALON_PROJECT") == project_name: - return - - self._dbcon.Session["AVALON_PROJECT"] = project_name - - self._refresh_assets() - self._validate_strict() - - def _on_asset_refresh_trigger(self): - self._assets_refreshing = True - self._on_asset_change() - - def _on_asset_change(self): - """Selected assets have changed""" - if self._ignore_value_changes: - return - self._set_asset_to_tasks_widget() - - def _on_task_change(self): - self._validate_strict() - - def _set_asset_to_tasks_widget(self): - asset_id = self._assets_widget.get_selected_asset_id() - - self._tasks_widget.set_asset_id(asset_id) - - def _confirm_values(self): - """Store values to output.""" - self._context_to_store["project"] = self.get_selected_project() - self._context_to_store["asset"] = self.get_selected_asset() - self._context_to_store["task"] = self.get_selected_task() - - def _on_ok_click(self): - # Store values to output - self._confirm_values() - # Close dialog - self.accept() - - def get_selected_project(self): - """Get selected project.""" - return self._project_combobox.currentText() - - def get_selected_asset(self): - """Currently selected asset in asset widget.""" - return self._assets_widget.get_selected_asset_name() - - def get_selected_task(self): - """Currently selected task.""" - return self._tasks_widget.get_selected_task_name() - - def _validate_strict(self): - if not self._strict: - if not self._ok_btn.isEnabled(): - self._ok_btn.setEnabled(True) - return - - enabled = True - if not self._set_context_project and not self.get_selected_project(): - enabled = False - elif not self._set_context_asset and not self.get_selected_asset(): - enabled = False - elif not self.get_selected_task(): - enabled = False - self._ok_btn.setEnabled(enabled) - - def set_context(self, project_name=None, asset_name=None): - """Set context which will be used and locked in dialog.""" - if project_name is None: - asset_name = None - - self._set_context_project = project_name - self._set_context_asset = asset_name - - self._context_to_store["project"] = project_name - self._context_to_store["asset"] = asset_name - - self._set_refresh_on_next_show() - - def get_context(self): - """Result of dialog.""" - return self._context_to_store - - -def main( - path_to_store, - project_name=None, - asset_name=None, - strict=True -): - # Run Qt application - app = get_openpype_qt_app() - window = ContextDialog() - window.set_strict(strict) - window.set_context(project_name, asset_name) - window.show() - app.exec_() - - # Get result from window - data = window.get_context() - - # Make sure json filepath directory exists - file_dir = os.path.dirname(path_to_store) - if not os.path.exists(file_dir): - os.makedirs(file_dir) - - # Store result into json file - with open(path_to_store, "w") as stream: - json.dump(data, stream) From b374bf7eaebcb151a38447593ab54864e4cc65ba Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 14:47:21 +0200 Subject: [PATCH 16/45] fix storing of data --- openpype/tools/context_dialog/_ayon_window.py | 34 +++++-------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/openpype/tools/context_dialog/_ayon_window.py b/openpype/tools/context_dialog/_ayon_window.py index 6514780236..04fd3495e1 100644 --- a/openpype/tools/context_dialog/_ayon_window.py +++ b/openpype/tools/context_dialog/_ayon_window.py @@ -356,27 +356,17 @@ class ContextDialogController: "task_name": None, } - def window_closed(self): - if not self._confirmed and not self._is_strict: - return - - self._store_output() - def confirm_selection(self): self._confirmed = True - self._emit_event( - "selection.confirmed", - {"confirmed": True} - ) - def _store_output(self): + def store_output(self): if not self._output_path: return dirpath = os.path.dirname(self._output_path) os.makedirs(dirpath, exist_ok=True) with open(self._output_path, "w") as stream: - json.dump(self.get_selected_context(), stream) + json.dump(self.get_selected_context(), stream, indent=4) def _get_event_system(self): """Inner event system for workfiles tool controller. @@ -627,7 +617,7 @@ class ContextDialog(QtWidgets.QDialog): return if self.is_strict(): - self._controller.confirm_selection() + self._confirm_selection() self._visible = False super(ContextDialog, self).closeEvent(event) @@ -666,10 +656,13 @@ class ContextDialog(QtWidgets.QDialog): def _on_ok_click(self): # Store values to output - self._controller.confirm_selection() + self._confirm_selection() # Close dialog self.accept() + def _confirm_selection(self): + self._controller.confirm_selection() + def _on_project_selection_change(self, event): self._on_selection_change( event["project_name"], @@ -769,15 +762,4 @@ def main( window = ContextDialog(controller=controller) window.show() app.exec_() - - # Get result from window - data = window.get_context() - - # Make sure json filepath directory exists - file_dir = os.path.dirname(path_to_store) - if not os.path.exists(file_dir): - os.makedirs(file_dir) - - # Store result into json file - with open(path_to_store, "w") as stream: - json.dump(data, stream) + controller.store_output() From f86874df3de22841587697cc8246eda14ddb4978 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 14:47:46 +0200 Subject: [PATCH 17/45] add select item to projects combobox --- openpype/tools/context_dialog/_ayon_window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/context_dialog/_ayon_window.py b/openpype/tools/context_dialog/_ayon_window.py index 04fd3495e1..07495b7674 100644 --- a/openpype/tools/context_dialog/_ayon_window.py +++ b/openpype/tools/context_dialog/_ayon_window.py @@ -496,6 +496,7 @@ class ContextDialog(QtWidgets.QDialog): parent=left_side_widget, handle_expected_selection=True ) + project_combobox.set_select_item_visible(True) # Assets widget folders_widget = FoldersWidget( From 28bcbc8053133a1a257078052e9c2115167fea0d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 14:48:07 +0200 Subject: [PATCH 18/45] implemented helper function to prepare initial context data --- openpype/tools/context_dialog/_ayon_window.py | 136 +++++++++++------- 1 file changed, 84 insertions(+), 52 deletions(-) diff --git a/openpype/tools/context_dialog/_ayon_window.py b/openpype/tools/context_dialog/_ayon_window.py index 07495b7674..73f9ed139c 100644 --- a/openpype/tools/context_dialog/_ayon_window.py +++ b/openpype/tools/context_dialog/_ayon_window.py @@ -277,52 +277,15 @@ class ContextDialogController: def is_initial_context_valid(self): return self._initial_folder_found and self._initial_project_found - def set_initial_context( - self, project_name=None, asset_name=None, folder_path=None - ): - if project_name is None: - project_found = True - asset_name = None - folder_path = None - - else: - project = ayon_api.get_project(project_name) - project_found = project is not None - - folder_id = None - folder_found = True - folder_label = None - if folder_path: - folder_label = folder_path - folder = ayon_api.get_folder_by_path(project_name, folder_path) - if folder: - folder_id = folder["id"] - else: - folder_found = False - elif asset_name: - folder_label = asset_name - for folder in ayon_api.get_folders( - project_name, folder_names=[asset_name] - ): - folder_id = folder["id"] - break - if not folder_id: - folder_found = False - - tasks_found = True - if folder_found and (folder_path or asset_name): - tasks = list(ayon_api.get_tasks( - project_name, folder_ids=[folder_id], fields=["id"] - )) - if not tasks: - tasks_found = False + def set_initial_context(self, project_name=None, asset_name=None): + result = self._prepare_initial_context(project_name, asset_name) self._initial_project_name = project_name - self._initial_folder_id = folder_id - self._initial_folder_label = folder_label - self._initial_folder_found = project_found - self._initial_folder_found = folder_found - self._initial_tasks_found = tasks_found + self._initial_folder_id = result["folder_id"] + self._initial_folder_label = result["folder_label"] + self._initial_project_found = result["project_found"] + self._initial_folder_found = result["folder_found"] + self._initial_tasks_found = result["tasks_found"] self._emit_event( "initial.context.changed", self.get_initial_context() @@ -345,15 +308,36 @@ class ContextDialogController: # Result of this tool def get_selected_context(self): + project_name = None + folder_id = None + task_id = None + task_name = None + folder_path = None + folder_name = None + if self._confirmed: + project_name = self.get_selected_project_name() + folder_id = self.get_selected_folder_id() + task_id = self.get_selected_task_id() + task_name = self.get_selected_task_name() + + folder_item = None + if folder_id: + folder_item = self._hierarchy_model.get_folder_item( + project_name, folder_id) + + if folder_item: + folder_path = folder_item.path + folder_name = folder_item.name return { - "project": None, - "project_name": None, - "asset": None, - "folder_id": None, - "folder_path": None, - "task": None, - "task_id": None, - "task_name": None, + "project": project_name, + "project_name": project_name, + "asset": folder_name, + "folder_id": folder_id, + "folder_path": folder_path, + "task": task_name, + "task_name": task_name, + "task_id": task_id, + "initial_context_valid": self.is_initial_context_valid(), } def confirm_selection(self): @@ -368,6 +352,54 @@ class ContextDialogController: with open(self._output_path, "w") as stream: json.dump(self.get_selected_context(), stream, indent=4) + def _prepare_initial_context(self, project_name, asset_name): + project_found = True + output = { + "project_found": project_found, + "folder_id": None, + "folder_label": None, + "folder_found": True, + "tasks_found": True, + } + if project_name is None: + asset_name = None + else: + project = ayon_api.get_project(project_name) + project_found = project is not None + output["project_found"] = project_found + if not project_found or not asset_name: + return output + + output["folder_label"] = asset_name + + folder_id = None + folder_found = False + # First try to find by path + folder = ayon_api.get_folder_by_path(project_name, asset_name) + # Try to find by name if folder was not found by path + # - prevent to query by name if 'asset_name' contains '/' + if not folder and "/" not in asset_name: + folder = next( + ayon_api.get_folders( + project_name, folder_names=[asset_name], fields=["id"]), + None + ) + + if folder: + folder_id = folder["id"] + folder_found = True + + output["folder_id"] = folder_id + output["folder_found"] = folder_found + if not folder_found: + return output + + tasks = list(ayon_api.get_tasks( + project_name, folder_ids=[folder_id], fields=["id"] + )) + output["tasks_found"] = bool(tasks) + return output + def _get_event_system(self): """Inner event system for workfiles tool controller. From a62718dc72b62a1823513795695a55b0601bb5a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 14:48:24 +0200 Subject: [PATCH 19/45] project name is in quotes --- openpype/tools/context_dialog/_ayon_window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/context_dialog/_ayon_window.py b/openpype/tools/context_dialog/_ayon_window.py index 73f9ed139c..f347978392 100644 --- a/openpype/tools/context_dialog/_ayon_window.py +++ b/openpype/tools/context_dialog/_ayon_window.py @@ -468,7 +468,8 @@ class InvalidContextOverlay(QtWidgets.QFrame): lines = [] if not project_found: lines.extend([ - "Requested project {} was not found...".format(project_name), + "Requested project '{}' was not found...".format( + project_name), ]) elif not folder_found: From 2f15dca3f50bbf687efd86bfb8339c284fd6aa7a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 13 Oct 2023 14:51:59 +0200 Subject: [PATCH 20/45] removed 'window.py' --- openpype/tools/context_dialog/__init__.py | 12 +++++++----- openpype/tools/context_dialog/window.py | 12 ------------ 2 files changed, 7 insertions(+), 17 deletions(-) delete mode 100644 openpype/tools/context_dialog/window.py diff --git a/openpype/tools/context_dialog/__init__.py b/openpype/tools/context_dialog/__init__.py index 9b10baf903..15b90463da 100644 --- a/openpype/tools/context_dialog/__init__.py +++ b/openpype/tools/context_dialog/__init__.py @@ -1,10 +1,12 @@ -from .window import ( - ContextDialog, - main -) +from openpype import AYON_SERVER_ENABLED + +if AYON_SERVER_ENABLED: + from ._ayon_window import ContextDialog, main +else: + from ._openpype_window import ContextDialog, main __all__ = ( "ContextDialog", - "main" + "main", ) diff --git a/openpype/tools/context_dialog/window.py b/openpype/tools/context_dialog/window.py deleted file mode 100644 index 15b90463da..0000000000 --- a/openpype/tools/context_dialog/window.py +++ /dev/null @@ -1,12 +0,0 @@ -from openpype import AYON_SERVER_ENABLED - -if AYON_SERVER_ENABLED: - from ._ayon_window import ContextDialog, main -else: - from ._openpype_window import ContextDialog, main - - -__all__ = ( - "ContextDialog", - "main", -) From 00fb722089a80dae83fe89b387ddcc481053f053 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 31 Oct 2023 13:55:22 +0100 Subject: [PATCH 21/45] use AYON username for user in template data --- openpype/lib/local_settings.py | 6 ++++++ openpype/plugins/publish/collect_current_pype_user.py | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/openpype/lib/local_settings.py b/openpype/lib/local_settings.py index dae6e074af..9b780fd88a 100644 --- a/openpype/lib/local_settings.py +++ b/openpype/lib/local_settings.py @@ -611,6 +611,12 @@ def get_openpype_username(): settings and last option is to use `getpass.getuser()` which returns machine username. """ + + if AYON_SERVER_ENABLED: + import ayon_api + + return ayon_api.get_user()["name"] + username = os.environ.get("OPENPYPE_USERNAME") if not username: local_settings = get_local_settings() diff --git a/openpype/plugins/publish/collect_current_pype_user.py b/openpype/plugins/publish/collect_current_pype_user.py index 2d507ba292..5c0c4fc82e 100644 --- a/openpype/plugins/publish/collect_current_pype_user.py +++ b/openpype/plugins/publish/collect_current_pype_user.py @@ -1,4 +1,6 @@ import pyblish.api + +from openpype import AYON_SERVER_ENABLED from openpype.lib import get_openpype_username @@ -7,7 +9,11 @@ class CollectCurrentUserPype(pyblish.api.ContextPlugin): # Order must be after default pyblish-base CollectCurrentUser order = pyblish.api.CollectorOrder + 0.001 - label = "Collect Pype User" + label = ( + "Collect AYON User" + if AYON_SERVER_ENABLED + else "Collect OpenPype User" + ) def process(self, context): user = get_openpype_username() From 711976e68586a6110b04a0b7d650effca08dcd30 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 31 Oct 2023 18:05:20 +0200 Subject: [PATCH 22/45] fix bug when loading shelf files --- openpype/hosts/houdini/api/shelves.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index 4b5ebd4202..0afc737665 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -44,7 +44,7 @@ def generate_shelves(): "{}".format(shelf_set_os_filepath)) continue - hou.shelves.newShelfSet(file_path=shelf_set_os_filepath) + hou.shelves.loadFile(shelf_set_os_filepath) continue shelf_set_name = shelf_set_config.get('shelf_set_name') From dd070c6fcc0ded177fefebcf64f5272bb4d007d3 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 31 Oct 2023 18:06:06 +0200 Subject: [PATCH 23/45] Enhance shelves settings visual appeal --- .../schemas/schema_houdini_scriptshelf.json | 24 ++++++++++++------- .../houdini/server/settings/shelves.py | 14 +++++++---- server_addon/houdini/server/version.py | 2 +- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json index 35d768843d..f45377c8b4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json @@ -7,22 +7,30 @@ "object_type": { "type": "dict", "children": [ + { + "type": "label", + "label": "Option 1: Add a .shelf file" + }, + { + "type": "path", + "key": "shelf_set_source_path", + "label": "Shelf Set Path", + "multipath": false, + "multiplatform": true + }, + { + "type": "label", + "label": "OR Option 2: Add Shelf Set Name and Shelves Definitions" + }, { "type": "text", "key": "shelf_set_name", "label": "Shelf Set Name" }, - { - "type": "path", - "key": "shelf_set_source_path", - "label": "Shelf Set Path (optional)", - "multipath": false, - "multiplatform": true - }, { "type": "list", "key": "shelf_definition", - "label": "Shelves", + "label": "Shelves Definitions", "use_label_wrap": true, "object_type": { "type": "dict", diff --git a/server_addon/houdini/server/settings/shelves.py b/server_addon/houdini/server/settings/shelves.py index 8d0512bdeb..e02ddf1c34 100644 --- a/server_addon/houdini/server/settings/shelves.py +++ b/server_addon/houdini/server/settings/shelves.py @@ -24,14 +24,18 @@ class ShelfDefinitionModel(BaseSettingsModel): class ShelvesModel(BaseSettingsModel): _layout = "expanded" - shelf_set_name: str = Field("", title="Shelfs set name") - shelf_set_source_path: MultiplatformPathModel = Field( default_factory=MultiplatformPathModel, - title="Shelf Set Path (optional)" + title="Shelf Set Path", + section="Option 1: Add a .shelf file." + ) + shelf_set_name: str = Field( + "", + title="Shelf Set Name", + section=("OR Option 2: Add Shelf Set Name " + "and Shelves Definitions.") ) - shelf_definition: list[ShelfDefinitionModel] = Field( default_factory=list, - title="Shelf Definitions" + title="Shelves Definitions" ) diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 01ef12070d..6cd38b7465 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.6" +__version__ = "0.2.7" From 918770f817e5ab1a1c930bae81a441609b4d5ce2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 31 Oct 2023 18:57:28 +0100 Subject: [PATCH 24/45] 'get_current_context_template_data' returns same values as base function 'get_template_data' --- openpype/pipeline/context_tools.py | 93 +++++++----------------------- 1 file changed, 22 insertions(+), 71 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 13630ae7ca..5afdb30f7b 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -25,10 +25,7 @@ from openpype.tests.lib import is_in_tests from .publish.lib import filter_pyblish_plugins from .anatomy import Anatomy -from .template_data import ( - get_template_data_with_names, - get_template_data -) +from .template_data import get_template_data_with_names from .workfile import ( get_workfile_template_key, get_custom_workfile_template_by_string_context, @@ -483,6 +480,27 @@ def get_template_data_from_session(session=None, system_settings=None): ) +def get_current_context_template_data(system_settings=None): + """Prepare template data for current context. + + Args: + system_settings (Optional[Dict[str, Any]]): Prepared system settings. + + Returns: + Dict[str, Any] Template data for current context. + """ + + context = get_current_context() + project_name = context["project_name"] + asset_name = context["asset_name"] + task_name = context["task_name"] + host_name = get_current_host_name() + + return get_template_data_with_names( + project_name, asset_name, task_name, host_name, system_settings + ) + + def get_workdir_from_session(session=None, template_key=None): """Template data for template fill from session keys. @@ -661,70 +679,3 @@ def get_process_id(): if _process_id is None: _process_id = str(uuid.uuid4()) return _process_id - - -def get_current_context_template_data(): - """Template data for template fill from current context - - Returns: - Dict[str, Any] of the following tokens and their values - Supported Tokens: - - Regular Tokens - - app - - user - - asset - - parent - - hierarchy - - folder[name] - - root[work, ...] - - studio[code, name] - - project[code, name] - - task[type, name, short] - - - Context Specific Tokens - - assetData[frameStart] - - assetData[frameEnd] - - assetData[handleStart] - - assetData[handleEnd] - - assetData[frameStartHandle] - - assetData[frameEndHandle] - - assetData[resolutionHeight] - - assetData[resolutionWidth] - - """ - - # pre-prepare get_template_data args - current_context = get_current_context() - project_name = current_context["project_name"] - asset_name = current_context["asset_name"] - anatomy = Anatomy(project_name) - - # prepare get_template_data args - project_doc = get_project(project_name) - asset_doc = get_asset_by_name(project_name, asset_name) - task_name = current_context["task_name"] - host_name = get_current_host_name() - - # get regular template data - template_data = get_template_data( - project_doc, asset_doc, task_name, host_name - ) - - template_data["root"] = anatomy.roots - - # get context specific vars - asset_data = asset_doc["data"].copy() - - # compute `frameStartHandle` and `frameEndHandle` - if "frameStart" in asset_data and "handleStart" in asset_data: - asset_data["frameStartHandle"] = \ - asset_data["frameStart"] - asset_data["handleStart"] - - if "frameEnd" in asset_data and "handleEnd" in asset_data: - asset_data["frameEndHandle"] = \ - asset_data["frameEnd"] + asset_data["handleEnd"] - - # add assetData - template_data["assetData"] = asset_data - - return template_data From 4a11eed09ba8936351ce8acf1fa06fdd0ef904fb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 31 Oct 2023 18:59:06 +0100 Subject: [PATCH 25/45] implemented new function which does what houdini requires --- openpype/hosts/houdini/api/lib.py | 56 +++++++++++++++++++++++---- openpype/hosts/houdini/api/shelves.py | 5 ++- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index cadeaa8ed4..ac375c56d6 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -11,20 +11,21 @@ import json import six from openpype.lib import StringTemplate -from openpype.client import get_asset_by_name +from openpype.client import get_project, get_asset_by_name from openpype.settings import get_current_project_settings from openpype.pipeline import ( + Anatomy, get_current_project_name, get_current_asset_name, - registered_host -) -from openpype.pipeline.context_tools import ( - get_current_context_template_data, - get_current_project_asset + registered_host, + get_current_context, + get_current_host_name, ) +from openpype.pipeline.create import CreateContext +from openpype.pipeline.template_data import get_template_data +from openpype.pipeline.context_tools import get_current_project_asset from openpype.widgets import popup from openpype.tools.utils.host_tools import get_tool_by_name -from openpype.pipeline.create import CreateContext import hou @@ -804,6 +805,45 @@ def get_camera_from_container(container): return cameras[0] +def get_current_context_template_data_with_asset_data(): + """ + TODOs: + Support both 'assetData' and 'folderData' in future. + """ + + context = get_current_context() + project_name = context["project_name"] + asset_name = context["asset_name"] + task_name = context["task_name"] + host_name = get_current_host_name() + + anatomy = Anatomy(project_name) + project_doc = get_project(project_name) + asset_doc = get_asset_by_name(project_name, asset_name) + + # get context specific vars + asset_data = asset_doc["data"] + + # compute `frameStartHandle` and `frameEndHandle` + frame_start = asset_data.get("frameStart") + frame_end = asset_data.get("frameEnd") + handle_start = asset_data.get("handleStart") + handle_end = asset_data.get("handleEnd") + if frame_start is not None and handle_start is not None: + asset_data["frameStartHandle"] = frame_start - handle_start + + if frame_end is not None and handle_end is not None: + asset_data["frameEndHandle"] = frame_end + handle_end + + template_data = get_template_data( + project_doc, asset_doc, task_name, host_name + ) + template_data["root"] = anatomy.roots + template_data["assetData"] = asset_data + + return template_data + + def get_context_var_changes(): """get context var changes.""" @@ -823,7 +863,7 @@ def get_context_var_changes(): return houdini_vars_to_update # Get Template data - template_data = get_current_context_template_data() + template_data = get_current_context_template_data_with_asset_data() # Set Houdini Vars for item in houdini_vars: diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index 4b5ebd4202..5df45a1f72 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -7,10 +7,11 @@ from openpype.settings import get_project_settings from openpype.pipeline import get_current_project_name from openpype.lib import StringTemplate -from openpype.pipeline.context_tools import get_current_context_template_data import hou +from .lib import get_current_context_template_data_with_asset_data + log = logging.getLogger("openpype.hosts.houdini.shelves") @@ -30,7 +31,7 @@ def generate_shelves(): return # Get Template data - template_data = get_current_context_template_data() + template_data = get_current_context_template_data_with_asset_data() for shelf_set_config in shelves_set_config: shelf_set_filepath = shelf_set_config.get('shelf_set_source_path') From f330f87993d6826b519c7ee949c8c1a2c21e197b Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 10:37:41 +0200 Subject: [PATCH 26/45] Add options to Houdini shelves manager settings --- .../schemas/schema_houdini_scriptshelf.json | 141 +++++++++--------- .../houdini/server/settings/shelves.py | 11 ++ 2 files changed, 85 insertions(+), 67 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json index f45377c8b4..2dfce906b7 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json @@ -5,78 +5,85 @@ "is_group": true, "use_label_wrap": true, "object_type": { - "type": "dict", - "children": [ + "type": "dict-conditional", + "enum_key": "options", + "enum_label": "Options", + "enum_children": [ { - "type": "label", - "label": "Option 1: Add a .shelf file" + + "key": "add_shelf_file", + "label": "Add a .shelf file", + "children": [ + { + "type": "path", + "key": "shelf_set_source_path", + "label": "Shelf Set Path", + "multipath": false, + "multiplatform": true + } + ] }, { - "type": "path", - "key": "shelf_set_source_path", - "label": "Shelf Set Path", - "multipath": false, - "multiplatform": true - }, - { - "type": "label", - "label": "OR Option 2: Add Shelf Set Name and Shelves Definitions" - }, - { - "type": "text", - "key": "shelf_set_name", - "label": "Shelf Set Name" - }, - { - "type": "list", - "key": "shelf_definition", - "label": "Shelves Definitions", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "text", - "key": "shelf_name", - "label": "Shelf Name" - }, - { - "type": "list", - "key": "tools_list", - "label": "Tools", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "label", - "label": "Name and Script Path are mandatory." - }, - { - "type": "text", - "key": "label", - "label": "Name" - }, - { - "type": "path", - "key": "script", - "label": "Script" - }, - { - "type": "path", - "key": "icon", - "label": "Icon" - }, - { - "type": "text", - "key": "help", - "label": "Help" + "key": "add_set_and_definitions", + "label": "Add Shelf Set Name and Shelves Definitions", + "children": [ + { + "type": "text", + "key": "shelf_set_name", + "label": "Shelf Set Name" + }, + { + "type": "list", + "key": "shelf_definition", + "label": "Shelves Definitions", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "shelf_name", + "label": "Shelf Name" + }, + { + "type": "list", + "key": "tools_list", + "label": "Tools", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "label", + "label": "Name and Script Path are mandatory." + }, + { + "type": "text", + "key": "label", + "label": "Name" + }, + { + "type": "path", + "key": "script", + "label": "Script" + }, + { + "type": "path", + "key": "icon", + "label": "Icon" + }, + { + "type": "text", + "key": "help", + "label": "Help" + } + ] } - ] - } + } + ] } - ] - } + } + ] } ] } diff --git a/server_addon/houdini/server/settings/shelves.py b/server_addon/houdini/server/settings/shelves.py index e02ddf1c34..651af27537 100644 --- a/server_addon/houdini/server/settings/shelves.py +++ b/server_addon/houdini/server/settings/shelves.py @@ -21,9 +21,20 @@ class ShelfDefinitionModel(BaseSettingsModel): title="Shelf Tools" ) +def shelves_enum_options(): + return [ + {"value": "add_shelf_file", "label": "Add a .shelf file"}, + {"value": "add_set_and_definitions", "label": "Add Shelf Set Name and Shelves Definitions"} + ] class ShelvesModel(BaseSettingsModel): _layout = "expanded" + options: str = Field( + title="Options", + description="Switch between shelves manager options", + enum_resolver=shelves_enum_options, + conditionalEnum=True + ) shelf_set_source_path: MultiplatformPathModel = Field( default_factory=MultiplatformPathModel, title="Shelf Set Path", From 0e331db93a0de805a55e88711d5d880f6281715e Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 11:24:48 +0200 Subject: [PATCH 27/45] Adjust Houdini Shelves Ayon settings --- .../houdini/server/settings/shelves.py | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/server_addon/houdini/server/settings/shelves.py b/server_addon/houdini/server/settings/shelves.py index 651af27537..a0acc90505 100644 --- a/server_addon/houdini/server/settings/shelves.py +++ b/server_addon/houdini/server/settings/shelves.py @@ -21,32 +21,47 @@ class ShelfDefinitionModel(BaseSettingsModel): title="Shelf Tools" ) + +class AddShelfFileModel(BaseSettingsModel): + shelf_set_source_path: MultiplatformPathModel = Field( + default_factory=MultiplatformPathModel, + title="Shelf Set Path" + ) + + +class AddSetAndDefinitionsModel(BaseSettingsModel): + shelf_set_name: str = Field(title="Shelf Set Name") + shelf_definition: list[ShelfDefinitionModel] = Field( + default_factory=list, + title="Shelves Definitions" + ) + + def shelves_enum_options(): return [ - {"value": "add_shelf_file", "label": "Add a .shelf file"}, - {"value": "add_set_and_definitions", "label": "Add Shelf Set Name and Shelves Definitions"} + { + "value": "add_shelf_file", + "label": "Add a .shelf file" + }, + { + "value": "add_set_and_definitions", + "label": "Add Shelf Set Name and Shelves Definitions" + } ] + class ShelvesModel(BaseSettingsModel): - _layout = "expanded" options: str = Field( title="Options", description="Switch between shelves manager options", enum_resolver=shelves_enum_options, conditionalEnum=True ) - shelf_set_source_path: MultiplatformPathModel = Field( - default_factory=MultiplatformPathModel, - title="Shelf Set Path", - section="Option 1: Add a .shelf file." + add_shelf_file: AddShelfFileModel = Field( + title="Add a .shelf file", + default_factory=AddShelfFileModel ) - shelf_set_name: str = Field( - "", - title="Shelf Set Name", - section=("OR Option 2: Add Shelf Set Name " - "and Shelves Definitions.") - ) - shelf_definition: list[ShelfDefinitionModel] = Field( - default_factory=list, - title="Shelves Definitions" + add_set_and_definitions: AddSetAndDefinitionsModel = Field( + title="Add Shelf Set Name and Shelves Definitions", + default_factory=AddSetAndDefinitionsModel ) From 381c00c3342193442e46b3e2475db64dda129beb Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 12:29:41 +0200 Subject: [PATCH 28/45] Align Openpype and Ayon settings, quick fix in shelves loader script --- openpype/hosts/houdini/api/shelves.py | 34 +++-- .../schemas/schema_houdini_scriptshelf.json | 128 ++++++++++-------- .../houdini/server/settings/shelves.py | 2 +- 3 files changed, 91 insertions(+), 73 deletions(-) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index 0afc737665..6fb3967be8 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -23,29 +23,33 @@ def generate_shelves(): # load configuration of houdini shelves project_name = get_current_project_name() project_settings = get_project_settings(project_name) - shelves_set_config = project_settings["houdini"]["shelves"] + shelves_configs = project_settings["houdini"]["shelves"] - if not shelves_set_config: + if not shelves_configs: log.debug("No custom shelves found in project settings.") return # Get Template data template_data = get_current_context_template_data() - for shelf_set_config in shelves_set_config: - shelf_set_filepath = shelf_set_config.get('shelf_set_source_path') - shelf_set_os_filepath = shelf_set_filepath[current_os] - if shelf_set_os_filepath: - shelf_set_os_filepath = get_path_using_template_data( - shelf_set_os_filepath, template_data - ) - if not os.path.isfile(shelf_set_os_filepath): - log.error("Shelf path doesn't exist - " - "{}".format(shelf_set_os_filepath)) - continue + for config in shelves_configs: + selected_option = config["options"] + shelf_set_config = config[selected_option] - hou.shelves.loadFile(shelf_set_os_filepath) - continue + shelf_set_filepath = shelf_set_config.get('shelf_set_source_path') + if shelf_set_filepath: + shelf_set_os_filepath = shelf_set_filepath[current_os] + if shelf_set_os_filepath: + shelf_set_os_filepath = get_path_using_template_data( + shelf_set_os_filepath, template_data + ) + if not os.path.isfile(shelf_set_os_filepath): + log.error("Shelf path doesn't exist - " + "{}".format(shelf_set_os_filepath)) + continue + + hou.shelves.loadFile(shelf_set_os_filepath) + continue shelf_set_name = shelf_set_config.get('shelf_set_name') if not shelf_set_name: diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json index 2dfce906b7..cee04b73e5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_scriptshelf.json @@ -15,11 +15,18 @@ "label": "Add a .shelf file", "children": [ { - "type": "path", - "key": "shelf_set_source_path", - "label": "Shelf Set Path", - "multipath": false, - "multiplatform": true + "type": "dict", + "key": "add_shelf_file", + "label": "Add a .shelf file", + "children": [ + { + "type": "path", + "key": "shelf_set_source_path", + "label": "Shelf Set Path", + "multipath": false, + "multiplatform": true + } + ] } ] }, @@ -28,60 +35,67 @@ "label": "Add Shelf Set Name and Shelves Definitions", "children": [ { - "type": "text", - "key": "shelf_set_name", - "label": "Shelf Set Name" - }, - { - "type": "list", - "key": "shelf_definition", - "label": "Shelves Definitions", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "text", - "key": "shelf_name", - "label": "Shelf Name" - }, - { - "type": "list", - "key": "tools_list", - "label": "Tools", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "type": "label", - "label": "Name and Script Path are mandatory." - }, - { - "type": "text", - "key": "label", - "label": "Name" - }, - { - "type": "path", - "key": "script", - "label": "Script" - }, - { - "type": "path", - "key": "icon", - "label": "Icon" - }, - { - "type": "text", - "key": "help", - "label": "Help" + "key": "add_set_and_definitions", + "label": "Add Shelf Set Name and Shelves Definitions", + "type": "dict", + "children": [ + { + "type": "text", + "key": "shelf_set_name", + "label": "Shelf Set Name" + }, + { + "type": "list", + "key": "shelf_definition", + "label": "Shelves Definitions", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "shelf_name", + "label": "Shelf Name" + }, + { + "type": "list", + "key": "tools_list", + "label": "Tools", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "label", + "label": "Name and Script Path are mandatory." + }, + { + "type": "text", + "key": "label", + "label": "Name" + }, + { + "type": "path", + "key": "script", + "label": "Script" + }, + { + "type": "path", + "key": "icon", + "label": "Icon" + }, + { + "type": "text", + "key": "help", + "label": "Help" + } + ] } - ] - } + } + ] } - ] - } + } + ] } ] } diff --git a/server_addon/houdini/server/settings/shelves.py b/server_addon/houdini/server/settings/shelves.py index a0acc90505..133c18f77c 100644 --- a/server_addon/houdini/server/settings/shelves.py +++ b/server_addon/houdini/server/settings/shelves.py @@ -30,7 +30,7 @@ class AddShelfFileModel(BaseSettingsModel): class AddSetAndDefinitionsModel(BaseSettingsModel): - shelf_set_name: str = Field(title="Shelf Set Name") + shelf_set_name: str = Field("", title="Shelf Set Name") shelf_definition: list[ShelfDefinitionModel] = Field( default_factory=list, title="Shelves Definitions" From c185d2cab8b9cb1a21d17c26acfeb8180eedcb15 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 12:35:41 +0200 Subject: [PATCH 29/45] resolve hound --- openpype/hosts/houdini/api/shelves.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index 8b27ecd67e..5093a90988 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -46,7 +46,7 @@ def generate_shelves(): ) if not os.path.isfile(shelf_set_os_filepath): log.error("Shelf path doesn't exist - " - "{}".format(shelf_set_os_filepath)) + "{}".format(shelf_set_os_filepath)) continue hou.shelves.loadFile(shelf_set_os_filepath) From 82f3b5e07f9d5b2bbe2a7679f11c52adf5ad51ff Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 1 Nov 2023 18:40:28 +0800 Subject: [PATCH 30/45] avoid using asset from context in collect render and also clean up unncessary code from the collector --- .../max/plugins/publish/collect_render.py | 19 +------------------ .../plugins/publish/collect_scene_version.py | 1 + 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 7765b3b924..38194a0735 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -4,11 +4,9 @@ import os import pyblish.api from pymxs import runtime as rt -from openpype.pipeline import get_current_asset_name from openpype.hosts.max.api import colorspace from openpype.hosts.max.api.lib import get_max_version, get_current_renderer from openpype.hosts.max.api.lib_renderproducts import RenderProducts -from openpype.client import get_last_version_by_subset_name class CollectRender(pyblish.api.InstancePlugin): @@ -27,7 +25,6 @@ class CollectRender(pyblish.api.InstancePlugin): filepath = current_file.replace("\\", "/") context.data['currentFile'] = current_file - asset = get_current_asset_name() files_by_aov = RenderProducts().get_beauty(instance.name) aovs = RenderProducts().get_aovs(instance.name) @@ -49,19 +46,6 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["files"].append(files_by_aov) img_format = RenderProducts().image_format() - project_name = context.data["projectName"] - asset_doc = context.data["assetEntity"] - asset_id = asset_doc["_id"] - version_doc = get_last_version_by_subset_name(project_name, - instance.name, - asset_id) - self.log.debug("version_doc: {0}".format(version_doc)) - version_int = 1 - if version_doc: - version_int += int(version_doc["name"]) - - self.log.debug(f"Setting {version_int} to context.") - context.data["version"] = version_int # OCIO config not support in # most of the 3dsmax renderers # so this is currently hard coded @@ -87,7 +71,7 @@ class CollectRender(pyblish.api.InstancePlugin): renderer = str(renderer_class).split(":")[0] # also need to get the render dir for conversion data = { - "asset": asset, + "asset": instance.data["asset"], "subset": str(instance.name), "publish": True, "maxversion": str(get_max_version()), @@ -99,7 +83,6 @@ class CollectRender(pyblish.api.InstancePlugin): "plugin": "3dsmax", "frameStart": instance.data["frameStartHandle"], "frameEnd": instance.data["frameEndHandle"], - "version": version_int, "farm": True } instance.data.update(data) diff --git a/openpype/plugins/publish/collect_scene_version.py b/openpype/plugins/publish/collect_scene_version.py index 7920c1e82b..f870ae9ad7 100644 --- a/openpype/plugins/publish/collect_scene_version.py +++ b/openpype/plugins/publish/collect_scene_version.py @@ -24,6 +24,7 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): "hiero", "houdini", "maya", + "max", "nuke", "photoshop", "resolve", From 424a0d6f2fdb9ab866aeb7bbf2c8d552e7a19a95 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 1 Nov 2023 11:14:38 +0000 Subject: [PATCH 31/45] Fix missing grease pencils in thumbnails and playblasts --- openpype/hosts/blender/plugins/publish/collect_review.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_review.py b/openpype/hosts/blender/plugins/publish/collect_review.py index 3bf2e39e24..2760ab9811 100644 --- a/openpype/hosts/blender/plugins/publish/collect_review.py +++ b/openpype/hosts/blender/plugins/publish/collect_review.py @@ -31,11 +31,12 @@ class CollectReview(pyblish.api.InstancePlugin): focal_length = cameras[0].data.lens - # get isolate objects list from meshes instance members . + # get isolate objects list from meshes instance members. + types = {"MESH", "GPENCIL"} isolate_objects = [ obj for obj in instance - if isinstance(obj, bpy.types.Object) and obj.type == "MESH" + if isinstance(obj, bpy.types.Object) and obj.type in types ] if not instance.data.get("remove"): From e4aa43e91bdc9e8f81055d7357bfb219cdd98a68 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 1 Nov 2023 11:53:49 +0000 Subject: [PATCH 32/45] Fix Blender Render Settings in Ayon --- server_addon/blender/server/settings/main.py | 2 +- server_addon/blender/server/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/blender/server/settings/main.py b/server_addon/blender/server/settings/main.py index 4476ea709b..374b2fafa2 100644 --- a/server_addon/blender/server/settings/main.py +++ b/server_addon/blender/server/settings/main.py @@ -41,7 +41,7 @@ class BlenderSettings(BaseSettingsModel): default_factory=BlenderImageIOModel, title="Color Management (ImageIO)" ) - render_settings: RenderSettingsModel = Field( + RenderSettings: RenderSettingsModel = Field( default_factory=RenderSettingsModel, title="Render Settings") workfile_builder: TemplateWorkfileBaseOptions = Field( default_factory=TemplateWorkfileBaseOptions, diff --git a/server_addon/blender/server/version.py b/server_addon/blender/server/version.py index ae7362549b..bbab0242f6 100644 --- a/server_addon/blender/server/version.py +++ b/server_addon/blender/server/version.py @@ -1 +1 @@ -__version__ = "0.1.3" +__version__ = "0.1.4" From c577d2bc84854382ecf157b22f293d4f23c38298 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 1 Nov 2023 13:09:57 +0000 Subject: [PATCH 33/45] Fix default settings --- server_addon/blender/server/settings/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/blender/server/settings/main.py b/server_addon/blender/server/settings/main.py index 374b2fafa2..5eff276ef5 100644 --- a/server_addon/blender/server/settings/main.py +++ b/server_addon/blender/server/settings/main.py @@ -61,7 +61,7 @@ DEFAULT_VALUES = { }, "set_frames_startup": True, "set_resolution_startup": True, - "render_settings": DEFAULT_RENDER_SETTINGS, + "RenderSettings": DEFAULT_RENDER_SETTINGS, "publish": DEFAULT_BLENDER_PUBLISH_SETTINGS, "workfile_builder": { "create_first_version": False, From 25c8a424ec65330777bc3968c41d9fe6f59d470c Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 15:40:29 +0200 Subject: [PATCH 34/45] skip check if node has no 'trange' parameter --- .../hosts/houdini/plugins/publish/validate_frame_range.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index 6a66f3de9f..2264372549 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -57,6 +57,14 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): return rop_node = hou.node(instance.data["instance_node"]) + + if rop_node.parm("trange") is None: + cls.log.debug( + "Skipping Check, Node has no 'trange' parameter: {}" + .format(rop_node.path()) + ) + return + if instance.data["frameStart"] > instance.data["frameEnd"]: cls.log.info( "The ROP node render range is set to " From 60438ab4a8bed39a8ee681f03e995e88a8b17943 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 17:00:41 +0200 Subject: [PATCH 35/45] BigRoy's comments - Better conditional and debug message --- .../houdini/plugins/publish/validate_frame_range.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index 2264372549..5d3866cfdb 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -57,15 +57,17 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): return rop_node = hou.node(instance.data["instance_node"]) + frame_start = instance.data.get("frameStart") + frame_end = instance.data.get("frameEnd") - if rop_node.parm("trange") is None: + if not (frame_start or frame_end): cls.log.debug( - "Skipping Check, Node has no 'trange' parameter: {}" - .format(rop_node.path()) + "Skipping frame range validation for " + "instance without frame data: {}".format(rop_node.path()) ) return - if instance.data["frameStart"] > instance.data["frameEnd"]: + if frame_start > frame_end: cls.log.info( "The ROP node render range is set to " "{0[frameStartHandle]} - {0[frameEndHandle]} " From f6b5f5b92779b047c204f46e19c05d09bd796ae3 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 1 Nov 2023 22:20:42 +0200 Subject: [PATCH 36/45] BigRoy's comment - Update COnditional --- openpype/hosts/houdini/plugins/publish/validate_frame_range.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py index 5d3866cfdb..90a079217b 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/houdini/plugins/publish/validate_frame_range.py @@ -60,7 +60,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): frame_start = instance.data.get("frameStart") frame_end = instance.data.get("frameEnd") - if not (frame_start or frame_end): + if frame_start is None or frame_end is None: cls.log.debug( "Skipping frame range validation for " "instance without frame data: {}".format(rop_node.path()) From 3da4e1c8e7eafec01b4a501234c6a78cf7a1c686 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 2 Nov 2023 08:33:08 +0000 Subject: [PATCH 37/45] Add Nuke 11.0 --- .../system_settings/applications.json | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 6a0ddb398e..a5283751e9 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -344,13 +344,30 @@ }, "environment": {} }, + "11-0": { + "use_python_2": true, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke11.0v4\\Nuke11.0.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": {} + }, "__dynamic_keys_labels__": { "13-2": "13.2", "13-0": "13.0", "12-2": "12.2", "12-0": "12.0", "11-3": "11.3", - "11-2": "11.2" + "11-2": "11.2", + "11-0": "11.0" } } }, From d55ac7aff7cd7d470ddac5aa9d8f9d2b43cf2cd2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Nov 2023 12:33:23 +0100 Subject: [PATCH 38/45] removed unused import --- openpype/hosts/tvpaint/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 58fbd09545..a84f196f09 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -7,7 +7,7 @@ import requests import pyblish.api -from openpype.client import get_project, get_asset_by_name +from openpype.client import get_asset_by_name from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost from openpype.hosts.tvpaint import TVPAINT_ROOT_DIR from openpype.settings import get_current_project_settings From 9d77421d9de2b2a7cee1d8c9e32290a632f6ac80 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Nov 2023 12:33:36 +0100 Subject: [PATCH 39/45] use AYON label when in AYON mode --- openpype/hosts/tvpaint/api/communication_server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/api/communication_server.py b/openpype/hosts/tvpaint/api/communication_server.py index d67ef8f798..34302eef6e 100644 --- a/openpype/hosts/tvpaint/api/communication_server.py +++ b/openpype/hosts/tvpaint/api/communication_server.py @@ -21,6 +21,7 @@ from aiohttp_json_rpc.protocol import ( ) from aiohttp_json_rpc.exceptions import RpcError +from openpype import AYON_SERVER_ENABLED from openpype.lib import emit_event from openpype.hosts.tvpaint.tvpaint_plugin import get_plugin_files_path @@ -834,8 +835,9 @@ class BaseCommunicator: class QtCommunicator(BaseCommunicator): + title = "AYON Tools" if AYON_SERVER_ENABLED else "OpenPype Tools" menu_definitions = { - "title": "OpenPype Tools", + "title": title, "menu_items": [ { "callback": "workfiles_tool", From 0cc90ceb081a5988192a993374b488ea0051618f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Nov 2023 12:33:46 +0100 Subject: [PATCH 40/45] removed unused 'previous_context' data --- openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py index 95a5cd77bd..56b51c812a 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -69,7 +69,6 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): "asset_name": context.data["asset"], "task_name": context.data["task"] } - context.data["previous_context"] = current_context self.log.debug("Current context is: {}".format(current_context)) # Collect context from workfile metadata From add4a1566d9dde015ac9b4422c0497f1c8710ab0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 2 Nov 2023 13:40:41 +0100 Subject: [PATCH 41/45] Use 'AVALON_LABEL' for label --- openpype/hosts/tvpaint/api/communication_server.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/api/communication_server.py b/openpype/hosts/tvpaint/api/communication_server.py index 34302eef6e..2c4d8160a6 100644 --- a/openpype/hosts/tvpaint/api/communication_server.py +++ b/openpype/hosts/tvpaint/api/communication_server.py @@ -835,7 +835,10 @@ class BaseCommunicator: class QtCommunicator(BaseCommunicator): - title = "AYON Tools" if AYON_SERVER_ENABLED else "OpenPype Tools" + label = os.getenv("AVALON_LABEL") + if not label: + label = "AYON" if AYON_SERVER_ENABLED else "OpenPype" + title = "{} Tools".format(label) menu_definitions = { "title": title, "menu_items": [ From 6e9c3b227815e7e8cbd158a8110549b2f095443e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Nov 2023 15:06:59 +0100 Subject: [PATCH 42/45] deadline: settings are not blocking extension input --- server_addon/deadline/server/settings/publish_plugins.py | 2 +- server_addon/deadline/server/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py index 8d48695a9c..54b7ff57c1 100644 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -267,7 +267,7 @@ class ProcessSubmittedJobOnFarmModel(BaseSettingsModel): title="Reviewable products filter", ) - @validator("aov_filter", "skip_integration_repre_list") + @validator("aov_filter") def validate_unique_names(cls, value): ensure_unique_names(value) return value diff --git a/server_addon/deadline/server/version.py b/server_addon/deadline/server/version.py index b3f4756216..ae7362549b 100644 --- a/server_addon/deadline/server/version.py +++ b/server_addon/deadline/server/version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" From 9154dbab05ec7f5b921874b5d523720df7c9409a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 3 Nov 2023 15:45:15 +0000 Subject: [PATCH 43/45] Fix loading of blend layout --- openpype/hosts/blender/plugins/load/load_blend.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index 25d6568889..3d6b634916 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -32,7 +32,7 @@ class BlendLoader(plugin.AssetLoader): empties = [obj for obj in objects if obj.type == 'EMPTY'] for empty in empties: - if empty.get(AVALON_PROPERTY): + if empty.get(AVALON_PROPERTY) and empty.parent is None: return empty return None @@ -90,6 +90,7 @@ class BlendLoader(plugin.AssetLoader): members.append(data) container = self._get_asset_container(data_to.objects) + print(container) assert container, "No asset group found" container.name = group_name @@ -100,8 +101,11 @@ class BlendLoader(plugin.AssetLoader): # Link all the container children to the collection for obj in container.children_recursive: + print(obj) bpy.context.scene.collection.objects.link(obj) + print("") + # Remove the library from the blend file library = bpy.data.libraries.get(bpy.path.basename(libpath)) bpy.data.libraries.remove(library) From bd94b8dcc972fe8f954b9b10de46350d1515864c Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 4 Nov 2023 03:25:15 +0000 Subject: [PATCH 44/45] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 4865fcfb31..c6ebd65e9c 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.5-nightly.2" +__version__ = "3.17.5-nightly.3" From 2ab07ec7ede740c13d98e8abeca9c02f73db4182 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 4 Nov 2023 03:25:51 +0000 Subject: [PATCH 45/45] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 249da3da0e..7a1fe9d83e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.17.5-nightly.3 - 3.17.5-nightly.2 - 3.17.5-nightly.1 - 3.17.4 @@ -134,7 +135,6 @@ body: - 3.15.1-nightly.5 - 3.15.1-nightly.4 - 3.15.1-nightly.3 - - 3.15.1-nightly.2 validations: required: true - type: dropdown