mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into chore/AY-4908_move-maya-code
This commit is contained in:
commit
7033998cf8
364 changed files with 2094 additions and 569 deletions
|
|
@ -51,9 +51,13 @@ IGNORED_MODULES_IN_AYON = set()
|
|||
# - this is used to log the missing addon
|
||||
MOVED_ADDON_MILESTONE_VERSIONS = {
|
||||
"applications": VersionInfo(0, 2, 0),
|
||||
"celaction": VersionInfo(0, 2, 0),
|
||||
"clockify": VersionInfo(0, 2, 0),
|
||||
"flame": VersionInfo(0, 2, 0),
|
||||
"traypublisher": VersionInfo(0, 2, 0),
|
||||
"tvpaint": VersionInfo(0, 2, 0),
|
||||
"maya": VersionInfo(0, 2, 0),
|
||||
"nuke": VersionInfo(0, 2, 0),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
from .addon import (
|
||||
HOST_DIR,
|
||||
FlameAddon,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"HOST_DIR",
|
||||
"FlameAddon",
|
||||
)
|
||||
|
|
@ -1,26 +1,23 @@
|
|||
Updated as of 26 May 2023
|
||||
Last Updated: 1 April 2024
|
||||
----------------------------
|
||||
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.
|
||||
|
||||
From v16.2.0 onwards, the nodeIndex parameters accepted by SetLUT() and SetCDL() are 1-based instead of 0-based, i.e. 1 <= nodeIndex <= total number of nodes.
|
||||
|
||||
|
||||
Overview
|
||||
--------
|
||||
As with Blackmagic Design Fusion scripts, user scripts written in Lua and Python programming languages are supported. By default, scripts can be invoked from the Console window in the Fusion page,
|
||||
As with Blackmagic Fusion scripts, user scripts written in Lua and Python programming languages are supported. By default, scripts can be invoked from the Console window in the Fusion page,
|
||||
or via command line. This permission can be changed in Resolve Preferences, to be only from Console, or to be invoked from the local network. Please be aware of the security implications when
|
||||
allowing scripting access from outside of the Resolve application.
|
||||
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
DaVinci Resolve scripting requires one of the following to be installed (for all users):
|
||||
|
||||
Lua 5.1
|
||||
Python 2.7 64-bit
|
||||
Python >= 3.6 64-bit
|
||||
|
||||
Python 2.7 64-bit
|
||||
|
||||
Using a script
|
||||
--------------
|
||||
|
|
@ -64,6 +61,7 @@ The interactive Console window allows for an easy way to execute simple scriptin
|
|||
and Lua and evaluates and executes them immediately. For more information on how to use the Console, please refer to the DaVinci Resolve User Manual.
|
||||
|
||||
This example Python script creates a simple project:
|
||||
|
||||
#!/usr/bin/env python
|
||||
import DaVinciResolveScript as dvr_script
|
||||
resolve = dvr_script.scriptapp("Resolve")
|
||||
|
|
@ -80,9 +78,8 @@ Running DaVinci Resolve in headless mode
|
|||
DaVinci Resolve can be launched in a headless mode without the user interface using the -nogui command line option. When DaVinci Resolve is launched using this option, the user interface is disabled.
|
||||
However, the various scripting APIs will continue to work as expected.
|
||||
|
||||
|
||||
Basic Resolve API
|
||||
-----------------
|
||||
DaVinci Resolve API
|
||||
-------------------
|
||||
Some commonly used API functions are described below (*). As with the resolve object, each object is inspectable for properties and functions.
|
||||
|
||||
Resolve
|
||||
|
|
@ -101,6 +98,12 @@ Resolve
|
|||
SaveLayoutPreset(presetName) --> Bool # Saves current UI layout as a preset named 'presetName'.
|
||||
ImportLayoutPreset(presetFilePath, presetName) --> Bool # Imports preset from path 'presetFilePath'. The optional argument 'presetName' specifies how the preset shall be named. If not specified, the preset is named based on the filename.
|
||||
Quit() --> None # Quits the Resolve App.
|
||||
ImportRenderPreset(presetPath) --> Bool # Import a preset from presetPath (string) and set it as current preset for rendering.
|
||||
ExportRenderPreset(presetName, exportPath) --> Bool # Export a preset to a given path (string) if presetName(string) exists.
|
||||
ImportBurnInPreset(presetPath) --> Bool # Import a data burn in preset from a given presetPath (string)
|
||||
ExportBurnInPreset(presetName, exportPath) --> Bool # Export a data burn in preset to a given path (string) if presetName (string) exists.
|
||||
GetKeyframeMode() --> keyframeMode # Returns the currently set keyframe mode (int). Refer to section 'Keyframe Mode information' below for details.
|
||||
SetKeyframeMode(keyframeMode) --> Bool # Returns True when 'keyframeMode'(enum) is successfully set. Refer to section 'Keyframe Mode information' below for details.
|
||||
|
||||
ProjectManager
|
||||
ArchiveProject(projectName,
|
||||
|
|
@ -131,6 +134,14 @@ ProjectManager
|
|||
# 'DbType': 'Disk' or 'PostgreSQL' (string)
|
||||
# 'DbName': database name (string)
|
||||
# 'IpAddress': IP address of the PostgreSQL server (string, optional key - defaults to '127.0.0.1')
|
||||
CreateCloudProject({cloudSettings}) --> Project # Creates and returns a cloud project.
|
||||
# '{cloudSettings}': Check 'Cloud Projects Settings' subsection below for more information.
|
||||
ImportCloudProject(filePath, {cloudSettings}) --> Bool # Returns True if import cloud project is successful; False otherwise
|
||||
# 'filePath': String; filePath of file to import
|
||||
# '{cloudSettings}': Check 'Cloud Projects Settings' subsection below for more information.
|
||||
RestoreCloudProject(folderPath, {cloudSettings}) --> Bool # Returns True if restore cloud project is successful; False otherwise
|
||||
# 'folderPath': String; path of folder to restore
|
||||
# '{cloudSettings}': Check 'Cloud Projects Settings' subsection below for more information.
|
||||
|
||||
Project
|
||||
GetMediaPool() --> MediaPool # Returns the Media Pool object.
|
||||
|
|
@ -175,6 +186,9 @@ Project
|
|||
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.
|
||||
GetColorGroupsList() --> [ColorGroups...] # Returns a list of all group objects in the timeline.
|
||||
AddColorGroup(groupName) --> ColorGroup # Creates a new ColorGroup. groupName must be a unique string.
|
||||
DeleteColorGroup(colorGroup) --> Bool # Deletes the given color group and sets clips to ungrouped.
|
||||
|
||||
MediaStorage
|
||||
GetMountedVolumeList() --> [paths...] # Returns list of folder paths corresponding to mounted volumes displayed in Resolve’s Media Storage.
|
||||
|
|
@ -198,7 +212,7 @@ MediaPool
|
|||
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), "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:
|
||||
ImportTimelineFromFile(filePath, {importOptions}) --> Timeline # Creates timeline based on parameters within given file (AAF/EDL/XML/FCPXML/DRT/ADL/OTIO) 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
|
||||
|
|
@ -225,6 +239,8 @@ MediaPool
|
|||
ExportMetadata(fileName, [clips]) --> Bool # Exports metadata of specified clips to 'fileName' in CSV format.
|
||||
# If no clips are specified, all clips from media pool will be used.
|
||||
GetUniqueId() --> string # Returns a unique ID for the media pool
|
||||
CreateStereoClip(LeftMediaPoolItem,
|
||||
RightMediaPoolItem) --> MediaPoolItem # Takes in two existing media pool items and creates a new 3D stereoscopic media pool entry replacing the input media in the media pool.
|
||||
|
||||
Folder
|
||||
GetClipList() --> [clips...] # Returns a list of clips (items) within the folder.
|
||||
|
|
@ -233,6 +249,8 @@ 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
|
||||
TranscribeAudio() --> Bool # Transcribes audio of the MediaPoolItems within the folder and nested folders. Returns True if successful; False otherwise
|
||||
ClearTranscription() --> Bool # Clears audio transcription of the MediaPoolItems within the folder and nested folders. Returns True if successful; False otherwise.
|
||||
|
||||
MediaPoolItem
|
||||
GetName() --> string # Returns the clip name.
|
||||
|
|
@ -340,8 +358,12 @@ 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.
|
||||
CreateSubtitlesFromAudio({autoCaptionSettings}) --> Bool # Creates subtitles from audio for the timeline.
|
||||
# Takes in optional dictionary {autoCaptionSettings}. Check 'Auto Caption Settings' subsection below for more information.
|
||||
# Returns True on success, False otherwise.
|
||||
DetectSceneCuts() --> Bool # Detects and makes scene cuts along the timeline. Returns True if successful, False otherwise.
|
||||
ConvertTimelineToStereo() --> Bool # Converts timeline to stereo. Returns True if successful; False otherwise.
|
||||
GetNodeGraph() --> Graph # Returns the timeline's node graph object.
|
||||
|
||||
TimelineItem
|
||||
GetName() --> string # Returns the item name.
|
||||
|
|
@ -390,12 +412,7 @@ TimelineItem
|
|||
GetStereoConvergenceValues() --> {keyframes...} # Returns a dict (offset -> value) of keyframe offsets and respective convergence values.
|
||||
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).
|
||||
GetLUT(nodeIndex) --> String # Gets relative LUT path based on the node index provided, 1 <= nodeIndex <= total number of nodes.
|
||||
SetCDL([CDL map]) --> Bool # Keys of map are: "NodeIndex", "Slope", "Offset", "Power", "Saturation", where 1 <= NodeIndex <= total number of nodes.
|
||||
# Example python code - SetCDL({"NodeIndex" : "1", "Slope" : "0.5 0.4 0.2", "Offset" : "0.4 0.3 0.2", "Power" : "0.6 0.7 0.8", "Saturation" : "0.65"})
|
||||
AddTake(mediaPoolItem, startFrame, endFrame) --> Bool # Adds mediaPoolItem as a new take. Initializes a take selector for the timeline item if needed. By default, the full clip extents is added. startFrame (int) and endFrame (int) are optional arguments used to specify the extents.
|
||||
|
|
@ -411,11 +428,17 @@ TimelineItem
|
|||
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.
|
||||
GetNodeGraph() --> Graph # Returns the clip's node graph object.
|
||||
GetColorGroup() --> ColorGroup # Returns the clip's color group if one exists.
|
||||
AssignToColorGroup(ColorGroup) --> Bool # Returns True if TiItem to successfully assigned to given ColorGroup. ColorGroup must be an existing group in the current project.
|
||||
RemoveFromColorGroup() --> Bool # Returns True if the TiItem is successfully removed from the ColorGroup it is in.
|
||||
ExportLUT(exportType, path) --> Bool # Exports LUTs from tiItem referring to value passed in 'exportType' (enum) for LUT size. Refer to. 'ExportLUT notes' section for possible values.
|
||||
# Saves generated LUT in the provided 'path' (string). 'path' should include the intended file name.
|
||||
# If an empty or incorrect extension is provided, the appropriate extension (.cube/.vlt) will be appended at the end of the path.
|
||||
|
||||
Gallery
|
||||
GetAlbumName(galleryStillAlbum) --> string # Returns the name of the GalleryStillAlbum object 'galleryStillAlbum'.
|
||||
|
|
@ -428,17 +451,63 @@ GalleryStillAlbum
|
|||
GetStills() --> [galleryStill] # Returns the list of GalleryStill objects in the album.
|
||||
GetLabel(galleryStill) --> string # Returns the label of the galleryStill.
|
||||
SetLabel(galleryStill, label) --> Bool # Sets the new 'label' to GalleryStill object 'galleryStill'.
|
||||
ExportStills([galleryStill], folderPath, filePrefix, format) --> Bool # Exports list of GalleryStill objects '[galleryStill]' to directory 'folderPath', with filename prefix 'filePrefix', using file format 'format' (supported formats: dpx, cin, tif, jpg, png, ppm, bmp, xpm).
|
||||
ImportStills([filePaths]) --> Bool # Imports GalleryStill from each filePath in [filePaths] list. True if at least one still is imported successfully. False otherwise.
|
||||
ExportStills([galleryStill], folderPath, filePrefix, format) --> Bool # Exports list of GalleryStill objects '[galleryStill]' to directory 'folderPath', with filename prefix 'filePrefix', using file format 'format' (supported formats: dpx, cin, tif, jpg, png, ppm, bmp, xpm, drx).
|
||||
DeleteStills([galleryStill]) --> Bool # Deletes specified list of GalleryStill objects '[galleryStill]'.
|
||||
|
||||
GalleryStill # This class does not provide any API functions but the object type is used by functions in other classes.
|
||||
|
||||
Graph
|
||||
GetNumNodes() --> int # Returns the number of nodes in the graph
|
||||
SetLUT(nodeIndex, lutPath) --> Bool # Sets LUT on the node mapping the node index provided, 1 <= nodeIndex <= self.GetNumNodes().
|
||||
# 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).
|
||||
GetLUT(nodeIndex) --> String # Gets relative LUT path based on the node index provided, 1 <= nodeIndex <= total number of nodes.
|
||||
GetNodeLabel(nodeIndex) --> string # Returns the label of the node at nodeIndex.
|
||||
GetToolsInNode(nodeIndex) --> [toolsList] # Returns toolsList (list of strings) of the tools used in the node indicated by given nodeIndex (int).
|
||||
|
||||
ColorGroup
|
||||
GetName() --> String # Returns the name (string) of the ColorGroup.
|
||||
SetName(groupName) --> Bool # Renames ColorGroup to groupName (string).
|
||||
GetClipsInTimeline(Timeline=CurrTimeline) --> [TimelineItem] # Returns a list of TimelineItem that are in colorGroup in the given Timeline. Timeline is Current Timeline by default.
|
||||
GetPreClipNodeGraph() --> Graph # Returns the ColorGroup Pre-clip graph.
|
||||
GetPostClipNodeGraph() --> Graph # Returns the ColorGroup Post-clip graph.
|
||||
|
||||
List and Dict Data Structures
|
||||
-----------------------------
|
||||
Beside primitive data types, Resolve's Python API mainly uses list and dict data structures. Lists are denoted by [ ... ] and dicts are denoted by { ... } above.
|
||||
As Lua does not support list and dict data structures, the Lua API implements "list" as a table with indices, e.g. { [1] = listValue1, [2] = listValue2, ... }.
|
||||
Similarly the Lua API implements "dict" as a table with the dictionary key as first element, e.g. { [dictKey1] = dictValue1, [dictKey2] = dictValue2, ... }.
|
||||
|
||||
Keyframe Mode information
|
||||
-------------------------
|
||||
This section covers additional notes for the functions Resolve.GetKeyframeMode() and Resolve.SetKeyframeMode(keyframeMode).
|
||||
|
||||
'keyframeMode' can be one of the following enums:
|
||||
- resolve.KEYFRAME_MODE_ALL == 0
|
||||
- resolve.KEYFRAME_MODE_COLOR == 1
|
||||
- resolve.KEYFRAME_MODE_SIZING == 2
|
||||
|
||||
Integer values returned by Resolve.GetKeyframeMode() will correspond to the enums above.
|
||||
|
||||
Cloud Projects Settings
|
||||
--------------------------------------
|
||||
This section covers additional notes for the functions "ProjectManager:CreateCloudProject," "ProjectManager:ImportCloudProject," and "ProjectManager:RestoreCloudProject"
|
||||
|
||||
All three functions take in a {cloudSettings} dict, that have the following keys:
|
||||
* resolve.CLOUD_SETTING_PROJECT_NAME: String, ["" by default]
|
||||
* resolve.CLOUD_SETTING_PROJECT_MEDIA_PATH: String, ["" by default]
|
||||
* resolve.CLOUD_SETTING_IS_COLLAB: Bool, [False by default]
|
||||
* resolve.CLOUD_SETTING_SYNC_MODE: syncMode (see below), [resolve.CLOUD_SYNC_PROXY_ONLY by default]
|
||||
* resolve.CLOUD_SETTING_IS_CAMERA_ACCESS: Bool [False by default]
|
||||
|
||||
Where syncMode is one of the following values:
|
||||
* resolve.CLOUD_SYNC_NONE,
|
||||
* resolve.CLOUD_SYNC_PROXY_ONLY,
|
||||
* resolve.CLOUD_SYNC_PROXY_AND_ORIG
|
||||
|
||||
All three "ProjectManager:CreateCloudProject," "ProjectManager:ImportCloudProject," and "ProjectManager:RestoreCloudProject" require resolve.PROJECT_MEDIA_PATH to be defined. "ProjectManager:CreateCloudProject" also requires resolve.PROJECT_NAME to be defined.
|
||||
|
||||
Looking up Project and Clip properties
|
||||
--------------------------------------
|
||||
This section covers additional notes for the functions "Project:GetSetting", "Project:SetSetting", "Timeline:GetSetting", "Timeline:SetSetting", "MediaPoolItem:GetClipProperty" and
|
||||
|
|
@ -478,6 +547,49 @@ 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]
|
||||
|
||||
Auto Caption Settings
|
||||
----------------------
|
||||
This section covers the supported settings for the method Timeline.CreateSubtitlesFromAudio({autoCaptionSettings})
|
||||
|
||||
The parameter setting is a dictionary containing the following keys:
|
||||
* resolve.SUBTITLE_LANGUAGE: languageID (see below), [resolve.AUTO_CAPTION_AUTO by default]
|
||||
* resolve.SUBTITLE_CAPTION_PRESET: presetType (see below), [resolve.AUTO_CAPTION_SUBTITLE_DEFAULT by default]
|
||||
* resolve.SUBTITLE_CHARS_PER_LINE: Number between 1 and 60 inclusive [42 by default]
|
||||
* resolve.SUBTITLE_LINE_BREAK: lineBreakType (see below), [resolve.AUTO_CAPTION_LINE_SINGLE by default]
|
||||
* resolve.SUBTITLE_GAP: Number between 0 and 10 inclusive [0 by default]
|
||||
|
||||
Note that the default values for some keys may change based on values defined for other keys, as per the UI.
|
||||
For example, if the following dictionary is supplied,
|
||||
CreateSubtitlesFromAudio( { resolve.SUBTITLE_LANGUAGE = resolve.AUTO_CAPTION_KOREAN,
|
||||
resolve.SUBTITLE_CAPTION_PRESET = resolve.AUTO_CAPTION_NETFLIX } )
|
||||
the default value for resolve.SUBTITLE_CHARS_PER_LINE will be 16 instead of 42
|
||||
|
||||
languageIDs:
|
||||
* resolve.AUTO_CAPTION_AUTO
|
||||
* resolve.AUTO_CAPTION_DANISH
|
||||
* resolve.AUTO_CAPTION_DUTCH
|
||||
* resolve.AUTO_CAPTION_ENGLISH
|
||||
* resolve.AUTO_CAPTION_FRENCH
|
||||
* resolve.AUTO_CAPTION_GERMAN
|
||||
* resolve.AUTO_CAPTION_ITALIAN
|
||||
* resolve.AUTO_CAPTION_JAPANESE
|
||||
* resolve.AUTO_CAPTION_KOREAN
|
||||
* resolve.AUTO_CAPTION_MANDARIN_SIMPLIFIED
|
||||
* resolve.AUTO_CAPTION_MANDARIN_TRADITIONAL
|
||||
* resolve.AUTO_CAPTION_NORWEGIAN
|
||||
* resolve.AUTO_CAPTION_PORTUGUESE
|
||||
* resolve.AUTO_CAPTION_RUSSIAN
|
||||
* resolve.AUTO_CAPTION_SPANISH
|
||||
* resolve.AUTO_CAPTION_SWEDISH
|
||||
|
||||
presetTypes:
|
||||
* resolve.AUTO_CAPTION_SUBTITLE_DEFAULT
|
||||
* resolve.AUTO_CAPTION_TELETEXT
|
||||
* resolve.AUTO_CAPTION_NETFLIX
|
||||
|
||||
lineBreakTypes:
|
||||
* resolve.AUTO_CAPTION_LINE_SINGLE
|
||||
* resolve.AUTO_CAPTION_LINE_DOUBLE
|
||||
|
||||
Looking up Render Settings
|
||||
--------------------------
|
||||
|
|
@ -531,6 +643,8 @@ exportType can be one of the following constants:
|
|||
- resolve.EXPORT_DOLBY_VISION_VER_4_0
|
||||
- resolve.EXPORT_DOLBY_VISION_VER_5_1
|
||||
- resolve.EXPORT_OTIO
|
||||
- resolve.EXPORT_ALE
|
||||
- resolve.EXPORT_ALE_CDL
|
||||
exportSubtype can be one of the following enums:
|
||||
- resolve.EXPORT_NONE
|
||||
- resolve.EXPORT_AAF_NEW
|
||||
|
|
@ -627,7 +741,8 @@ The supported keys with their accepted values are:
|
|||
- MOTION_EST_STANDARD_BETTER
|
||||
- MOTION_EST_ENHANCED_FASTER
|
||||
- MOTION_EST_ENHANCED_BETTER
|
||||
- MOTION_EST_SPEED_WRAP
|
||||
- MOTION_EST_SPEED_WARP_BETTER
|
||||
- MOTION_EST_SPEED_WARP_FASTER
|
||||
"Scaling" : A value from the following constants
|
||||
- SCALE_USE_PROJECT = 0
|
||||
- SCALE_CROP
|
||||
|
|
@ -659,6 +774,16 @@ as a single argument.
|
|||
|
||||
Getting the values for the keys that uses constants will return the number which is in the constant
|
||||
|
||||
ExportLUT notes
|
||||
---------------
|
||||
The following section covers additional notes for TimelineItem.ExportLUT(exportType, path).
|
||||
|
||||
Supported values for 'exportType' (enum) are:
|
||||
- resolve.EXPORT_LUT_17PTCUBE
|
||||
- resolve.EXPORT_LUT_33PTCUBE
|
||||
- resolve.EXPORT_LUT_65PTCUBE
|
||||
- resolve.EXPORT_LUT_PANASONICVLUT
|
||||
|
||||
Deprecated Resolve API Functions
|
||||
--------------------------------
|
||||
The following API functions are deprecated.
|
||||
|
|
@ -693,7 +818,12 @@ TimelineItem
|
|||
GetFusionCompNames() --> {names...} # Returns a dict of Fusion composition names associated with the timeline item.
|
||||
GetFlags() --> {colors...} # Returns a dict of flag colors assigned to the item.
|
||||
GetVersionNames(versionType) --> {names...} # Returns a dict of version names by provided versionType: 0 - local, 1 - remote.
|
||||
|
||||
GetNumNodes() --> int # Returns the number of nodes in the current graph for the timeline item
|
||||
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).
|
||||
GetLUT(nodeIndex) --> String # Gets relative LUT path based on the node index provided, 1 <= nodeIndex <= total number of nodes.
|
||||
GetNodeLabel(nodeIndex) --> string # Returns the label of the node at nodeIndex.
|
||||
|
||||
Unsupported Resolve API Functions
|
||||
---------------------------------
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
from pathlib import Path
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
load,
|
||||
get_representation_path,
|
||||
)
|
||||
|
||||
from ayon_core.hosts.resolve.api import lib
|
||||
|
||||
|
||||
class LoadEditorialPackage(load.LoaderPlugin):
|
||||
"""Load editorial package to timeline.
|
||||
|
||||
Loading timeline from OTIO file included media sources
|
||||
and timeline structure.
|
||||
"""
|
||||
|
||||
product_types = {"editorial_pkg"}
|
||||
|
||||
representations = {"*"}
|
||||
extensions = {"otio"}
|
||||
|
||||
label = "Load as Timeline"
|
||||
order = -10
|
||||
icon = "ei.align-left"
|
||||
color = "orange"
|
||||
|
||||
def load(self, context, name, namespace, data):
|
||||
files = get_representation_path(context["representation"])
|
||||
|
||||
search_folder_path = Path(files).parent / "resources"
|
||||
|
||||
project = lib.get_current_project()
|
||||
media_pool = project.GetMediaPool()
|
||||
|
||||
# create versioned bin for editorial package
|
||||
version_name = context["version"]["name"]
|
||||
bin_name = f"{name}_{version_name}"
|
||||
lib.create_bin(bin_name)
|
||||
|
||||
import_options = {
|
||||
"timelineName": "Editorial Package Timeline",
|
||||
"importSourceClips": True,
|
||||
"sourceClipsPath": search_folder_path.as_posix(),
|
||||
}
|
||||
|
||||
timeline = media_pool.ImportTimelineFromFile(files, import_options)
|
||||
print("Timeline imported: ", timeline)
|
||||
|
||||
def update(self, container, context):
|
||||
# TODO: implement update method in future
|
||||
pass
|
||||
|
|
@ -3,6 +3,8 @@ import re
|
|||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
import contextlib
|
||||
import substance_painter
|
||||
import substance_painter.project
|
||||
import substance_painter.resource
|
||||
import substance_painter.js
|
||||
|
|
@ -640,3 +642,88 @@ def prompt_new_file_with_mesh(mesh_filepath):
|
|||
return
|
||||
|
||||
return project_mesh
|
||||
|
||||
|
||||
def get_filtered_export_preset(export_preset_name, channel_type_names):
|
||||
"""Return export presets included with specific channels
|
||||
requested by users.
|
||||
|
||||
Args:
|
||||
export_preset_name (str): Name of export preset
|
||||
channel_type_list (list): A list of channel type requested by users
|
||||
|
||||
Returns:
|
||||
dict: export preset data
|
||||
"""
|
||||
|
||||
target_maps = []
|
||||
|
||||
export_presets = get_export_presets()
|
||||
export_preset_nice_name = export_presets[export_preset_name]
|
||||
resource_presets = substance_painter.export.list_resource_export_presets()
|
||||
preset = next(
|
||||
(
|
||||
preset for preset in resource_presets
|
||||
if preset.resource_id.name == export_preset_nice_name
|
||||
), None
|
||||
)
|
||||
if preset is None:
|
||||
return {}
|
||||
|
||||
maps = preset.list_output_maps()
|
||||
for channel_map in maps:
|
||||
for channel_name in channel_type_names:
|
||||
if not channel_map.get("fileName"):
|
||||
continue
|
||||
|
||||
if channel_name in channel_map["fileName"]:
|
||||
target_maps.append(channel_map)
|
||||
# Create a new preset
|
||||
return {
|
||||
"exportPresets": [
|
||||
{
|
||||
"name": export_preset_name,
|
||||
"maps": target_maps
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def set_layer_stack_opacity(node_ids, channel_types):
|
||||
"""Function to set the opacity of the layer stack during
|
||||
context
|
||||
Args:
|
||||
node_ids (list[int]): Substance painter root layer node ids
|
||||
channel_types (list[str]): Channel type names as defined as
|
||||
attributes in `substance_painter.textureset.ChannelType`
|
||||
"""
|
||||
# Do nothing
|
||||
if not node_ids or not channel_types:
|
||||
yield
|
||||
return
|
||||
|
||||
stack = substance_painter.textureset.get_active_stack()
|
||||
stack_root_layers = (
|
||||
substance_painter.layerstack.get_root_layer_nodes(stack)
|
||||
)
|
||||
node_ids = set(node_ids) # lookup
|
||||
excluded_nodes = [
|
||||
node for node in stack_root_layers
|
||||
if node.uid() not in node_ids
|
||||
]
|
||||
|
||||
original_opacity_values = []
|
||||
for node in excluded_nodes:
|
||||
for channel in channel_types:
|
||||
chan = getattr(substance_painter.textureset.ChannelType, channel)
|
||||
original_opacity_values.append((chan, node.get_opacity(chan)))
|
||||
try:
|
||||
for node in excluded_nodes:
|
||||
for channel, _ in original_opacity_values:
|
||||
node.set_opacity(0.0, channel)
|
||||
yield
|
||||
finally:
|
||||
for node in excluded_nodes:
|
||||
for channel, opacity in original_opacity_values:
|
||||
node.set_opacity(opacity, channel)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating textures."""
|
||||
|
||||
from ayon_core.pipeline import CreatedInstance, Creator, CreatorError
|
||||
from ayon_core.lib import (
|
||||
EnumDef,
|
||||
|
|
@ -17,6 +16,7 @@ from ayon_core.hosts.substancepainter.api.pipeline import (
|
|||
)
|
||||
from ayon_core.hosts.substancepainter.api.lib import get_export_presets
|
||||
|
||||
import substance_painter
|
||||
import substance_painter.project
|
||||
|
||||
|
||||
|
|
@ -28,9 +28,16 @@ class CreateTextures(Creator):
|
|||
icon = "picture-o"
|
||||
|
||||
default_variant = "Main"
|
||||
channel_mapping = []
|
||||
|
||||
def apply_settings(self, project_settings):
|
||||
settings = project_settings["substancepainter"].get("create", []) # noqa
|
||||
if settings:
|
||||
self.channel_mapping = settings["CreateTextures"].get(
|
||||
"channel_mapping", [])
|
||||
|
||||
|
||||
def create(self, product_name, instance_data, pre_create_data):
|
||||
|
||||
if not substance_painter.project.is_open():
|
||||
raise CreatorError("Can't create a Texture Set instance without "
|
||||
"an open project.")
|
||||
|
|
@ -42,11 +49,20 @@ class CreateTextures(Creator):
|
|||
"exportFileFormat",
|
||||
"exportSize",
|
||||
"exportPadding",
|
||||
"exportDilationDistance"
|
||||
"exportDilationDistance",
|
||||
"useCustomExportPreset",
|
||||
"exportChannel"
|
||||
]:
|
||||
if key in pre_create_data:
|
||||
creator_attributes[key] = pre_create_data[key]
|
||||
|
||||
if pre_create_data.get("use_selection"):
|
||||
stack = substance_painter.textureset.get_active_stack()
|
||||
|
||||
instance_data["selected_node_id"] = [
|
||||
node_number.uid() for node_number in
|
||||
substance_painter.layerstack.get_selected_nodes(stack)]
|
||||
|
||||
instance = self.create_instance_in_context(product_name,
|
||||
instance_data)
|
||||
set_instance(
|
||||
|
|
@ -88,8 +104,53 @@ class CreateTextures(Creator):
|
|||
return instance
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
if self.channel_mapping:
|
||||
export_channel_enum = {
|
||||
item["value"]: item["name"]
|
||||
for item in self.channel_mapping
|
||||
}
|
||||
else:
|
||||
export_channel_enum = {
|
||||
"BaseColor": "Base Color",
|
||||
"Metallic": "Metallic",
|
||||
"Roughness": "Roughness",
|
||||
"SpecularEdgeColor": "Specular Edge Color",
|
||||
"Emissive": "Emissive",
|
||||
"Opacity": "Opacity",
|
||||
"Displacement": "Displacement",
|
||||
"Glossiness": "Glossiness",
|
||||
"Anisotropylevel": "Anisotropy Level",
|
||||
"AO": "Ambient Occulsion",
|
||||
"Anisotropyangle": "Anisotropy Angle",
|
||||
"Transmissive": "Transmissive",
|
||||
"Reflection": "Reflection",
|
||||
"Diffuse": "Diffuse",
|
||||
"Ior": "Index of Refraction",
|
||||
"Specularlevel": "Specular Level",
|
||||
"BlendingMask": "Blending Mask",
|
||||
"Translucency": "Translucency",
|
||||
"Scattering": "Scattering",
|
||||
"ScatterColor": "Scatter Color",
|
||||
"SheenOpacity": "Sheen Opacity",
|
||||
"SheenRoughness": "Sheen Roughness",
|
||||
"SheenColor": "Sheen Color",
|
||||
"CoatOpacity": "Coat Opacity",
|
||||
"CoatColor": "Coat Color",
|
||||
"CoatRoughness": "Coat Roughness",
|
||||
"CoatSpecularLevel": "Coat Specular Level",
|
||||
"CoatNormal": "Coat Normal",
|
||||
}
|
||||
|
||||
return [
|
||||
EnumDef("exportChannel",
|
||||
items=export_channel_enum,
|
||||
multiselection=True,
|
||||
default=None,
|
||||
label="Export Channel(s)",
|
||||
tooltip="Choose the channel which you "
|
||||
"want to solely export. The value "
|
||||
"is 'None' by default which exports "
|
||||
"all channels"),
|
||||
EnumDef("exportPresetUrl",
|
||||
items=get_export_presets(),
|
||||
label="Output Template"),
|
||||
|
|
@ -149,7 +210,6 @@ class CreateTextures(Creator):
|
|||
},
|
||||
default=None,
|
||||
label="Size"),
|
||||
|
||||
EnumDef("exportPadding",
|
||||
items={
|
||||
"passthrough": "No padding (passthrough)",
|
||||
|
|
@ -172,4 +232,10 @@ class CreateTextures(Creator):
|
|||
|
||||
def get_pre_create_attr_defs(self):
|
||||
# Use same attributes as for instance attributes
|
||||
return self.get_instance_attr_defs()
|
||||
attr_defs = []
|
||||
if substance_painter.application.version_info()[0] >= 10:
|
||||
attr_defs.append(
|
||||
BoolDef("use_selection", label="Use selection",
|
||||
tooltip="Select Layer Stack(s) for exporting")
|
||||
)
|
||||
return attr_defs + self.get_instance_attr_defs()
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import substance_painter.textureset
|
|||
from ayon_core.pipeline import publish
|
||||
from ayon_core.hosts.substancepainter.api.lib import (
|
||||
get_parsed_export_maps,
|
||||
get_filtered_export_preset,
|
||||
strip_template
|
||||
)
|
||||
from ayon_core.pipeline.create import get_product_name
|
||||
|
|
@ -207,5 +208,8 @@ class CollectTextureSet(pyblish.api.InstancePlugin):
|
|||
for key, value in dict(parameters).items():
|
||||
if value is None:
|
||||
parameters.pop(key)
|
||||
|
||||
channel_layer = creator_attrs.get("exportChannel", [])
|
||||
if channel_layer:
|
||||
maps = get_filtered_export_preset(preset_url, channel_layer)
|
||||
config.update(maps)
|
||||
return config
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import substance_painter.export
|
||||
|
||||
from ayon_core.pipeline import KnownPublishError, publish
|
||||
from ayon_core.hosts.substancepainter.api.lib import set_layer_stack_opacity
|
||||
|
||||
|
||||
class ExtractTextures(publish.Extractor,
|
||||
|
|
@ -25,19 +25,24 @@ class ExtractTextures(publish.Extractor,
|
|||
def process(self, instance):
|
||||
|
||||
config = instance.data["exportConfig"]
|
||||
result = substance_painter.export.export_project_textures(config)
|
||||
creator_attrs = instance.data["creator_attributes"]
|
||||
export_channel = creator_attrs.get("exportChannel", [])
|
||||
node_ids = instance.data.get("selected_node_id", [])
|
||||
|
||||
if result.status != substance_painter.export.ExportStatus.Success:
|
||||
raise KnownPublishError(
|
||||
"Failed to export texture set: {}".format(result.message)
|
||||
)
|
||||
with set_layer_stack_opacity(node_ids, export_channel):
|
||||
result = substance_painter.export.export_project_textures(config)
|
||||
|
||||
# Log what files we generated
|
||||
for (texture_set_name, stack_name), maps in result.textures.items():
|
||||
# Log our texture outputs
|
||||
self.log.info(f"Exported stack: {texture_set_name} {stack_name}")
|
||||
for texture_map in maps:
|
||||
self.log.info(f"Exported texture: {texture_map}")
|
||||
if result.status != substance_painter.export.ExportStatus.Success:
|
||||
raise KnownPublishError(
|
||||
"Failed to export texture set: {}".format(result.message)
|
||||
)
|
||||
|
||||
# Log what files we generated
|
||||
for (texture_set_name, stack_name), maps in result.textures.items():
|
||||
# Log our texture outputs
|
||||
self.log.info(f"Exported stack: {texture_set_name} {stack_name}")
|
||||
for texture_map in maps:
|
||||
self.log.info(f"Exported texture: {texture_map}")
|
||||
|
||||
# We'll insert the color space data for each image instance that we
|
||||
# added into this texture set. The collector couldn't do so because
|
||||
|
|
|
|||
|
|
@ -30,11 +30,16 @@ class ValidateOutputMaps(pyblish.api.InstancePlugin):
|
|||
# it will generate without actually exporting the files. So we try to
|
||||
# generate the smallest size / fastest export as possible
|
||||
config = copy.deepcopy(config)
|
||||
invalid_channels = self.get_invalid_channels(instance, config)
|
||||
if invalid_channels:
|
||||
raise PublishValidationError(
|
||||
"Invalid Channel(s): {} found in texture set {}".format(
|
||||
invalid_channels, instance.name
|
||||
))
|
||||
parameters = config["exportParameters"][0]["parameters"]
|
||||
parameters["sizeLog2"] = [1, 1] # output 2x2 images (smallest)
|
||||
parameters["paddingAlgorithm"] = "passthrough" # no dilation (faster)
|
||||
parameters["dithering"] = False # no dithering (faster)
|
||||
|
||||
result = substance_painter.export.export_project_textures(config)
|
||||
if result.status != substance_painter.export.ExportStatus.Success:
|
||||
raise PublishValidationError(
|
||||
|
|
@ -108,3 +113,41 @@ class ValidateOutputMaps(pyblish.api.InstancePlugin):
|
|||
message=message,
|
||||
title="Missing output maps"
|
||||
)
|
||||
|
||||
def get_invalid_channels(self, instance, config):
|
||||
"""Function to get invalid channel(s) from export channel
|
||||
filtering
|
||||
|
||||
Args:
|
||||
instance (pyblish.api.Instance): Instance
|
||||
config (dict): export config
|
||||
|
||||
Raises:
|
||||
PublishValidationError: raise Publish Validation
|
||||
Error if any invalid channel(s) found
|
||||
|
||||
Returns:
|
||||
list: invalid channel(s)
|
||||
"""
|
||||
creator_attrs = instance.data["creator_attributes"]
|
||||
export_channel = creator_attrs.get("exportChannel", [])
|
||||
tmp_export_channel = copy.deepcopy(export_channel)
|
||||
invalid_channel = []
|
||||
if export_channel:
|
||||
for export_preset in config.get("exportPresets", {}):
|
||||
if not export_preset.get("maps", {}):
|
||||
raise PublishValidationError(
|
||||
"No Texture Map Exported with texture set: {}.".format(
|
||||
instance.name)
|
||||
)
|
||||
map_names = [channel_map["fileName"] for channel_map
|
||||
in export_preset["maps"]]
|
||||
for channel in tmp_export_channel:
|
||||
# Check if channel is found in at least one map
|
||||
for map_name in map_names:
|
||||
if channel in map_name:
|
||||
break
|
||||
else:
|
||||
invalid_channel.append(channel)
|
||||
|
||||
return invalid_channel
|
||||
|
|
|
|||
|
|
@ -26,27 +26,32 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin,
|
|||
|
||||
order = pyblish.api.CollectorOrder + 0.420
|
||||
label = "Collect Deadline Pools"
|
||||
hosts = ["aftereffects",
|
||||
"fusion",
|
||||
"harmony"
|
||||
"nuke",
|
||||
"maya",
|
||||
"max",
|
||||
"houdini"]
|
||||
hosts = [
|
||||
"aftereffects",
|
||||
"fusion",
|
||||
"harmony",
|
||||
"maya",
|
||||
"max",
|
||||
"houdini",
|
||||
"nuke",
|
||||
]
|
||||
|
||||
families = ["render",
|
||||
"rendering",
|
||||
"render.farm",
|
||||
"renderFarm",
|
||||
"renderlayer",
|
||||
"maxrender",
|
||||
"usdrender",
|
||||
"redshift_rop",
|
||||
"arnold_rop",
|
||||
"mantra_rop",
|
||||
"karma_rop",
|
||||
"vray_rop",
|
||||
"publish.hou"]
|
||||
families = [
|
||||
"render",
|
||||
"prerender",
|
||||
"rendering",
|
||||
"render.farm",
|
||||
"renderFarm",
|
||||
"renderlayer",
|
||||
"maxrender",
|
||||
"usdrender",
|
||||
"redshift_rop",
|
||||
"arnold_rop",
|
||||
"mantra_rop",
|
||||
"karma_rop",
|
||||
"vray_rop",
|
||||
"publish.hou",
|
||||
]
|
||||
|
||||
primary_pool = None
|
||||
secondary_pool = None
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ from ayon_core.pipeline.publish.lib import (
|
|||
replace_with_published_scene_path
|
||||
)
|
||||
from ayon_core.pipeline.publish import KnownPublishError
|
||||
from ayon_core.hosts.max.api.lib import (
|
||||
from ayon_max.api.lib import (
|
||||
get_current_renderer,
|
||||
get_multipass_setting
|
||||
)
|
||||
from ayon_core.hosts.max.api.lib_rendersettings import RenderSettings
|
||||
from ayon_max.api.lib_rendersettings import RenderSettings
|
||||
from openpype_modules.deadline import abstract_submit_deadline
|
||||
from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo
|
||||
|
||||
|
|
@ -205,11 +205,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline,
|
|||
|
||||
def _use_published_name(self, data, project_settings):
|
||||
# Not all hosts can import these modules.
|
||||
from ayon_core.hosts.max.api.lib import (
|
||||
from ayon_max.api.lib import (
|
||||
get_current_renderer,
|
||||
get_multipass_setting
|
||||
)
|
||||
from ayon_core.hosts.max.api.lib_rendersettings import RenderSettings
|
||||
from ayon_max.api.lib_rendersettings import RenderSettings
|
||||
|
||||
instance = self._instance
|
||||
job_info = copy.deepcopy(self.job_info)
|
||||
|
|
|
|||
|
|
@ -681,7 +681,7 @@ class PublishAttributeValues(AttributeValues):
|
|||
|
||||
@property
|
||||
def parent(self):
|
||||
self.publish_attributes.parent
|
||||
return self.publish_attributes.parent
|
||||
|
||||
|
||||
class PublishAttributes:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import pyblish.api
|
||||
from ayon_core.pipeline.publish import PublishValidationError
|
||||
|
||||
from ayon_core.lib import filter_profiles
|
||||
from ayon_core.pipeline.publish import (
|
||||
PublishValidationError,
|
||||
OptionalPyblishPluginMixin,
|
||||
get_current_host_name,
|
||||
)
|
||||
|
||||
|
||||
class ValidateVersion(pyblish.api.InstancePlugin):
|
||||
class ValidateVersion(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin):
|
||||
"""Validate instance version.
|
||||
|
||||
AYON does not allow overwriting previously published versions.
|
||||
|
|
@ -11,13 +17,39 @@ class ValidateVersion(pyblish.api.InstancePlugin):
|
|||
order = pyblish.api.ValidatorOrder
|
||||
|
||||
label = "Validate Version"
|
||||
hosts = ["nuke", "maya", "houdini", "blender",
|
||||
"photoshop", "aftereffects"]
|
||||
|
||||
optional = False
|
||||
active = True
|
||||
|
||||
@classmethod
|
||||
def apply_settings(cls, settings):
|
||||
# Disable if no profile is found for the current host
|
||||
profiles = (
|
||||
settings
|
||||
["core"]
|
||||
["publish"]
|
||||
["ValidateVersion"]
|
||||
["plugin_state_profiles"]
|
||||
)
|
||||
profile = filter_profiles(
|
||||
profiles, {"host_names": get_current_host_name()}
|
||||
)
|
||||
if not profile:
|
||||
cls.enabled = False
|
||||
return
|
||||
|
||||
# Apply settings from profile
|
||||
for attr_name in {
|
||||
"enabled",
|
||||
"optional",
|
||||
"active",
|
||||
}:
|
||||
setattr(cls, attr_name, profile[attr_name])
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
version = instance.data.get("version")
|
||||
latest_version = instance.data.get("latestVersion")
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from .hierarchy import (
|
|||
)
|
||||
from .thumbnails import ThumbnailsModel
|
||||
from .selection import HierarchyExpectedSelection
|
||||
from .users import UsersModel
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
|
@ -32,4 +33,6 @@ __all__ = (
|
|||
"ThumbnailsModel",
|
||||
|
||||
"HierarchyExpectedSelection",
|
||||
|
||||
"UsersModel",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import ayon_api
|
|||
import six
|
||||
|
||||
from ayon_core.style import get_default_entity_icon_color
|
||||
from ayon_core.lib import CacheItem
|
||||
from ayon_core.lib import CacheItem, NestedCacheItem
|
||||
|
||||
PROJECTS_MODEL_SENDER = "projects.model"
|
||||
|
||||
|
|
@ -17,6 +17,49 @@ class AbstractHierarchyController:
|
|||
pass
|
||||
|
||||
|
||||
class StatusItem:
|
||||
"""Item representing status of project.
|
||||
|
||||
Args:
|
||||
name (str): Status name ("Not ready").
|
||||
color (str): Status color in hex ("#434a56").
|
||||
short (str): Short status name ("NRD").
|
||||
icon (str): Icon name in MaterialIcons ("fiber_new").
|
||||
state (Literal["not_started", "in_progress", "done", "blocked"]):
|
||||
Status state.
|
||||
|
||||
"""
|
||||
def __init__(self, name, color, short, icon, state):
|
||||
self.name = name
|
||||
self.color = color
|
||||
self.short = short
|
||||
self.icon = icon
|
||||
self.state = state
|
||||
|
||||
def to_data(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"color": self.color,
|
||||
"short": self.short,
|
||||
"icon": self.icon,
|
||||
"state": self.state,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
return cls(**data)
|
||||
|
||||
@classmethod
|
||||
def from_project_item(cls, status_data):
|
||||
return cls(
|
||||
name=status_data["name"],
|
||||
color=status_data["color"],
|
||||
short=status_data["shortName"],
|
||||
icon=status_data["icon"],
|
||||
state=status_data["state"],
|
||||
)
|
||||
|
||||
|
||||
class ProjectItem:
|
||||
"""Item representing folder entity on a server.
|
||||
|
||||
|
|
@ -40,6 +83,23 @@ class ProjectItem:
|
|||
}
|
||||
self.icon = icon
|
||||
|
||||
@classmethod
|
||||
def from_entity(cls, project_entity):
|
||||
"""Creates folder item from entity.
|
||||
|
||||
Args:
|
||||
project_entity (dict[str, Any]): Project entity.
|
||||
|
||||
Returns:
|
||||
ProjectItem: Project item.
|
||||
|
||||
"""
|
||||
return cls(
|
||||
project_entity["name"],
|
||||
project_entity["active"],
|
||||
project_entity["library"],
|
||||
)
|
||||
|
||||
def to_data(self):
|
||||
"""Converts folder item to data.
|
||||
|
||||
|
|
@ -79,7 +139,7 @@ def _get_project_items_from_entitiy(projects):
|
|||
"""
|
||||
|
||||
return [
|
||||
ProjectItem(project["name"], project["active"], project["library"])
|
||||
ProjectItem.from_entity(project)
|
||||
for project in projects
|
||||
]
|
||||
|
||||
|
|
@ -87,18 +147,29 @@ def _get_project_items_from_entitiy(projects):
|
|||
class ProjectsModel(object):
|
||||
def __init__(self, controller):
|
||||
self._projects_cache = CacheItem(default_factory=list)
|
||||
self._project_items_by_name = {}
|
||||
self._projects_by_name = {}
|
||||
self._project_statuses_cache = NestedCacheItem(
|
||||
levels=1, default_factory=list
|
||||
)
|
||||
self._projects_by_name = NestedCacheItem(
|
||||
levels=1, default_factory=list
|
||||
)
|
||||
|
||||
self._is_refreshing = False
|
||||
self._controller = controller
|
||||
|
||||
def reset(self):
|
||||
self._projects_cache.reset()
|
||||
self._project_items_by_name = {}
|
||||
self._projects_by_name = {}
|
||||
self._project_statuses_cache.reset()
|
||||
self._projects_by_name.reset()
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh project items.
|
||||
|
||||
This method will requery list of ProjectItem returned by
|
||||
'get_project_items'.
|
||||
|
||||
To reset all cached items use 'reset' method.
|
||||
"""
|
||||
self._refresh_projects_cache()
|
||||
|
||||
def get_project_items(self, sender):
|
||||
|
|
@ -117,12 +188,51 @@ class ProjectsModel(object):
|
|||
return self._projects_cache.get_data()
|
||||
|
||||
def get_project_entity(self, project_name):
|
||||
if project_name not in self._projects_by_name:
|
||||
"""Get project entity.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
|
||||
Returns:
|
||||
Union[dict[str, Any], None]: Project entity or None if project
|
||||
was not found by name.
|
||||
|
||||
"""
|
||||
project_cache = self._projects_by_name[project_name]
|
||||
if not project_cache.is_valid:
|
||||
entity = None
|
||||
if project_name:
|
||||
entity = ayon_api.get_project(project_name)
|
||||
self._projects_by_name[project_name] = entity
|
||||
return self._projects_by_name[project_name]
|
||||
project_cache.update_data(entity)
|
||||
return project_cache.get_data()
|
||||
|
||||
def get_project_status_items(self, project_name, sender):
|
||||
"""Get project status items.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
sender (Union[str, None]): Name of sender who asked for items.
|
||||
|
||||
Returns:
|
||||
list[StatusItem]: Status items for project.
|
||||
|
||||
"""
|
||||
statuses_cache = self._project_statuses_cache[project_name]
|
||||
if not statuses_cache.is_valid:
|
||||
with self._project_statuses_refresh_event_manager(
|
||||
sender, project_name
|
||||
):
|
||||
project_entity = None
|
||||
if project_name:
|
||||
project_entity = self.get_project_entity(project_name)
|
||||
statuses = []
|
||||
if project_entity:
|
||||
statuses = [
|
||||
StatusItem.from_project_item(status)
|
||||
for status in project_entity["statuses"]
|
||||
]
|
||||
statuses_cache.update_data(statuses)
|
||||
return statuses_cache.get_data()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _project_refresh_event_manager(self, sender):
|
||||
|
|
@ -143,6 +253,23 @@ class ProjectsModel(object):
|
|||
)
|
||||
self._is_refreshing = False
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _project_statuses_refresh_event_manager(self, sender, project_name):
|
||||
self._controller.emit_event(
|
||||
"projects.statuses.refresh.started",
|
||||
{"sender": sender, "project_name": project_name},
|
||||
PROJECTS_MODEL_SENDER
|
||||
)
|
||||
try:
|
||||
yield
|
||||
|
||||
finally:
|
||||
self._controller.emit_event(
|
||||
"projects.statuses.refresh.finished",
|
||||
{"sender": sender, "project_name": project_name},
|
||||
PROJECTS_MODEL_SENDER
|
||||
)
|
||||
|
||||
def _refresh_projects_cache(self, sender=None):
|
||||
if self._is_refreshing:
|
||||
return None
|
||||
|
|
|
|||
84
client/ayon_core/tools/common_models/users.py
Normal file
84
client/ayon_core/tools/common_models/users.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import ayon_api
|
||||
|
||||
from ayon_core.lib import CacheItem
|
||||
|
||||
|
||||
class UserItem:
|
||||
def __init__(
|
||||
self,
|
||||
username,
|
||||
full_name,
|
||||
email,
|
||||
avatar_url,
|
||||
active,
|
||||
):
|
||||
self.username = username
|
||||
self.full_name = full_name
|
||||
self.email = email
|
||||
self.avatar_url = avatar_url
|
||||
self.active = active
|
||||
|
||||
@classmethod
|
||||
def from_entity_data(cls, user_data):
|
||||
return cls(
|
||||
user_data["name"],
|
||||
user_data["attrib"]["fullName"],
|
||||
user_data["attrib"]["email"],
|
||||
user_data["attrib"]["avatarUrl"],
|
||||
user_data["active"],
|
||||
)
|
||||
|
||||
|
||||
class UsersModel:
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
self._users_cache = CacheItem(default_factory=list)
|
||||
|
||||
def get_user_items(self):
|
||||
"""Get user items.
|
||||
|
||||
Returns:
|
||||
List[UserItem]: List of user items.
|
||||
|
||||
"""
|
||||
self._invalidate_cache()
|
||||
return self._users_cache.get_data()
|
||||
|
||||
def get_user_items_by_name(self):
|
||||
"""Get user items by name.
|
||||
|
||||
Implemented as most of cases using this model will need to find
|
||||
user information by username.
|
||||
|
||||
Returns:
|
||||
Dict[str, UserItem]: Dictionary of user items by name.
|
||||
|
||||
"""
|
||||
return {
|
||||
user_item.username: user_item
|
||||
for user_item in self.get_user_items()
|
||||
}
|
||||
|
||||
def get_user_item_by_username(self, username):
|
||||
"""Get user item by username.
|
||||
|
||||
Args:
|
||||
username (str): Username.
|
||||
|
||||
Returns:
|
||||
Union[UserItem, None]: User item or None if not found.
|
||||
|
||||
"""
|
||||
self._invalidate_cache()
|
||||
for user_item in self.get_user_items():
|
||||
if user_item.username == username:
|
||||
return user_item
|
||||
return None
|
||||
|
||||
def _invalidate_cache(self):
|
||||
if self._users_cache.is_valid:
|
||||
return
|
||||
self._users_cache.update_data([
|
||||
UserItem.from_entity_data(user)
|
||||
for user in ayon_api.get_users()
|
||||
])
|
||||
|
|
@ -114,6 +114,7 @@ class VersionItem:
|
|||
thumbnail_id (Union[str, None]): Thumbnail id.
|
||||
published_time (Union[str, None]): Published time in format
|
||||
'%Y%m%dT%H%M%SZ'.
|
||||
status (Union[str, None]): Status name.
|
||||
author (Union[str, None]): Author.
|
||||
frame_range (Union[str, None]): Frame range.
|
||||
duration (Union[int, None]): Duration.
|
||||
|
|
@ -132,6 +133,7 @@ class VersionItem:
|
|||
thumbnail_id,
|
||||
published_time,
|
||||
author,
|
||||
status,
|
||||
frame_range,
|
||||
duration,
|
||||
handles,
|
||||
|
|
@ -146,6 +148,7 @@ class VersionItem:
|
|||
self.is_hero = is_hero
|
||||
self.published_time = published_time
|
||||
self.author = author
|
||||
self.status = status
|
||||
self.frame_range = frame_range
|
||||
self.duration = duration
|
||||
self.handles = handles
|
||||
|
|
@ -185,6 +188,7 @@ class VersionItem:
|
|||
"is_hero": self.is_hero,
|
||||
"published_time": self.published_time,
|
||||
"author": self.author,
|
||||
"status": self.status,
|
||||
"frame_range": self.frame_range,
|
||||
"duration": self.duration,
|
||||
"handles": self.handles,
|
||||
|
|
@ -488,6 +492,27 @@ class FrontendLoaderController(_BaseLoaderController):
|
|||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_project_status_items(self, project_name, sender=None):
|
||||
"""Items for all projects available on server.
|
||||
|
||||
Triggers event topics "projects.statuses.refresh.started" and
|
||||
"projects.statuses.refresh.finished" with data:
|
||||
{
|
||||
"sender": sender,
|
||||
"project_name": project_name
|
||||
}
|
||||
|
||||
Args:
|
||||
project_name (Union[str, None]): Project name.
|
||||
sender (Optional[str]): Sender who requested the items.
|
||||
|
||||
Returns:
|
||||
list[StatusItem]: List of status items.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_product_items(self, project_name, folder_ids, sender=None):
|
||||
"""Product items for folder ids.
|
||||
|
|
|
|||
|
|
@ -180,6 +180,11 @@ class LoaderController(BackendLoaderController, FrontendLoaderController):
|
|||
def get_project_items(self, sender=None):
|
||||
return self._projects_model.get_project_items(sender)
|
||||
|
||||
def get_project_status_items(self, project_name, sender=None):
|
||||
return self._projects_model.get_project_status_items(
|
||||
project_name, sender
|
||||
)
|
||||
|
||||
def get_folder_items(self, project_name, sender=None):
|
||||
return self._hierarchy_model.get_folder_items(project_name, sender)
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ def version_item_from_entity(version):
|
|||
thumbnail_id=version["thumbnailId"],
|
||||
published_time=published_time,
|
||||
author=author,
|
||||
status=version["status"],
|
||||
frame_range=frame_range,
|
||||
duration=duration,
|
||||
handles=handles,
|
||||
|
|
@ -526,8 +527,11 @@ class ProductsModel:
|
|||
products = list(ayon_api.get_products(project_name, **kwargs))
|
||||
product_ids = {product["id"] for product in products}
|
||||
|
||||
# Add 'status' to fields -> fixed in ayon-python-api 1.0.4
|
||||
fields = ayon_api.get_default_fields_for_type("version")
|
||||
fields.add("status")
|
||||
versions = ayon_api.get_versions(
|
||||
project_name, product_ids=product_ids
|
||||
project_name, product_ids=product_ids, fields=fields
|
||||
)
|
||||
|
||||
return self._create_product_items(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ from ayon_core.tools.utils.lib import format_version
|
|||
from .products_model import (
|
||||
PRODUCT_ID_ROLE,
|
||||
VERSION_NAME_EDIT_ROLE,
|
||||
VERSION_STATUS_NAME_ROLE,
|
||||
VERSION_STATUS_SHORT_ROLE,
|
||||
VERSION_STATUS_COLOR_ROLE,
|
||||
VERSION_ID_ROLE,
|
||||
PRODUCT_IN_SCENE_ROLE,
|
||||
ACTIVE_SITE_ICON_ROLE,
|
||||
|
|
@ -104,7 +107,10 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
style = QtWidgets.QApplication.style()
|
||||
|
||||
style.drawControl(
|
||||
style.CE_ItemViewItem, option, painter, option.widget
|
||||
QtWidgets.QCommonStyle.CE_ItemViewItem,
|
||||
option,
|
||||
painter,
|
||||
option.widget
|
||||
)
|
||||
|
||||
painter.save()
|
||||
|
|
@ -116,9 +122,14 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate):
|
|||
pen.setColor(fg_color)
|
||||
painter.setPen(pen)
|
||||
|
||||
text_rect = style.subElementRect(style.SE_ItemViewItemText, option)
|
||||
text_rect = style.subElementRect(
|
||||
QtWidgets.QCommonStyle.SE_ItemViewItemText,
|
||||
option
|
||||
)
|
||||
text_margin = style.proxy().pixelMetric(
|
||||
style.PM_FocusFrameHMargin, option, option.widget
|
||||
QtWidgets.QCommonStyle.PM_FocusFrameHMargin,
|
||||
option,
|
||||
option.widget
|
||||
) + 1
|
||||
|
||||
painter.drawText(
|
||||
|
|
@ -194,6 +205,57 @@ class LoadedInSceneDelegate(QtWidgets.QStyledItemDelegate):
|
|||
option.palette.setBrush(QtGui.QPalette.Text, color)
|
||||
|
||||
|
||||
class StatusDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""Delegate showing status name and short name."""
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
if option.widget:
|
||||
style = option.widget.style()
|
||||
else:
|
||||
style = QtWidgets.QApplication.style()
|
||||
|
||||
style.drawControl(
|
||||
QtWidgets.QCommonStyle.CE_ItemViewItem,
|
||||
option,
|
||||
painter,
|
||||
option.widget
|
||||
)
|
||||
|
||||
painter.save()
|
||||
|
||||
text_rect = style.subElementRect(
|
||||
QtWidgets.QCommonStyle.SE_ItemViewItemText,
|
||||
option
|
||||
)
|
||||
text_margin = style.proxy().pixelMetric(
|
||||
QtWidgets.QCommonStyle.PM_FocusFrameHMargin,
|
||||
option,
|
||||
option.widget
|
||||
) + 1
|
||||
padded_text_rect = text_rect.adjusted(
|
||||
text_margin, 0, - text_margin, 0
|
||||
)
|
||||
|
||||
fm = QtGui.QFontMetrics(option.font)
|
||||
text = index.data(VERSION_STATUS_NAME_ROLE)
|
||||
if padded_text_rect.width() < fm.width(text):
|
||||
text = index.data(VERSION_STATUS_SHORT_ROLE)
|
||||
|
||||
status_color = index.data(VERSION_STATUS_COLOR_ROLE)
|
||||
fg_color = QtGui.QColor(status_color)
|
||||
pen = painter.pen()
|
||||
pen.setColor(fg_color)
|
||||
painter.setPen(pen)
|
||||
|
||||
painter.drawText(
|
||||
padded_text_rect,
|
||||
option.displayAlignment,
|
||||
text
|
||||
)
|
||||
|
||||
painter.restore()
|
||||
|
||||
|
||||
class SiteSyncDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""Paints icons and downloaded representation ration for both sites."""
|
||||
|
||||
|
|
|
|||
|
|
@ -22,18 +22,21 @@ VERSION_HERO_ROLE = QtCore.Qt.UserRole + 11
|
|||
VERSION_NAME_ROLE = QtCore.Qt.UserRole + 12
|
||||
VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 13
|
||||
VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 14
|
||||
VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 15
|
||||
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 16
|
||||
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 17
|
||||
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 18
|
||||
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 19
|
||||
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 20
|
||||
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 21
|
||||
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 22
|
||||
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 23
|
||||
REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 24
|
||||
SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 25
|
||||
SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 26
|
||||
VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 15
|
||||
VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 16
|
||||
VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 17
|
||||
VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 18
|
||||
VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 19
|
||||
VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 20
|
||||
VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 21
|
||||
VERSION_STEP_ROLE = QtCore.Qt.UserRole + 22
|
||||
VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 23
|
||||
VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 24
|
||||
ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 25
|
||||
REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 26
|
||||
REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 27
|
||||
SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 28
|
||||
SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 29
|
||||
|
||||
|
||||
class ProductsModel(QtGui.QStandardItemModel):
|
||||
|
|
@ -44,6 +47,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
"Product type",
|
||||
"Folder",
|
||||
"Version",
|
||||
"Status",
|
||||
"Time",
|
||||
"Author",
|
||||
"Frames",
|
||||
|
|
@ -69,11 +73,35 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
]
|
||||
]
|
||||
|
||||
version_col = column_labels.index("Version")
|
||||
published_time_col = column_labels.index("Time")
|
||||
product_name_col = column_labels.index("Product name")
|
||||
product_type_col = column_labels.index("Product type")
|
||||
folders_label_col = column_labels.index("Folder")
|
||||
version_col = column_labels.index("Version")
|
||||
status_col = column_labels.index("Status")
|
||||
published_time_col = column_labels.index("Time")
|
||||
author_col = column_labels.index("Author")
|
||||
frame_range_col = column_labels.index("Frames")
|
||||
duration_col = column_labels.index("Duration")
|
||||
handles_col = column_labels.index("Handles")
|
||||
step_col = column_labels.index("Step")
|
||||
in_scene_col = column_labels.index("In scene")
|
||||
sitesync_avail_col = column_labels.index("Availability")
|
||||
_display_role_mapping = {
|
||||
product_name_col: QtCore.Qt.DisplayRole,
|
||||
product_type_col: PRODUCT_TYPE_ROLE,
|
||||
folders_label_col: FOLDER_LABEL_ROLE,
|
||||
version_col: VERSION_NAME_ROLE,
|
||||
status_col: VERSION_STATUS_NAME_ROLE,
|
||||
published_time_col: VERSION_PUBLISH_TIME_ROLE,
|
||||
author_col: VERSION_AUTHOR_ROLE,
|
||||
frame_range_col: VERSION_FRAME_RANGE_ROLE,
|
||||
duration_col: VERSION_DURATION_ROLE,
|
||||
handles_col: VERSION_HANDLES_ROLE,
|
||||
step_col: VERSION_STEP_ROLE,
|
||||
in_scene_col: PRODUCT_IN_SCENE_ROLE,
|
||||
sitesync_avail_col: VERSION_AVAILABLE_ROLE,
|
||||
|
||||
}
|
||||
|
||||
def __init__(self, controller):
|
||||
super(ProductsModel, self).__init__()
|
||||
|
|
@ -96,6 +124,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
|
||||
self._last_project_name = None
|
||||
self._last_folder_ids = []
|
||||
self._last_project_statuses = {}
|
||||
|
||||
def get_product_item_indexes(self):
|
||||
return [
|
||||
|
|
@ -141,6 +170,15 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
if not index.isValid():
|
||||
return None
|
||||
|
||||
if role in (VERSION_STATUS_SHORT_ROLE, VERSION_STATUS_COLOR_ROLE):
|
||||
status_name = self.data(index, VERSION_STATUS_NAME_ROLE)
|
||||
status_item = self._last_project_statuses.get(status_name)
|
||||
if status_item is None:
|
||||
return ""
|
||||
if role == VERSION_STATUS_SHORT_ROLE:
|
||||
return status_item.short
|
||||
return status_item.color
|
||||
|
||||
col = index.column()
|
||||
if col == 0:
|
||||
return super(ProductsModel, self).data(index, role)
|
||||
|
|
@ -168,29 +206,8 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
if role == QtCore.Qt.DisplayRole:
|
||||
if not index.data(PRODUCT_ID_ROLE):
|
||||
return None
|
||||
if col == self.version_col:
|
||||
role = VERSION_NAME_ROLE
|
||||
elif col == 1:
|
||||
role = PRODUCT_TYPE_ROLE
|
||||
elif col == 2:
|
||||
role = FOLDER_LABEL_ROLE
|
||||
elif col == 4:
|
||||
role = VERSION_PUBLISH_TIME_ROLE
|
||||
elif col == 5:
|
||||
role = VERSION_AUTHOR_ROLE
|
||||
elif col == 6:
|
||||
role = VERSION_FRAME_RANGE_ROLE
|
||||
elif col == 7:
|
||||
role = VERSION_DURATION_ROLE
|
||||
elif col == 8:
|
||||
role = VERSION_HANDLES_ROLE
|
||||
elif col == 9:
|
||||
role = VERSION_STEP_ROLE
|
||||
elif col == 10:
|
||||
role = PRODUCT_IN_SCENE_ROLE
|
||||
elif col == 11:
|
||||
role = VERSION_AVAILABLE_ROLE
|
||||
else:
|
||||
role = self._display_role_mapping.get(col)
|
||||
if role is None:
|
||||
return None
|
||||
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
|
|
@ -312,6 +329,7 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
version_item.published_time, VERSION_PUBLISH_TIME_ROLE
|
||||
)
|
||||
model_item.setData(version_item.author, VERSION_AUTHOR_ROLE)
|
||||
model_item.setData(version_item.status, VERSION_STATUS_NAME_ROLE)
|
||||
model_item.setData(version_item.frame_range, VERSION_FRAME_RANGE_ROLE)
|
||||
model_item.setData(version_item.duration, VERSION_DURATION_ROLE)
|
||||
model_item.setData(version_item.handles, VERSION_HANDLES_ROLE)
|
||||
|
|
@ -393,6 +411,11 @@ class ProductsModel(QtGui.QStandardItemModel):
|
|||
|
||||
self._last_project_name = project_name
|
||||
self._last_folder_ids = folder_ids
|
||||
status_items = self._controller.get_project_status_items(project_name)
|
||||
self._last_project_statuses = {
|
||||
status_item.name: status_item
|
||||
for status_item in status_items
|
||||
}
|
||||
|
||||
active_site_icon_def = self._controller.get_active_site_icon_def(
|
||||
project_name
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ from .products_model import (
|
|||
from .products_delegates import (
|
||||
VersionDelegate,
|
||||
LoadedInSceneDelegate,
|
||||
SiteSyncDelegate
|
||||
StatusDelegate,
|
||||
SiteSyncDelegate,
|
||||
)
|
||||
from .actions_utils import show_actions_menu
|
||||
|
||||
|
|
@ -89,6 +90,7 @@ class ProductsWidget(QtWidgets.QWidget):
|
|||
90, # Product type
|
||||
130, # Folder label
|
||||
60, # Version
|
||||
100, # Status
|
||||
125, # Time
|
||||
75, # Author
|
||||
75, # Frames
|
||||
|
|
@ -128,20 +130,19 @@ class ProductsWidget(QtWidgets.QWidget):
|
|||
products_view.setColumnWidth(idx, width)
|
||||
|
||||
version_delegate = VersionDelegate()
|
||||
products_view.setItemDelegateForColumn(
|
||||
products_model.version_col, version_delegate)
|
||||
|
||||
time_delegate = PrettyTimeDelegate()
|
||||
products_view.setItemDelegateForColumn(
|
||||
products_model.published_time_col, time_delegate)
|
||||
|
||||
status_delegate = StatusDelegate()
|
||||
in_scene_delegate = LoadedInSceneDelegate()
|
||||
products_view.setItemDelegateForColumn(
|
||||
products_model.in_scene_col, in_scene_delegate)
|
||||
|
||||
sitesync_delegate = SiteSyncDelegate()
|
||||
products_view.setItemDelegateForColumn(
|
||||
products_model.sitesync_avail_col, sitesync_delegate)
|
||||
|
||||
for col, delegate in (
|
||||
(products_model.version_col, version_delegate),
|
||||
(products_model.published_time_col, time_delegate),
|
||||
(products_model.status_col, status_delegate),
|
||||
(products_model.in_scene_col, in_scene_delegate),
|
||||
(products_model.sitesync_avail_col, sitesync_delegate),
|
||||
):
|
||||
products_view.setItemDelegateForColumn(col, delegate)
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
@ -175,6 +176,7 @@ class ProductsWidget(QtWidgets.QWidget):
|
|||
|
||||
self._version_delegate = version_delegate
|
||||
self._time_delegate = time_delegate
|
||||
self._status_delegate = status_delegate
|
||||
self._in_scene_delegate = in_scene_delegate
|
||||
self._sitesync_delegate = sitesync_delegate
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ class WorkfileInfo:
|
|||
task_id (str): Task id.
|
||||
filepath (str): Filepath.
|
||||
filesize (int): File size.
|
||||
creation_time (int): Creation time (timestamp).
|
||||
modification_time (int): Modification time (timestamp).
|
||||
creation_time (float): Creation time (timestamp).
|
||||
modification_time (float): Modification time (timestamp).
|
||||
created_by (Union[str, none]): User who created the file.
|
||||
updated_by (Union[str, none]): User who last updated the file.
|
||||
note (str): Note.
|
||||
"""
|
||||
|
||||
|
|
@ -26,6 +28,8 @@ class WorkfileInfo:
|
|||
filesize,
|
||||
creation_time,
|
||||
modification_time,
|
||||
created_by,
|
||||
updated_by,
|
||||
note,
|
||||
):
|
||||
self.folder_id = folder_id
|
||||
|
|
@ -34,6 +38,8 @@ class WorkfileInfo:
|
|||
self.filesize = filesize
|
||||
self.creation_time = creation_time
|
||||
self.modification_time = modification_time
|
||||
self.created_by = created_by
|
||||
self.updated_by = updated_by
|
||||
self.note = note
|
||||
|
||||
def to_data(self):
|
||||
|
|
@ -50,6 +56,8 @@ class WorkfileInfo:
|
|||
"filesize": self.filesize,
|
||||
"creation_time": self.creation_time,
|
||||
"modification_time": self.modification_time,
|
||||
"created_by": self.created_by,
|
||||
"updated_by": self.updated_by,
|
||||
"note": self.note,
|
||||
}
|
||||
|
||||
|
|
@ -212,6 +220,7 @@ class FileItem:
|
|||
dirpath (str): Directory path of file.
|
||||
filename (str): Filename.
|
||||
modified (float): Modified timestamp.
|
||||
created_by (Optional[str]): Username.
|
||||
representation_id (Optional[str]): Representation id of published
|
||||
workfile.
|
||||
filepath (Optional[str]): Prepared filepath.
|
||||
|
|
@ -223,6 +232,8 @@ class FileItem:
|
|||
dirpath,
|
||||
filename,
|
||||
modified,
|
||||
created_by=None,
|
||||
updated_by=None,
|
||||
representation_id=None,
|
||||
filepath=None,
|
||||
exists=None
|
||||
|
|
@ -230,6 +241,8 @@ class FileItem:
|
|||
self.filename = filename
|
||||
self.dirpath = dirpath
|
||||
self.modified = modified
|
||||
self.created_by = created_by
|
||||
self.updated_by = updated_by
|
||||
self.representation_id = representation_id
|
||||
self._filepath = filepath
|
||||
self._exists = exists
|
||||
|
|
@ -269,6 +282,7 @@ class FileItem:
|
|||
"filename": self.filename,
|
||||
"dirpath": self.dirpath,
|
||||
"modified": self.modified,
|
||||
"created_by": self.created_by,
|
||||
"representation_id": self.representation_id,
|
||||
"filepath": self.filepath,
|
||||
"exists": self.exists,
|
||||
|
|
@ -522,6 +536,16 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon):
|
|||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_user_items_by_name(self):
|
||||
"""Get user items available on AYON server.
|
||||
|
||||
Returns:
|
||||
Dict[str, UserItem]: User items by username.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
# Host information
|
||||
@abstractmethod
|
||||
def get_workfile_extensions(self):
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from ayon_core.tools.common_models import (
|
|||
HierarchyModel,
|
||||
HierarchyExpectedSelection,
|
||||
ProjectsModel,
|
||||
UsersModel,
|
||||
)
|
||||
|
||||
from .abstract import (
|
||||
|
|
@ -161,6 +162,7 @@ class BaseWorkfileController(
|
|||
self._save_is_enabled = True
|
||||
|
||||
# Expected selected folder and task
|
||||
self._users_model = self._create_users_model()
|
||||
self._expected_selection = self._create_expected_selection_obj()
|
||||
self._selection_model = self._create_selection_model()
|
||||
self._projects_model = self._create_projects_model()
|
||||
|
|
@ -176,6 +178,12 @@ class BaseWorkfileController(
|
|||
def is_host_valid(self):
|
||||
return self._host_is_valid
|
||||
|
||||
def _create_users_model(self):
|
||||
return UsersModel(self)
|
||||
|
||||
def _create_workfiles_model(self):
|
||||
return WorkfilesModel(self)
|
||||
|
||||
def _create_expected_selection_obj(self):
|
||||
return WorkfilesToolExpectedSelection(self)
|
||||
|
||||
|
|
@ -188,9 +196,6 @@ class BaseWorkfileController(
|
|||
def _create_hierarchy_model(self):
|
||||
return HierarchyModel(self)
|
||||
|
||||
def _create_workfiles_model(self):
|
||||
return WorkfilesModel(self)
|
||||
|
||||
@property
|
||||
def event_system(self):
|
||||
"""Inner event system for workfiles tool controller.
|
||||
|
|
@ -272,6 +277,9 @@ class BaseWorkfileController(
|
|||
{"enabled": enabled}
|
||||
)
|
||||
|
||||
def get_user_items_by_name(self):
|
||||
return self._users_model.get_user_items_by_name()
|
||||
|
||||
# Host information
|
||||
def get_workfile_extensions(self):
|
||||
host = self._host
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import arrow
|
|||
import ayon_api
|
||||
from ayon_api.operations import OperationsSession
|
||||
|
||||
from ayon_core.lib import get_ayon_username
|
||||
from ayon_core.pipeline.template_data import (
|
||||
get_template_data,
|
||||
get_task_template_data,
|
||||
|
|
@ -23,6 +24,8 @@ from ayon_core.tools.workfiles.abstract import (
|
|||
WorkfileInfo,
|
||||
)
|
||||
|
||||
_NOT_SET = object()
|
||||
|
||||
|
||||
class CommentMatcher(object):
|
||||
"""Use anatomy and work file data to parse comments from filenames.
|
||||
|
|
@ -188,10 +191,17 @@ class WorkareaModel:
|
|||
if ext not in self._extensions:
|
||||
continue
|
||||
|
||||
modified = os.path.getmtime(filepath)
|
||||
items.append(
|
||||
FileItem(workdir, filename, modified)
|
||||
workfile_info = self._controller.get_workfile_info(
|
||||
folder_id, task_id, filepath
|
||||
)
|
||||
modified = os.path.getmtime(filepath)
|
||||
items.append(FileItem(
|
||||
workdir,
|
||||
filename,
|
||||
modified,
|
||||
workfile_info.created_by,
|
||||
workfile_info.updated_by,
|
||||
))
|
||||
return items
|
||||
|
||||
def _get_template_key(self, fill_data):
|
||||
|
|
@ -439,6 +449,7 @@ class WorkfileEntitiesModel:
|
|||
self._controller = controller
|
||||
self._cache = {}
|
||||
self._items = {}
|
||||
self._current_username = _NOT_SET
|
||||
|
||||
def _get_workfile_info_identifier(
|
||||
self, folder_id, task_id, rootless_path
|
||||
|
|
@ -459,8 +470,12 @@ class WorkfileEntitiesModel:
|
|||
self, folder_id, task_id, workfile_info, filepath
|
||||
):
|
||||
note = ""
|
||||
created_by = None
|
||||
updated_by = None
|
||||
if workfile_info:
|
||||
note = workfile_info["attrib"].get("description") or ""
|
||||
created_by = workfile_info.get("createdBy")
|
||||
updated_by = workfile_info.get("updatedBy")
|
||||
|
||||
filestat = os.stat(filepath)
|
||||
return WorkfileInfo(
|
||||
|
|
@ -470,6 +485,8 @@ class WorkfileEntitiesModel:
|
|||
filesize=filestat.st_size,
|
||||
creation_time=filestat.st_ctime,
|
||||
modification_time=filestat.st_mtime,
|
||||
created_by=created_by,
|
||||
updated_by=updated_by,
|
||||
note=note
|
||||
)
|
||||
|
||||
|
|
@ -481,7 +498,7 @@ class WorkfileEntitiesModel:
|
|||
for workfile_info in ayon_api.get_workfiles_info(
|
||||
self._controller.get_current_project_name(),
|
||||
task_ids=[task_id],
|
||||
fields=["id", "path", "attrib"],
|
||||
fields=["id", "path", "attrib", "createdBy", "updatedBy"],
|
||||
):
|
||||
workfile_identifier = self._get_workfile_info_identifier(
|
||||
folder_id, task_id, workfile_info["path"]
|
||||
|
|
@ -525,18 +542,32 @@ class WorkfileEntitiesModel:
|
|||
self._items.pop(identifier, None)
|
||||
return
|
||||
|
||||
if note is None:
|
||||
return
|
||||
|
||||
old_note = workfile_info.get("attrib", {}).get("note")
|
||||
|
||||
new_workfile_info = copy.deepcopy(workfile_info)
|
||||
attrib = new_workfile_info.setdefault("attrib", {})
|
||||
attrib["description"] = note
|
||||
update_data = {}
|
||||
if note is not None and old_note != note:
|
||||
update_data["attrib"] = {"description": note}
|
||||
attrib = new_workfile_info.setdefault("attrib", {})
|
||||
attrib["description"] = note
|
||||
|
||||
username = self._get_current_username()
|
||||
# Automatically fix 'createdBy' and 'updatedBy' fields
|
||||
# NOTE both fields were not automatically filled by server
|
||||
# until 1.1.3 release.
|
||||
if workfile_info.get("createdBy") is None:
|
||||
update_data["createdBy"] = username
|
||||
new_workfile_info["createdBy"] = username
|
||||
|
||||
if workfile_info.get("updatedBy") != username:
|
||||
update_data["updatedBy"] = username
|
||||
new_workfile_info["updatedBy"] = username
|
||||
|
||||
if not update_data:
|
||||
return
|
||||
|
||||
self._cache[identifier] = new_workfile_info
|
||||
self._items.pop(identifier, None)
|
||||
if old_note == note:
|
||||
return
|
||||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
|
||||
|
|
@ -545,7 +576,7 @@ class WorkfileEntitiesModel:
|
|||
project_name,
|
||||
"workfile",
|
||||
workfile_info["id"],
|
||||
{"attrib": {"description": note}},
|
||||
update_data,
|
||||
)
|
||||
session.commit()
|
||||
|
||||
|
|
@ -554,13 +585,18 @@ class WorkfileEntitiesModel:
|
|||
|
||||
project_name = self._controller.get_current_project_name()
|
||||
|
||||
username = self._get_current_username()
|
||||
workfile_info = {
|
||||
"path": rootless_path,
|
||||
"taskId": task_id,
|
||||
"attrib": {
|
||||
"extension": extension,
|
||||
"description": note
|
||||
}
|
||||
},
|
||||
# TODO remove 'createdBy' and 'updatedBy' fields when server is
|
||||
# or above 1.1.3 .
|
||||
"createdBy": username,
|
||||
"updatedBy": username,
|
||||
}
|
||||
|
||||
session = OperationsSession()
|
||||
|
|
@ -568,6 +604,11 @@ class WorkfileEntitiesModel:
|
|||
session.commit()
|
||||
return workfile_info
|
||||
|
||||
def _get_current_username(self):
|
||||
if self._current_username is _NOT_SET:
|
||||
self._current_username = get_ayon_username()
|
||||
return self._current_username
|
||||
|
||||
|
||||
class PublishWorkfilesModel:
|
||||
"""Model for handling of published workfiles.
|
||||
|
|
@ -599,7 +640,7 @@ class PublishWorkfilesModel:
|
|||
return self._cached_repre_extensions
|
||||
|
||||
def _file_item_from_representation(
|
||||
self, repre_entity, project_anatomy, task_name=None
|
||||
self, repre_entity, project_anatomy, author, task_name=None
|
||||
):
|
||||
if task_name is not None:
|
||||
task_info = repre_entity["context"].get("task")
|
||||
|
|
@ -634,6 +675,8 @@ class PublishWorkfilesModel:
|
|||
dirpath,
|
||||
filename,
|
||||
created_at.float_timestamp,
|
||||
author,
|
||||
None,
|
||||
repre_entity["id"]
|
||||
)
|
||||
|
||||
|
|
@ -643,9 +686,9 @@ class PublishWorkfilesModel:
|
|||
# Get subset docs of folder
|
||||
product_entities = ayon_api.get_products(
|
||||
project_name,
|
||||
folder_ids=[folder_id],
|
||||
product_types=["workfile"],
|
||||
fields=["id", "name"]
|
||||
folder_ids={folder_id},
|
||||
product_types={"workfile"},
|
||||
fields={"id", "name"}
|
||||
)
|
||||
|
||||
output = []
|
||||
|
|
@ -657,25 +700,33 @@ class PublishWorkfilesModel:
|
|||
version_entities = ayon_api.get_versions(
|
||||
project_name,
|
||||
product_ids=product_ids,
|
||||
fields=["id", "productId"]
|
||||
fields={"id", "author"}
|
||||
)
|
||||
version_ids = {version["id"] for version in version_entities}
|
||||
if not version_ids:
|
||||
versions_by_id = {
|
||||
version["id"]: version
|
||||
for version in version_entities
|
||||
}
|
||||
if not versions_by_id:
|
||||
return output
|
||||
|
||||
# Query representations of filtered versions and add filter for
|
||||
# extension
|
||||
repre_entities = ayon_api.get_representations(
|
||||
project_name,
|
||||
version_ids=version_ids
|
||||
version_ids=set(versions_by_id)
|
||||
)
|
||||
project_anatomy = self._controller.project_anatomy
|
||||
|
||||
# Filter queried representations by task name if task is set
|
||||
file_items = []
|
||||
for repre_entity in repre_entities:
|
||||
version_id = repre_entity["versionId"]
|
||||
version_entity = versions_by_id[version_id]
|
||||
file_item = self._file_item_from_representation(
|
||||
repre_entity, project_anatomy, task_name
|
||||
repre_entity,
|
||||
project_anatomy,
|
||||
version_entity["author"],
|
||||
task_name,
|
||||
)
|
||||
if file_item is not None:
|
||||
file_items.append(file_item)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ from .utils import BaseOverlayFrame
|
|||
|
||||
REPRE_ID_ROLE = QtCore.Qt.UserRole + 1
|
||||
FILEPATH_ROLE = QtCore.Qt.UserRole + 2
|
||||
DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3
|
||||
AUTHOR_ROLE = QtCore.Qt.UserRole + 3
|
||||
DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 4
|
||||
|
||||
|
||||
class PublishedFilesModel(QtGui.QStandardItemModel):
|
||||
|
|
@ -23,13 +24,19 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
|
|||
controller (AbstractWorkfilesFrontend): The control object.
|
||||
"""
|
||||
|
||||
columns = [
|
||||
"Name",
|
||||
"Author",
|
||||
"Date Modified",
|
||||
]
|
||||
date_modified_col = columns.index("Date Modified")
|
||||
|
||||
def __init__(self, controller):
|
||||
super(PublishedFilesModel, self).__init__()
|
||||
|
||||
self.setColumnCount(2)
|
||||
|
||||
self.setHeaderData(0, QtCore.Qt.Horizontal, "Name")
|
||||
self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified")
|
||||
self.setColumnCount(len(self.columns))
|
||||
for idx, label in enumerate(self.columns):
|
||||
self.setHeaderData(idx, QtCore.Qt.Horizontal, label)
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.task.changed",
|
||||
|
|
@ -185,6 +192,8 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
|
|||
self._remove_empty_item()
|
||||
self._remove_missing_context_item()
|
||||
|
||||
user_items_by_name = self._controller.get_user_items_by_name()
|
||||
|
||||
items_to_remove = set(self._items_by_id.keys())
|
||||
new_items = []
|
||||
for file_item in file_items:
|
||||
|
|
@ -205,8 +214,15 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
|
|||
else:
|
||||
flags = QtCore.Qt.NoItemFlags
|
||||
|
||||
author = file_item.created_by
|
||||
user_item = user_items_by_name.get(author)
|
||||
if user_item is not None and user_item.full_name:
|
||||
author = user_item.full_name
|
||||
|
||||
item.setFlags(flags)
|
||||
|
||||
item.setData(file_item.filepath, FILEPATH_ROLE)
|
||||
item.setData(author, AUTHOR_ROLE)
|
||||
item.setData(file_item.modified, DATE_MODIFIED_ROLE)
|
||||
|
||||
self._items_by_id[repre_id] = item
|
||||
|
|
@ -225,22 +241,30 @@ class PublishedFilesModel(QtGui.QStandardItemModel):
|
|||
# Use flags of first column for all columns
|
||||
if index.column() != 0:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
return super(PublishedFilesModel, self).flags(index)
|
||||
return super().flags(index)
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
|
||||
# Handle roles for first column
|
||||
if index.column() == 1:
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
return None
|
||||
col = index.column()
|
||||
if col != 1:
|
||||
return super().data(index, role)
|
||||
|
||||
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
return None
|
||||
|
||||
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
|
||||
if col == 1:
|
||||
role = AUTHOR_ROLE
|
||||
elif col == 2:
|
||||
role = DATE_MODIFIED_ROLE
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
else:
|
||||
return None
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
|
||||
return super(PublishedFilesModel, self).data(index, role)
|
||||
return super().data(index, role)
|
||||
|
||||
|
||||
class SelectContextOverlay(BaseOverlayFrame):
|
||||
|
|
@ -295,7 +319,7 @@ class PublishedFilesWidget(QtWidgets.QWidget):
|
|||
view.setModel(proxy_model)
|
||||
|
||||
time_delegate = PrettyTimeDelegate()
|
||||
view.setItemDelegateForColumn(1, time_delegate)
|
||||
view.setItemDelegateForColumn(model.date_modified_col, time_delegate)
|
||||
|
||||
# Default to a wider first filename column it is what we mostly care
|
||||
# about and the date modified is relatively small anyway.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ from ayon_core.tools.utils.delegates import PrettyTimeDelegate
|
|||
|
||||
FILENAME_ROLE = QtCore.Qt.UserRole + 1
|
||||
FILEPATH_ROLE = QtCore.Qt.UserRole + 2
|
||||
DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3
|
||||
AUTHOR_ROLE = QtCore.Qt.UserRole + 3
|
||||
DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 4
|
||||
|
||||
|
||||
class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
||||
|
|
@ -21,14 +22,20 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
|||
"""
|
||||
|
||||
refreshed = QtCore.Signal()
|
||||
columns = [
|
||||
"Name",
|
||||
"Author",
|
||||
"Date Modified",
|
||||
]
|
||||
date_modified_col = columns.index("Date Modified")
|
||||
|
||||
def __init__(self, controller):
|
||||
super(WorkAreaFilesModel, self).__init__()
|
||||
|
||||
self.setColumnCount(2)
|
||||
self.setColumnCount(len(self.columns))
|
||||
|
||||
self.setHeaderData(0, QtCore.Qt.Horizontal, "Name")
|
||||
self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified")
|
||||
for idx, label in enumerate(self.columns):
|
||||
self.setHeaderData(idx, QtCore.Qt.Horizontal, label)
|
||||
|
||||
controller.register_event_callback(
|
||||
"selection.folder.changed",
|
||||
|
|
@ -186,6 +193,7 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
|||
return
|
||||
self._remove_empty_item()
|
||||
self._remove_missing_context_item()
|
||||
user_items_by_name = self._controller.get_user_items_by_name()
|
||||
|
||||
items_to_remove = set(self._items_by_filename.keys())
|
||||
new_items = []
|
||||
|
|
@ -205,7 +213,13 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
|||
item.setData(file_item.filename, QtCore.Qt.DisplayRole)
|
||||
item.setData(file_item.filename, FILENAME_ROLE)
|
||||
|
||||
updated_by = file_item.updated_by
|
||||
user_item = user_items_by_name.get(updated_by)
|
||||
if user_item is not None and user_item.full_name:
|
||||
updated_by = user_item.full_name
|
||||
|
||||
item.setData(file_item.filepath, FILEPATH_ROLE)
|
||||
item.setData(updated_by, AUTHOR_ROLE)
|
||||
item.setData(file_item.modified, DATE_MODIFIED_ROLE)
|
||||
|
||||
self._items_by_filename[file_item.filename] = item
|
||||
|
|
@ -224,22 +238,30 @@ class WorkAreaFilesModel(QtGui.QStandardItemModel):
|
|||
# Use flags of first column for all columns
|
||||
if index.column() != 0:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
return super(WorkAreaFilesModel, self).flags(index)
|
||||
return super().flags(index)
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
|
||||
# Handle roles for first column
|
||||
if index.column() == 1:
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
return None
|
||||
col = index.column()
|
||||
if col == 0:
|
||||
return super().data(index, role)
|
||||
|
||||
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
return None
|
||||
|
||||
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
|
||||
if col == 1:
|
||||
role = AUTHOR_ROLE
|
||||
elif col == 2:
|
||||
role = DATE_MODIFIED_ROLE
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
else:
|
||||
return None
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
|
||||
return super(WorkAreaFilesModel, self).data(index, role)
|
||||
return super().data(index, role)
|
||||
|
||||
def set_published_mode(self, published_mode):
|
||||
if self._published_mode == published_mode:
|
||||
|
|
@ -279,7 +301,7 @@ class WorkAreaFilesWidget(QtWidgets.QWidget):
|
|||
view.setModel(proxy_model)
|
||||
|
||||
time_delegate = PrettyTimeDelegate()
|
||||
view.setItemDelegateForColumn(1, time_delegate)
|
||||
view.setItemDelegateForColumn(model.date_modified_col, time_delegate)
|
||||
|
||||
# Default to a wider first filename column it is what we mostly care
|
||||
# about and the date modified is relatively small anyway.
|
||||
|
|
|
|||
|
|
@ -147,13 +147,38 @@ class SidePanelWidget(QtWidgets.QWidget):
|
|||
workfile_info.creation_time)
|
||||
modification_time = datetime.datetime.fromtimestamp(
|
||||
workfile_info.modification_time)
|
||||
|
||||
user_items_by_name = self._controller.get_user_items_by_name()
|
||||
|
||||
def convert_username(username):
|
||||
user_item = user_items_by_name.get(username)
|
||||
if user_item is not None and user_item.full_name:
|
||||
return user_item.full_name
|
||||
return username
|
||||
|
||||
created_lines = [
|
||||
creation_time.strftime(datetime_format)
|
||||
]
|
||||
if workfile_info.created_by:
|
||||
created_lines.insert(
|
||||
0, convert_username(workfile_info.created_by)
|
||||
)
|
||||
|
||||
modified_lines = [
|
||||
modification_time.strftime(datetime_format)
|
||||
]
|
||||
if workfile_info.updated_by:
|
||||
modified_lines.insert(
|
||||
0, convert_username(workfile_info.updated_by)
|
||||
)
|
||||
|
||||
lines = (
|
||||
"<b>Size:</b>",
|
||||
size_value,
|
||||
"<b>Created:</b>",
|
||||
creation_time.strftime(datetime_format),
|
||||
"<br/>".join(created_lines),
|
||||
"<b>Modified:</b>",
|
||||
modification_time.strftime(datetime_format)
|
||||
"<br/>".join(modified_lines),
|
||||
)
|
||||
self._orig_note = note
|
||||
self._note_input.setPlainText(note)
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
split_widget.addWidget(tasks_widget)
|
||||
split_widget.addWidget(col_3_widget)
|
||||
split_widget.addWidget(side_panel)
|
||||
split_widget.setSizes([255, 160, 455, 175])
|
||||
split_widget.setSizes([255, 175, 550, 190])
|
||||
|
||||
body_layout.addWidget(split_widget)
|
||||
|
||||
|
|
@ -169,7 +169,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget):
|
|||
# Force focus on the open button by default, required for Houdini.
|
||||
self._files_widget.setFocus()
|
||||
|
||||
self.resize(1200, 600)
|
||||
self.resize(1260, 600)
|
||||
|
||||
def _create_col_1_widget(self, controller, parent):
|
||||
col_widget = QtWidgets.QWidget(parent)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ from typing import Any
|
|||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .settings import CoreSettings, DEFAULT_VALUES
|
||||
from .settings import (
|
||||
CoreSettings,
|
||||
DEFAULT_VALUES,
|
||||
convert_settings_overrides,
|
||||
)
|
||||
|
||||
|
||||
class CoreAddon(BaseServerAddon):
|
||||
|
|
@ -17,47 +21,8 @@ class CoreAddon(BaseServerAddon):
|
|||
source_version: str,
|
||||
overrides: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
self._convert_imagio_configs_0_3_1(overrides)
|
||||
convert_settings_overrides(source_version, overrides)
|
||||
# Use super conversion
|
||||
return await super().convert_settings_overrides(
|
||||
source_version, overrides
|
||||
)
|
||||
|
||||
def _convert_imagio_configs_0_3_1(self, overrides):
|
||||
"""Imageio config settings did change to profiles since 0.3.1. ."""
|
||||
imageio_overrides = overrides.get("imageio") or {}
|
||||
if (
|
||||
"ocio_config" not in imageio_overrides
|
||||
or "filepath" not in imageio_overrides["ocio_config"]
|
||||
):
|
||||
return
|
||||
|
||||
ocio_config = imageio_overrides.pop("ocio_config")
|
||||
|
||||
filepath = ocio_config["filepath"]
|
||||
if not filepath:
|
||||
return
|
||||
first_filepath = filepath[0]
|
||||
ocio_config_profiles = imageio_overrides.setdefault(
|
||||
"ocio_config_profiles", []
|
||||
)
|
||||
base_value = {
|
||||
"type": "builtin_path",
|
||||
"product_name": "",
|
||||
"host_names": [],
|
||||
"task_names": [],
|
||||
"task_types": [],
|
||||
"custom_path": "",
|
||||
"builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio"
|
||||
}
|
||||
if first_filepath in (
|
||||
"{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
|
||||
"{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio",
|
||||
):
|
||||
base_value["type"] = "builtin_path"
|
||||
base_value["builtin_path"] = first_filepath
|
||||
else:
|
||||
base_value["type"] = "custom_path"
|
||||
base_value["custom_path"] = first_filepath
|
||||
|
||||
ocio_config_profiles.append(base_value)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
from .main import CoreSettings, DEFAULT_VALUES
|
||||
from .conversion import convert_settings_overrides
|
||||
|
||||
|
||||
__all__ = (
|
||||
"CoreSettings",
|
||||
"DEFAULT_VALUES",
|
||||
|
||||
"convert_settings_overrides",
|
||||
)
|
||||
|
|
|
|||
86
server/settings/conversion.py
Normal file
86
server/settings/conversion.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import copy
|
||||
from typing import Any
|
||||
|
||||
from .publish_plugins import DEFAULT_PUBLISH_VALUES
|
||||
|
||||
|
||||
def _convert_imageio_configs_0_3_1(overrides):
|
||||
"""Imageio config settings did change to profiles since 0.3.1. ."""
|
||||
imageio_overrides = overrides.get("imageio") or {}
|
||||
if (
|
||||
"ocio_config" not in imageio_overrides
|
||||
or "filepath" not in imageio_overrides["ocio_config"]
|
||||
):
|
||||
return
|
||||
|
||||
ocio_config = imageio_overrides.pop("ocio_config")
|
||||
|
||||
filepath = ocio_config["filepath"]
|
||||
if not filepath:
|
||||
return
|
||||
first_filepath = filepath[0]
|
||||
ocio_config_profiles = imageio_overrides.setdefault(
|
||||
"ocio_config_profiles", []
|
||||
)
|
||||
base_value = {
|
||||
"type": "builtin_path",
|
||||
"product_name": "",
|
||||
"host_names": [],
|
||||
"task_names": [],
|
||||
"task_types": [],
|
||||
"custom_path": "",
|
||||
"builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio"
|
||||
}
|
||||
if first_filepath in (
|
||||
"{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
|
||||
"{BUILTIN_OCIO_ROOT}/nuke-default/config.ocio",
|
||||
):
|
||||
base_value["type"] = "builtin_path"
|
||||
base_value["builtin_path"] = first_filepath
|
||||
else:
|
||||
base_value["type"] = "custom_path"
|
||||
base_value["custom_path"] = first_filepath
|
||||
|
||||
ocio_config_profiles.append(base_value)
|
||||
|
||||
|
||||
def _convert_validate_version_0_3_3(publish_overrides):
|
||||
"""ValidateVersion plugin changed in 0.3.3."""
|
||||
if "ValidateVersion" not in publish_overrides:
|
||||
return
|
||||
|
||||
validate_version = publish_overrides["ValidateVersion"]
|
||||
# Already new settings
|
||||
if "plugin_state_profiles" in validate_version:
|
||||
return
|
||||
|
||||
# Use new default profile as base
|
||||
profile = copy.deepcopy(
|
||||
DEFAULT_PUBLISH_VALUES["ValidateVersion"]["plugin_state_profiles"][0]
|
||||
)
|
||||
# Copy values from old overrides to new overrides
|
||||
for key in {
|
||||
"enabled",
|
||||
"optional",
|
||||
"active",
|
||||
}:
|
||||
if key not in validate_version:
|
||||
continue
|
||||
profile[key] = validate_version.pop(key)
|
||||
|
||||
validate_version["plugin_state_profiles"] = [profile]
|
||||
|
||||
|
||||
def _conver_publish_plugins(overrides):
|
||||
if "publish" not in overrides:
|
||||
return
|
||||
_convert_validate_version_0_3_3(overrides["publish"])
|
||||
|
||||
|
||||
def convert_settings_overrides(
|
||||
source_version: str,
|
||||
overrides: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
_convert_imageio_configs_0_3_1(overrides)
|
||||
_conver_publish_plugins(overrides)
|
||||
return overrides
|
||||
|
|
@ -59,7 +59,7 @@ class CollectFramesFixDefModel(BaseSettingsModel):
|
|||
)
|
||||
|
||||
|
||||
class ValidateOutdatedContainersProfile(BaseSettingsModel):
|
||||
class PluginStateByHostModelProfile(BaseSettingsModel):
|
||||
_layout = "expanded"
|
||||
# Filtering
|
||||
host_names: list[str] = SettingsField(
|
||||
|
|
@ -72,17 +72,12 @@ class ValidateOutdatedContainersProfile(BaseSettingsModel):
|
|||
active: bool = SettingsField(True, title="Active")
|
||||
|
||||
|
||||
class ValidateOutdatedContainersModel(BaseSettingsModel):
|
||||
"""Validate if Publishing intent was selected.
|
||||
|
||||
It is possible to disable validation for specific publishing context
|
||||
with profiles.
|
||||
"""
|
||||
|
||||
class PluginStateByHostModel(BaseSettingsModel):
|
||||
_isGroup = True
|
||||
plugin_state_profiles: list[ValidateOutdatedContainersProfile] = SettingsField(
|
||||
plugin_state_profiles: list[PluginStateByHostModelProfile] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Plugin enable state profiles",
|
||||
description="Change plugin state based on host name."
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -563,7 +558,7 @@ class ExtractBurninProfile(BaseSettingsModel):
|
|||
_layout = "expanded"
|
||||
product_types: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Produt types"
|
||||
title="Product types"
|
||||
)
|
||||
hosts: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
|
|
@ -793,12 +788,16 @@ class PublishPuginsModel(BaseSettingsModel):
|
|||
default_factory=ValidateBaseModel,
|
||||
title="Validate Editorial Asset Name"
|
||||
)
|
||||
ValidateVersion: ValidateBaseModel = SettingsField(
|
||||
default_factory=ValidateBaseModel,
|
||||
title="Validate Version"
|
||||
ValidateVersion: PluginStateByHostModel = SettingsField(
|
||||
default_factory=PluginStateByHostModel,
|
||||
title="Validate Version",
|
||||
description=(
|
||||
"Validate that product version to integrate"
|
||||
" is newer than latest version in AYON."
|
||||
)
|
||||
)
|
||||
ValidateOutdatedContainers: ValidateOutdatedContainersModel = SettingsField(
|
||||
default_factory=ValidateOutdatedContainersModel,
|
||||
ValidateOutdatedContainers: PluginStateByHostModel = SettingsField(
|
||||
default_factory=PluginStateByHostModel,
|
||||
title="Validate Containers"
|
||||
)
|
||||
ValidateIntent: ValidateIntentModel = SettingsField(
|
||||
|
|
@ -882,9 +881,21 @@ DEFAULT_PUBLISH_VALUES = {
|
|||
"active": True
|
||||
},
|
||||
"ValidateVersion": {
|
||||
"enabled": True,
|
||||
"optional": False,
|
||||
"active": True
|
||||
"plugin_state_profiles": [
|
||||
{
|
||||
"host_names": [
|
||||
"aftereffects",
|
||||
"blender",
|
||||
"houdini",
|
||||
"maya",
|
||||
"nuke",
|
||||
"photoshop",
|
||||
],
|
||||
"enabled": True,
|
||||
"optional": False,
|
||||
"active": True
|
||||
}
|
||||
]
|
||||
},
|
||||
"ValidateOutdatedContainers": {
|
||||
"plugin_state_profiles": [
|
||||
|
|
|
|||
|
|
@ -212,7 +212,13 @@ class ApplicationsAddonSettings(BaseSettingsModel):
|
|||
scope=["studio"]
|
||||
)
|
||||
only_available: bool = SettingsField(
|
||||
True, title="Show only available applications")
|
||||
True,
|
||||
title="Show only available applications",
|
||||
description="Enable to show only applications in AYON Launcher"
|
||||
" for which the executable paths are found on the running machine."
|
||||
" This applies as an additional filter to the applications defined in a "
|
||||
" project's anatomy settings to ignore unavailable applications."
|
||||
)
|
||||
|
||||
@validator("tool_groups")
|
||||
def validate_unique_name(cls, value):
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from .version import __version__
|
||||
from .addon import (
|
||||
CELACTION_ROOT_DIR,
|
||||
CelactionAddon,
|
||||
|
|
@ -5,6 +6,8 @@ from .addon import (
|
|||
|
||||
|
||||
__all__ = (
|
||||
"__version__",
|
||||
|
||||
"CELACTION_ROOT_DIR",
|
||||
"CelactionAddon",
|
||||
)
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
import os
|
||||
from ayon_core.addon import AYONAddon, IHostAddon
|
||||
|
||||
from .version import __version__
|
||||
|
||||
CELACTION_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
class CelactionAddon(AYONAddon, IHostAddon):
|
||||
name = "celaction"
|
||||
version = __version__
|
||||
host_name = "celaction"
|
||||
|
||||
def get_launch_hook_paths(self, app):
|
||||
|
|
@ -4,13 +4,11 @@ import winreg
|
|||
import subprocess
|
||||
from ayon_core.lib import get_ayon_launcher_args
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_core.hosts.celaction import CELACTION_ROOT_DIR
|
||||
from ayon_celaction import CELACTION_ROOT_DIR
|
||||
|
||||
|
||||
class CelactionPrelaunchHook(PreLaunchHook):
|
||||
"""
|
||||
Bootstrap celacion with pype
|
||||
"""
|
||||
"""Bootstrap celacion with AYON"""
|
||||
app_groups = {"celaction"}
|
||||
platforms = {"windows"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
|
@ -39,7 +37,7 @@ class CelactionPrelaunchHook(PreLaunchHook):
|
|||
CELACTION_ROOT_DIR, "scripts", "publish_cli.py"
|
||||
)
|
||||
subprocess_args = get_ayon_launcher_args("run", path_to_cli)
|
||||
openpype_executable = subprocess_args.pop(0)
|
||||
executable = subprocess_args.pop(0)
|
||||
workfile_settings = self.get_workfile_settings()
|
||||
|
||||
winreg.SetValueEx(
|
||||
|
|
@ -47,7 +45,7 @@ class CelactionPrelaunchHook(PreLaunchHook):
|
|||
"SubmitAppTitle",
|
||||
0,
|
||||
winreg.REG_SZ,
|
||||
openpype_executable
|
||||
executable
|
||||
)
|
||||
|
||||
# add required arguments for workfile path
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
import copy
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectRenderPath(pyblish.api.InstancePlugin):
|
||||
|
|
@ -10,6 +10,8 @@ class CollectRenderPath(pyblish.api.InstancePlugin):
|
|||
order = pyblish.api.CollectorOrder + 0.495
|
||||
families = ["render.farm"]
|
||||
|
||||
settings_category = "celaction"
|
||||
|
||||
# Presets
|
||||
output_extension = "png"
|
||||
anatomy_template_key_render_files = None
|
||||
|
|
@ -4,7 +4,7 @@ import sys
|
|||
import pyblish.api
|
||||
import pyblish.util
|
||||
|
||||
import ayon_core.hosts.celaction
|
||||
from ayon_celaction import CELACTION_ROOT_DIR
|
||||
from ayon_core.lib import Logger
|
||||
from ayon_core.tools.utils import host_tools
|
||||
from ayon_core.pipeline import install_ayon_plugins
|
||||
|
|
@ -13,13 +13,12 @@ from ayon_core.pipeline import install_ayon_plugins
|
|||
log = Logger.get_logger("celaction")
|
||||
|
||||
PUBLISH_HOST = "celaction"
|
||||
HOST_DIR = os.path.dirname(os.path.abspath(ayon_core.hosts.celaction.__file__))
|
||||
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
|
||||
PLUGINS_DIR = os.path.join(CELACTION_ROOT_DIR, "plugins")
|
||||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
||||
|
||||
|
||||
def main():
|
||||
# Registers pype's Global pyblish plugins
|
||||
# Registers global pyblish plugins
|
||||
install_ayon_plugins()
|
||||
|
||||
if os.path.exists(PUBLISH_PATH):
|
||||
3
server_addon/celaction/client/ayon_celaction/version.py
Normal file
3
server_addon/celaction/client/ayon_celaction/version.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'celaction' version."""
|
||||
__version__ = "0.2.0"
|
||||
|
|
@ -1,3 +1,12 @@
|
|||
name = "celaction"
|
||||
title = "CelAction"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
client_dir = "ayon_celaction"
|
||||
|
||||
ayon_required_addons = {
|
||||
"core": ">0.3.2",
|
||||
}
|
||||
ayon_compatible_addons = {
|
||||
"applications": ">=0.2.0",
|
||||
}
|
||||
|
|
|
|||
13
server_addon/flame/client/ayon_flame/__init__.py
Normal file
13
server_addon/flame/client/ayon_flame/__init__.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from .version import __version__
|
||||
from .addon import (
|
||||
FLAME_ADDON_ROOT,
|
||||
FlameAddon,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"__version__",
|
||||
|
||||
"FLAME_ADDON_ROOT",
|
||||
"FlameAddon",
|
||||
)
|
||||
|
|
@ -1,16 +1,19 @@
|
|||
import os
|
||||
from ayon_core.addon import AYONAddon, IHostAddon
|
||||
|
||||
HOST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
from .version import __version__
|
||||
|
||||
FLAME_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
class FlameAddon(AYONAddon, IHostAddon):
|
||||
name = "flame"
|
||||
version = __version__
|
||||
host_name = "flame"
|
||||
|
||||
def add_implementation_envs(self, env, _app):
|
||||
# Add requirements to DL_PYTHON_HOOK_PATH
|
||||
env["DL_PYTHON_HOOK_PATH"] = os.path.join(HOST_DIR, "startup")
|
||||
env["DL_PYTHON_HOOK_PATH"] = os.path.join(FLAME_ADDON_ROOT, "startup")
|
||||
env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None)
|
||||
|
||||
# Set default values if are not already set via settings
|
||||
|
|
@ -25,7 +28,7 @@ class FlameAddon(AYONAddon, IHostAddon):
|
|||
if app.host_name != self.host_name:
|
||||
return []
|
||||
return [
|
||||
os.path.join(HOST_DIR, "hooks")
|
||||
os.path.join(FLAME_ADDON_ROOT, "hooks")
|
||||
]
|
||||
|
||||
def get_workfile_extensions(self):
|
||||
|
|
@ -28,7 +28,7 @@ default_flame_export_presets = {
|
|||
|
||||
|
||||
def callback_selection(selection, function):
|
||||
import ayon_core.hosts.flame.api as opfapi
|
||||
import ayon_flame.api as opfapi
|
||||
opfapi.CTX.selection = selection
|
||||
print("Hook Selection: \n\t{}".format(
|
||||
pformat({
|
||||
|
|
@ -13,6 +13,7 @@ from ayon_core.pipeline import (
|
|||
deregister_creator_plugin_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
)
|
||||
from ayon_flame import FLAME_ADDON_ROOT
|
||||
from .lib import (
|
||||
set_segment_data_marker,
|
||||
set_publish_attribute,
|
||||
|
|
@ -20,10 +21,8 @@ from .lib import (
|
|||
get_current_sequence,
|
||||
reset_segment_selection
|
||||
)
|
||||
from .. import HOST_DIR
|
||||
|
||||
API_DIR = os.path.join(HOST_DIR, "api")
|
||||
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
|
||||
PLUGINS_DIR = os.path.join(FLAME_ADDON_ROOT, "plugins")
|
||||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
||||
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
||||
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
|
||||
|
|
@ -113,10 +112,6 @@ def on_pyblish_instance_toggled(instance, old_value, new_value):
|
|||
log.info("instance toggle: {}, old_value: {}, new_value:{} ".format(
|
||||
instance, old_value, new_value))
|
||||
|
||||
# from ayon_core.hosts.resolve import (
|
||||
# set_publish_attribute
|
||||
# )
|
||||
|
||||
# # Whether instances should be passthrough based on new value
|
||||
# timeline_item = instance.data["item"]
|
||||
# set_publish_attribute(timeline_item, new_value)
|
||||
|
|
@ -5,6 +5,8 @@ Flame utils for syncing scripts
|
|||
import os
|
||||
import shutil
|
||||
from ayon_core.lib import Logger
|
||||
from ayon_flame import FLAME_ADDON_ROOT
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
||||
|
|
@ -16,7 +18,6 @@ def _sync_utility_scripts(env=None):
|
|||
`/opt/Autodesk/shared/python`. This will be always synchronizing those
|
||||
folders.
|
||||
"""
|
||||
from .. import HOST_DIR
|
||||
|
||||
env = env or os.environ
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ def _sync_utility_scripts(env=None):
|
|||
flame_shared_dir = "/opt/Autodesk/shared/python"
|
||||
|
||||
fsd_paths = [os.path.join(
|
||||
HOST_DIR,
|
||||
FLAME_ADDON_ROOT,
|
||||
"api",
|
||||
"utility_scripts"
|
||||
)]
|
||||
|
|
@ -10,7 +10,7 @@ from ayon_core.lib import (
|
|||
run_subprocess,
|
||||
)
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
from ayon_core.hosts import flame as opflame
|
||||
from ayon_flame import FLAME_ADDON_ROOT
|
||||
|
||||
|
||||
class FlamePrelaunch(PreLaunchHook):
|
||||
|
|
@ -23,7 +23,8 @@ class FlamePrelaunch(PreLaunchHook):
|
|||
permissions = 0o777
|
||||
|
||||
wtc_script_path = os.path.join(
|
||||
opflame.HOST_DIR, "api", "scripts", "wiretap_com.py")
|
||||
FLAME_ADDON_ROOT, "api", "scripts", "wiretap_com.py"
|
||||
)
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
|
@ -275,7 +275,7 @@ def create_otio_reference(clip_data, fps=None):
|
|||
|
||||
|
||||
def create_otio_clip(clip_data):
|
||||
from ayon_core.hosts.flame.api import MediaInfoFile, TimeEffectMetadata
|
||||
from ayon_flame.api import MediaInfoFile, TimeEffectMetadata
|
||||
|
||||
segment = clip_data["PySegment"]
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
from copy import deepcopy
|
||||
import ayon_core.hosts.flame.api as opfapi
|
||||
import ayon_flame.api as opfapi
|
||||
|
||||
|
||||
class CreateShotClip(opfapi.Creator):
|
||||
|
|
@ -2,7 +2,7 @@ from copy import deepcopy
|
|||
import os
|
||||
import flame
|
||||
from pprint import pformat
|
||||
import ayon_core.hosts.flame.api as opfapi
|
||||
import ayon_flame.api as opfapi
|
||||
from ayon_core.lib import StringTemplate
|
||||
from ayon_core.lib.transcoding import (
|
||||
VIDEO_EXTENSIONS,
|
||||
|
|
@ -2,7 +2,7 @@ from copy import deepcopy
|
|||
import os
|
||||
import flame
|
||||
from pprint import pformat
|
||||
import ayon_core.hosts.flame.api as opfapi
|
||||
import ayon_flame.api as opfapi
|
||||
from ayon_core.lib import StringTemplate
|
||||
from ayon_core.lib.transcoding import (
|
||||
VIDEO_EXTENSIONS,
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
import tempfile
|
||||
import ayon_core.hosts.flame.api as opfapi
|
||||
from ayon_core.hosts.flame.otio import flame_export as otio_export
|
||||
import ayon_flame.api as opfapi
|
||||
from ayon_flame.otio import flame_export as otio_export
|
||||
import opentimelineio as otio
|
||||
from pprint import pformat
|
||||
reload(otio_export) # noqa
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import re
|
||||
from types import NoneType
|
||||
import pyblish
|
||||
import ayon_core.hosts.flame.api as opfapi
|
||||
from ayon_core.hosts.flame.otio import flame_export
|
||||
import ayon_flame.api as opfapi
|
||||
from ayon_flame.otio import flame_export
|
||||
from ayon_core.pipeline import AYON_INSTANCE_ID, AVALON_INSTANCE_ID
|
||||
from ayon_core.pipeline.editorial import (
|
||||
is_overlapping_otio_ranges,
|
||||
|
|
@ -24,6 +24,8 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
|
|||
label = "Collect timeline Instances"
|
||||
hosts = ["flame"]
|
||||
|
||||
settings_category = "flame"
|
||||
|
||||
audio_track_items = []
|
||||
|
||||
# settings
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import pyblish.api
|
||||
|
||||
import ayon_core.hosts.flame.api as opfapi
|
||||
from ayon_core.hosts.flame.otio import flame_export
|
||||
import ayon_flame.api as opfapi
|
||||
from ayon_flame.otio import flame_export
|
||||
from ayon_core.pipeline.create import get_product_name
|
||||
|
||||
|
||||
|
|
@ -5,8 +5,8 @@ from copy import deepcopy
|
|||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_core.hosts.flame import api as opfapi
|
||||
from ayon_core.hosts.flame.api import MediaInfoFile
|
||||
from ayon_flame import api as opfapi
|
||||
from ayon_flame.api import MediaInfoFile
|
||||
from ayon_core.pipeline.editorial import (
|
||||
get_media_range_with_retimes
|
||||
)
|
||||
|
|
@ -24,6 +24,8 @@ class ExtractProductResources(publish.Extractor):
|
|||
families = ["clip"]
|
||||
hosts = ["flame"]
|
||||
|
||||
settings_category = "flame"
|
||||
|
||||
# plugin defaults
|
||||
keep_original_representation = False
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ import copy
|
|||
from collections import OrderedDict
|
||||
from pprint import pformat
|
||||
import pyblish
|
||||
import ayon_core.hosts.flame.api as opfapi
|
||||
import ayon_flame.api as opfapi
|
||||
import ayon_core.pipeline as op_pipeline
|
||||
from ayon_core.pipeline.workfile import get_workdir
|
||||
|
||||
|
|
@ -16,6 +16,8 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin):
|
|||
hosts = ["flame"]
|
||||
families = ["clip"]
|
||||
|
||||
settings_category = "flame"
|
||||
|
||||
# settings
|
||||
default_loader = "LoadClip"
|
||||
|
||||
|
|
@ -4,7 +4,7 @@ from qtpy import QtWidgets
|
|||
from pprint import pformat
|
||||
import atexit
|
||||
|
||||
import ayon_core.hosts.flame.api as opfapi
|
||||
import ayon_flame.api as opfapi
|
||||
from ayon_core.pipeline import (
|
||||
install_host,
|
||||
registered_host,
|
||||
3
server_addon/flame/client/ayon_flame/version.py
Normal file
3
server_addon/flame/client/ayon_flame/version.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'flame' version."""
|
||||
__version__ = "0.2.0"
|
||||
|
|
@ -1,3 +1,10 @@
|
|||
name = "flame"
|
||||
title = "Flame"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
client_dir = "ayon_flame"
|
||||
|
||||
ayon_required_addons = {
|
||||
"core": ">0.3.2",
|
||||
}
|
||||
ayon_compatible_addons = {}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from pymxs import runtime as rt
|
|||
|
||||
|
||||
JSON_PREFIX = "JSON::"
|
||||
log = logging.getLogger("ayon_core.hosts.max")
|
||||
log = logging.getLogger("ayon_max")
|
||||
|
||||
|
||||
def get_main_window():
|
||||
|
|
@ -6,7 +6,7 @@ import os
|
|||
|
||||
from pymxs import runtime as rt
|
||||
|
||||
from ayon_core.hosts.max.api.lib import get_current_renderer
|
||||
from ayon_max.api.lib import get_current_renderer
|
||||
from ayon_core.pipeline import get_current_project_name
|
||||
from ayon_core.settings import get_project_settings
|
||||
|
||||
|
|
@ -5,7 +5,7 @@ from ayon_core.settings import get_project_settings
|
|||
from ayon_core.pipeline import get_current_project_name
|
||||
from ayon_core.pipeline.context_tools import get_current_folder_entity
|
||||
|
||||
from ayon_core.hosts.max.api.lib import (
|
||||
from ayon_max.api.lib import (
|
||||
set_render_frame_range,
|
||||
get_current_renderer,
|
||||
get_default_render_folder
|
||||
|
|
@ -5,7 +5,7 @@ from qtpy import QtWidgets, QtCore
|
|||
from pymxs import runtime as rt
|
||||
|
||||
from ayon_core.tools.utils import host_tools
|
||||
from ayon_core.hosts.max.api import lib
|
||||
from ayon_max.api import lib
|
||||
|
||||
|
||||
class AYONMenu(object):
|
||||
|
|
@ -14,14 +14,14 @@ from ayon_core.pipeline import (
|
|||
AVALON_CONTAINER_ID,
|
||||
AYON_CONTAINER_ID,
|
||||
)
|
||||
from ayon_core.hosts.max.api.menu import AYONMenu
|
||||
from ayon_core.hosts.max.api import lib
|
||||
from ayon_core.hosts.max.api.plugin import MS_CUSTOM_ATTRIB
|
||||
from ayon_core.hosts.max import MAX_HOST_DIR
|
||||
from ayon_max.api.menu import AYONMenu
|
||||
from ayon_max.api import lib
|
||||
from ayon_max.api.plugin import MS_CUSTOM_ATTRIB
|
||||
from ayon_max import MAX_HOST_DIR
|
||||
|
||||
from pymxs import runtime as rt # noqa
|
||||
|
||||
log = logging.getLogger("ayon_core.hosts.max")
|
||||
log = logging.getLogger("ayon_max")
|
||||
|
||||
PLUGINS_DIR = os.path.join(MAX_HOST_DIR, "plugins")
|
||||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
||||
|
|
@ -3,7 +3,7 @@ import contextlib
|
|||
from pymxs import runtime as rt
|
||||
from .lib import get_max_version, render_resolution
|
||||
|
||||
log = logging.getLogger("ayon_core.hosts.max")
|
||||
log = logging.getLogger("ayon_max")
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Pre-launch to force 3ds max startup script."""
|
||||
import os
|
||||
from ayon_core.hosts.max import MAX_HOST_DIR
|
||||
from ayon_max import MAX_HOST_DIR
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue