Merge remote-tracking branch 'origin/develop' into feature/143-publishing_of_rendered_vrscenes

This commit is contained in:
Ondrej Samohel 2020-06-08 09:39:19 +02:00
commit e65112ee92
No known key found for this signature in database
GPG key ID: 8A29C663C672C2B7
82 changed files with 2732 additions and 1325 deletions

View file

@ -1,39 +0,0 @@
import os
from avalon import api, harmony
import pyblish.api
def install():
print("Installing Pype config...")
plugins_directory = os.path.join(
os.path.dirname(os.path.dirname(__file__)), "plugins", "harmony"
)
pyblish.api.register_plugin_path(
os.path.join(plugins_directory, "publish")
)
api.register_plugin_path(
api.Loader, os.path.join(plugins_directory, "load")
)
api.register_plugin_path(
api.Creator, os.path.join(plugins_directory, "create")
)
pyblish.api.register_callback(
"instanceToggled", on_pyblish_instance_toggled
)
def on_pyblish_instance_toggled(instance, old_value, new_value):
"""Toggle node enabling on instance toggles."""
func = """function func(args)
{
node.setEnable(args[0], args[1])
}
func
"""
harmony.send(
{"function": func, "args": [instance[0], new_value]}
)

View file

@ -0,0 +1,67 @@
import os
import traceback
import importlib
from pype.lib import PypeHook
from pypeapp import Logger
from pype.hosts.resolve import utils
class ResolvePrelaunch(PypeHook):
"""
This hook will check if current workfile path has Resolve
project inside. IF not, it initialize it and finally it pass
path to the project by environment variable to Premiere launcher
shell script.
"""
def __init__(self, logger=None):
if not logger:
self.log = Logger().get_logger(self.__class__.__name__)
else:
self.log = logger
self.signature = "( {} )".format(self.__class__.__name__)
def execute(self, *args, env: dict = None) -> bool:
if not env:
env = os.environ
# making sure pyton 3.6 is installed at provided path
py36_dir = os.path.normpath(env.get("PYTHON36_RESOLVE", ""))
assert os.path.isdir(py36_dir), (
"Python 3.6 is not installed at the provided folder path. Either "
"make sure the `environments\resolve.json` is having correctly "
"set `PYTHON36_RESOLVE` or make sure Python 3.6 is installed "
f"in given path. \nPYTHON36_RESOLVE: `{py36_dir}`"
)
self.log.info(f"Path to Resolve Python folder: `{py36_dir}`...")
env["PYTHON36_RESOLVE"] = py36_dir
# setting utility scripts dir for scripts syncing
us_dir = os.path.normpath(env.get("RESOLVE_UTILITY_SCRIPTS_DIR", ""))
assert os.path.isdir(us_dir), (
"Resolve utility script dir does not exists. Either make sure "
"the `environments\resolve.json` is having correctly set "
"`RESOLVE_UTILITY_SCRIPTS_DIR` or reinstall DaVinci Resolve. \n"
f"RESOLVE_UTILITY_SCRIPTS_DIR: `{us_dir}`"
)
# correctly format path for pre python script
pre_py_sc = os.path.normpath(env.get("PRE_PYTHON_SCRIPT", ""))
env["PRE_PYTHON_SCRIPT"] = pre_py_sc
try:
__import__("pype.resolve")
__import__("pyblish")
except ImportError as e:
print(traceback.format_exc())
print("pyblish: Could not load integration: %s " % e)
else:
# Resolve Setup integration
importlib.reload(utils)
utils.setup(env)
return True

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,189 @@
Updated as of 08 March 2019
--------------------------
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.
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, 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.
Using a script
--------------
DaVinci Resolve needs to be running for a script to be invoked.
For a Resolve script to be executed from an external folder, the script needs to know of the API location.
You may need to set the these environment variables to allow for your Python installation to pick up the appropriate dependencies as shown below:
Mac OS X:
RESOLVE_SCRIPT_API="/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/"
RESOLVE_SCRIPT_LIB="/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so"
PYTHONPATH="$PYTHONPATH:$RESOLVE_SCRIPT_API/Modules/"
Windows:
RESOLVE_SCRIPT_API="%PROGRAMDATA%\\Blackmagic Design\\DaVinci Resolve\\Support\\Developer\\Scripting\\"
RESOLVE_SCRIPT_LIB="C:\\Program Files\\Blackmagic Design\\DaVinci Resolve\\fusionscript.dll"
PYTHONPATH="%PYTHONPATH%;%RESOLVE_SCRIPT_API%\\Modules\\"
Linux:
RESOLVE_SCRIPT_API="/opt/resolve/Developer/Scripting/"
RESOLVE_SCRIPT_LIB="/opt/resolve/libs/Fusion/fusionscript.so"
PYTHONPATH="$PYTHONPATH:$RESOLVE_SCRIPT_API/Modules/"
(Note: For standard ISO Linux installations, the path above may need to be modified to refer to /home/resolve instead of /opt/resolve)
As with Fusion scripts, Resolve scripts can also be invoked via the menu and the Console.
On startup, DaVinci Resolve scans the Utility Scripts directory and enumerates the scripts found in the Script application menu. Placing your script in this folder and invoking it from this menu is the easiest way to use scripts. The Utility Scripts folder is located in:
Mac OS X: /Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Comp/
Windows: %APPDATA%\Blackmagic Design\DaVinci Resolve\Fusion\Scripts\Comp\
Linux: /opt/resolve/Fusion/Scripts/Comp/ (or /home/resolve/Fusion/Scripts/Comp/ depending on installation)
The interactive Console window allows for an easy way to execute simple scripting commands, to query or modify properties, and to test scripts. The console accepts commands in Python 2.7, Python 3.6 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")
fusion = resolve.Fusion()
projectManager = resolve.GetProjectManager()
projectManager.CreateProject("Hello World")
The resolve object is the fundamental starting point for scripting via Resolve. As a native object, it can be inspected for further scriptable properties - using table iteration and `getmetatable` in Lua and dir, help etc in Python (among other methods). A notable scriptable object above is fusion - it allows access to all existing Fusion scripting functionality.
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
-----------------
Some commonly used API functions are described below (*). As with the resolve object, each object is inspectable for properties and functions.
Resolve
Fusion() --> Fusion # Returns the Fusion object. Starting point for Fusion scripts.
GetMediaStorage() --> MediaStorage # Returns media storage object to query and act on media locations.
GetProjectManager() --> ProjectManager # Returns project manager object for currently open database.
OpenPage(pageName) --> None # Switches to indicated page in DaVinci Resolve. Input can be one of ("media", "edit", "fusion", "color", "fairlight", "deliver").
ProjectManager
CreateProject(projectName) --> Project # Creates and returns a project if projectName (text) is unique, and None if it is not.
LoadProject(projectName) --> Project # Loads and returns the project with name = projectName (text) if there is a match found, and None if there is no matching Project.
GetCurrentProject() --> Project # Returns the currently loaded Resolve project.
SaveProject() --> Bool # Saves the currently loaded project with its own name. Returns True if successful.
CreateFolder(folderName) --> Bool # Creates a folder if folderName (text) is unique.
GetProjectsInCurrentFolder() --> [project names...] # Returns an array of project names in current folder.
GetFoldersInCurrentFolder() --> [folder names...] # Returns an array of folder names in current folder.
GotoRootFolder() --> Bool # Opens root folder in database.
GotoParentFolder() --> Bool # Opens parent folder of current folder in database if current folder has parent.
OpenFolder(folderName) --> Bool # Opens folder under given name.
ImportProject(filePath) --> Bool # Imports a project under given file path. Returns true in case of success.
ExportProject(projectName, filePath) --> Bool # Exports a project based on given name into provided file path. Returns true in case of success.
RestoreProject(filePath) --> Bool # Restores a project under given backup file path. Returns true in case of success.
Project
GetMediaPool() --> MediaPool # Returns the Media Pool object.
GetTimelineCount() --> int # Returns the number of timelines currently present in the project.
GetTimelineByIndex(idx) --> Timeline # Returns timeline at the given index, 1 <= idx <= project.GetTimelineCount()
GetCurrentTimeline() --> Timeline # Returns the currently loaded timeline.
SetCurrentTimeline(timeline) --> Bool # Sets given timeline as current timeline for the project. Returns True if successful.
GetName() --> string # Returns project name.
SetName(projectName) --> Bool # Sets project name if given projectname (text) is unique.
GetPresets() --> [presets...] # Returns a table of presets and their information.
SetPreset(presetName) --> Bool # Sets preset by given presetName (string) into project.
GetRenderJobs() --> [render jobs...] # Returns a table of render jobs and their information.
GetRenderPresets() --> [presets...] # Returns a table of render presets and their information.
StartRendering(index1, index2, ...) --> Bool # Starts rendering for given render jobs based on their indices. If no parameter is given rendering would start for all render jobs.
StartRendering([idxs...]) --> Bool # Starts rendering for given render jobs based on their indices. If no parameter is given rendering would start for all render jobs.
StopRendering() --> None # Stops rendering for all render jobs.
IsRenderingInProgress() --> Bool # Returns true is rendering is in progress.
AddRenderJob() --> Bool # Adds render job to render queue.
DeleteRenderJobByIndex(idx) --> Bool # Deletes render job based on given job index (int).
DeleteAllRenderJobs() --> Bool # Deletes all render jobs.
LoadRenderPreset(presetName) --> Bool # Sets a preset as current preset for rendering if presetName (text) exists.
SaveAsNewRenderPreset(presetName) --> Bool # Creates a new render preset by given name if presetName(text) is unique.
SetRenderSettings([settings map]) --> Bool # Sets given settings for rendering. Settings map is a map, keys of map are: "SelectAllFrames", "MarkIn", "MarkOut", "TargetDir", "CustomName".
GetRenderJobStatus(idx) --> [status info] # Returns job status and completion rendering percentage of the job by given job index (int).
GetSetting(settingName) --> string # Returns setting value by given settingName (string) if the setting exist. With empty settingName the function returns a full list of settings.
SetSetting(settingName, settingValue) --> Bool # Sets project setting base on given name (string) and value (string).
GetRenderFormats() --> [render formats...]# Returns a list of available render formats.
GetRenderCodecs(renderFormat) --> [render codecs...] # Returns a list of available codecs for given render format (string).
GetCurrentRenderFormatAndCodec() --> [format, codec] # Returns currently selected render format and render codec.
SetCurrentRenderFormatAndCodec(format, codec) --> Bool # Sets given render format (string) and render codec (string) as options for rendering.
MediaStorage
GetMountedVolumes() --> [paths...] # Returns an array of folder paths corresponding to mounted volumes displayed in Resolves Media Storage.
GetSubFolders(folderPath) --> [paths...] # Returns an array of folder paths in the given absolute folder path.
GetFiles(folderPath) --> [paths...] # Returns an array of media and file listings in the given absolute folder path. Note that media listings may be logically consolidated entries.
RevealInStorage(path) --> None # Expands and displays a given file/folder path in Resolves Media Storage.
AddItemsToMediaPool(item1, item2, ...) --> [clips...] # Adds specified file/folder paths from Media Store into current Media Pool folder. Input is one or more file/folder paths.
AddItemsToMediaPool([items...]) --> [clips...] # Adds specified file/folder paths from Media Store into current Media Pool folder. Input is an array of file/folder paths.
MediaPool
GetRootFolder() --> Folder # Returns the root Folder of Media Pool
AddSubFolder(folder, name) --> Folder # Adds a new subfolder under specified Folder object with the given name.
CreateEmptyTimeline(name) --> Timeline # Adds a new timeline with given name.
AppendToTimeline(clip1, clip2...) --> Bool # Appends specified MediaPoolItem objects in the current timeline. Returns True if successful.
AppendToTimeline([clips]) --> Bool # Appends specified MediaPoolItem objects in the current timeline. Returns True if successful.
CreateTimelineFromClips(name, clip1, clip2, ...)--> Timeline # Creates a new timeline with specified name, and appends the specified MediaPoolItem objects.
CreateTimelineFromClips(name, [clips]) --> Timeline # Creates a new timeline with specified name, and appends the specified MediaPoolItem objects.
ImportTimelineFromFile(filePath) --> Timeline # Creates timeline based on parameters within given file.
GetCurrentFolder() --> Folder # Returns currently selected Folder.
SetCurrentFolder(Folder) --> Bool # Sets current folder by given Folder.
Folder
GetClips() --> [clips...] # Returns a list of clips (items) within the folder.
GetName() --> string # Returns user-defined name of the folder.
GetSubFolders() --> [folders...] # Returns a list of subfolders in the folder.
MediaPoolItem
GetMetadata(metadataType) --> [[types],[values]] # Returns a value of metadataType. If parameter is not specified returns all set metadata parameters.
SetMetadata(metadataType, metadataValue) --> Bool # Sets metadata by given type and value. Returns True if successful.
GetMediaId() --> string # Returns a unique ID name related to MediaPoolItem.
AddMarker(frameId, color, name, note, duration) --> Bool # Creates a new marker at given frameId position and with given marker information.
GetMarkers() --> [markers...] # Returns a list of all markers and their information.
AddFlag(color) --> Bool # Adds a flag with given color (text).
GetFlags() --> [colors...] # Returns a list of flag colors assigned to the item.
GetClipColor() --> string # Returns an item color as a string.
GetClipProperty(propertyName) --> [[types],[values]] # Returns property value related to the item based on given propertyName (string). if propertyName is empty then it returns a full list of properties.
SetClipProperty(propertyName, propertyValue) --> Bool # Sets into given propertyName (string) propertyValue (string).
Timeline
GetName() --> string # Returns user-defined name of the timeline.
SetName(timelineName) --> Bool # Sets timeline name is timelineName (text) is unique.
GetStartFrame() --> int # Returns frame number at the start of timeline.
GetEndFrame() --> int # Returns frame number at the end of timeline.
GetTrackCount(trackType) --> int # Returns a number of track based on specified track type ("audio", "video" or "subtitle").
GetItemsInTrack(trackType, index) --> [items...] # Returns an array of Timeline items on the video or audio track (based on trackType) at specified index. 1 <= index <= GetTrackCount(trackType).
AddMarker(frameId, color, name, note, duration) --> Bool # Creates a new marker at given frameId position and with given marker information.
GetMarkers() --> [markers...] # Returns a list of all markers and their information.
ApplyGradeFromDRX(path, gradeMode, item1, item2, ...)--> Bool # Loads a still from given file path (string) and applies grade to Timeline Items with gradeMode (int): 0 - "No keyframes", 1 - "Source Timecode aligned", 2 - "Start Frames aligned".
ApplyGradeFromDRX(path, gradeMode, [items]) --> Bool # Loads a still from given file path (string) and applies grade to Timeline Items with gradeMode (int): 0 - "No keyframes", 1 - "Source Timecode aligned", 2 - "Start Frames aligned".
GetCurrentTimecode() --> string # Returns a string representing a timecode for current position of the timeline, while on Cut, Edit, Color and Deliver page.
GetCurrentVideoItem() --> item # Returns current video timeline item.
GetCurrentClipThumbnailImage() --> [width, height, format, data] # Returns raw thumbnail image data (This image data is encoded in base 64 format and the image format is RGB 8 bit) for the current media in the Color Page in the format of dictionary (in Python) and table (in Lua). Information return are "width", "height", "format" and "data". Example is provided in 6_get_current_media_thumbnail.py in Example folder.
TimelineItem
GetName() --> string # Returns a name of the item.
GetDuration() --> int # Returns a duration of item.
GetEnd() --> int # Returns a position of end frame.
GetFusionCompCount() --> int # Returns the number of Fusion compositions associated with the timeline item.
GetFusionCompByIndex(compIndex) --> fusionComp # Returns Fusion composition object based on given index. 1 <= compIndex <= timelineItem.GetFusionCompCount()
GetFusionCompNames() --> [names...] # Returns a list of Fusion composition names associated with the timeline item.
GetFusionCompByName(compName) --> fusionComp # Returns Fusion composition object based on given name.
GetLeftOffset() --> int # Returns a maximum extension by frame for clip from left side.
GetRightOffset() --> int # Returns a maximum extension by frame for clip from right side.
GetStart() --> int # Returns a position of first frame.
AddMarker(frameId, color, name, note, duration) --> Bool # Creates a new marker at given frameId position and with given marker information.
GetMarkers() --> [markers...] # Returns a list of all markers and their information.
GetFlags() --> [colors...] # Returns a list of flag colors assigned to the item.
GetClipColor() --> string # Returns an item color as a string.
AddFusionComp() --> fusionComp # Adds a new Fusion composition associated with the timeline item.
ImportFusionComp(path) --> fusionComp # Imports Fusion composition from given file path by creating and adding a new composition for the item.
ExportFusionComp(path, compIndex) --> Bool # Exports Fusion composition based on given index into provided file name path.
DeleteFusionCompByName(compName) --> Bool # Deletes Fusion composition by provided name.
LoadFusionCompByName(compName) --> fusionComp # Loads Fusion composition by provided name and sets it as active composition.
RenameFusionCompByName(oldName, newName) --> Bool # Renames Fusion composition by provided name with new given name.
AddVersion(versionName, versionType) --> Bool # Adds a new Version associated with the timeline item. versionType: 0 - local, 1 - remote.
DeleteVersionByName(versionName, versionType) --> Bool # Deletes Version by provided name. versionType: 0 - local, 1 - remote.
LoadVersionByName(versionName, versionType) --> Bool # Loads Version by provided name and sets it as active Version. versionType: 0 - local, 1 - remote.
RenameVersionByName(oldName, newName, versionType)--> Bool # Renames Version by provided name with new given name. versionType: 0 - local, 1 - remote.
GetMediaPoolItem() --> MediaPoolItem # Returns a corresponding to the timeline item media pool item if it exists.
GetVersionNames(versionType) --> [strings...] # Returns a list of version names by provided versionType: 0 - local, 1 - remote.
GetStereoConvergenceValues() --> [offset, value] # Returns a table of keyframe offsets and respective convergence values
GetStereoLeftFloatingWindowParams() --> [offset, value] # For the LEFT eye -> returns a table of keyframe offsets and respective floating window params. Value at particular offset includes the left, right, top and bottom floating window values
GetStereoRightFloatingWindowParams() --> [offset, value] # For the RIGHT eye -> returns a table of keyframe offsets and respective floating window params. Value at particular offset includes the left, right, top and bottom floating window values

View file

@ -0,0 +1,59 @@
from .pipeline import (
install,
uninstall,
ls,
containerise,
publish,
launch_workfiles_app
)
from .utils import (
setup,
get_resolve_module
)
from .workio import (
open_file,
save_file,
current_file,
has_unsaved_changes,
file_extensions,
work_root
)
from .lib import (
get_project_manager,
set_project_manager_to_folder_name
)
from .menu import launch_pype_menu
__all__ = [
# pipeline
"install",
"uninstall",
"ls",
"containerise",
"reload_pipeline",
"publish",
"launch_workfiles_app",
# utils
"setup",
"get_resolve_module",
# lib
"get_project_manager",
"set_project_manager_to_folder_name",
# menu
"launch_pype_menu",
# workio
"open_file",
"save_file",
"current_file",
"has_unsaved_changes",
"file_extensions",
"work_root"
]

View file

@ -0,0 +1,54 @@
# absolute_import is needed to counter the `module has no cmds error` in Maya
from __future__ import absolute_import
import pyblish.api
from ...action import get_errored_instances_from_context
class SelectInvalidAction(pyblish.api.Action):
"""Select invalid clips in Resolve timeline when plug-in failed.
To retrieve the invalid nodes this assumes a static `get_invalid()`
method is available on the plugin.
"""
label = "Select invalid"
on = "failed" # This action is only available on a failed plug-in
icon = "search" # Icon from Awesome Icon
def process(self, context, plugin):
try:
from pype.hosts.resolve.utils import get_resolve_module
resolve = get_resolve_module()
self.log.debug(resolve)
except ImportError:
raise ImportError("Current host is not Resolve")
errored_instances = get_errored_instances_from_context(context)
# Apply pyblish.logic to get the instances for the plug-in
instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
# Get the invalid nodes for the plug-ins
self.log.info("Finding invalid clips..")
invalid = list()
for instance in instances:
invalid_nodes = plugin.get_invalid(instance)
if invalid_nodes:
if isinstance(invalid_nodes, (list, tuple)):
invalid.extend(invalid_nodes)
else:
self.log.warning("Plug-in returned to be invalid, "
"but has no selectable nodes.")
# Ensure unique (process each node only once)
invalid = list(set(invalid))
if invalid:
self.log.info("Selecting invalid nodes: %s" % ", ".join(invalid))
# TODO: select resolve timeline track items in current timeline
else:
self.log.info("No invalid nodes found.")

78
pype/hosts/resolve/lib.py Normal file
View file

@ -0,0 +1,78 @@
import sys
from .utils import get_resolve_module
from pypeapp import Logger
log = Logger().get_logger(__name__, "resolve")
self = sys.modules[__name__]
self.pm = None
def get_project_manager():
if not self.pm:
resolve = get_resolve_module()
self.pm = resolve.GetProjectManager()
return self.pm
def set_project_manager_to_folder_name(folder_name):
"""
Sets context of Project manager to given folder by name.
Searching for folder by given name from root folder to nested.
If no existing folder by name it will create one in root folder.
Args:
folder_name (str): name of searched folder
Returns:
bool: True if success
Raises:
Exception: Cannot create folder in root
"""
# initialize project manager
get_project_manager()
set_folder = False
# go back to root folder
if self.pm.GotoRootFolder():
log.info(f"Testing existing folder: {folder_name}")
folders = convert_resolve_list_type(
self.pm.GetFoldersInCurrentFolder())
log.info(f"Testing existing folders: {folders}")
# get me first available folder object
# with the same name as in `folder_name` else return False
if next((f for f in folders if f in folder_name), False):
log.info(f"Found existing folder: {folder_name}")
set_folder = self.pm.OpenFolder(folder_name)
if set_folder:
return True
# if folder by name is not existent then create one
# go back to root folder
log.info(f"Folder `{folder_name}` not found and will be created")
if self.pm.GotoRootFolder():
try:
# create folder by given name
self.pm.CreateFolder(folder_name)
self.pm.OpenFolder(folder_name)
return True
except NameError as e:
log.error((f"Folder with name `{folder_name}` cannot be created!"
f"Error: {e}"))
return False
def convert_resolve_list_type(resolve_list):
""" Resolve is using indexed dictionary as list type.
`{1.0: 'vaule'}`
This will convert it to normal list class
"""
assert isinstance(resolve_list, dict), (
"Input argument should be dict() type")
return [resolve_list[i] for i in sorted(resolve_list.keys())]

154
pype/hosts/resolve/menu.py Normal file
View file

@ -0,0 +1,154 @@
import os
import sys
from Qt import QtWidgets, QtCore
from .pipeline import (
publish,
launch_workfiles_app
)
from avalon.tools import (
creator,
loader,
sceneinventory,
libraryloader
)
def load_stylesheet():
path = os.path.join(os.path.dirname(__file__), "menu_style.qss")
if not os.path.exists(path):
print("Unable to load stylesheet, file not found in resources")
return ""
with open(path, "r") as file_stream:
stylesheet = file_stream.read()
return stylesheet
class Spacer(QtWidgets.QWidget):
def __init__(self, height, *args, **kwargs):
super(self.__class__, self).__init__(*args, **kwargs)
self.setFixedHeight(height)
real_spacer = QtWidgets.QWidget(self)
real_spacer.setObjectName("Spacer")
real_spacer.setFixedHeight(height)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(real_spacer)
self.setLayout(layout)
class PypeMenu(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super(self.__class__, self).__init__(*args, **kwargs)
self.setObjectName("PypeMenu")
self.setWindowFlags(
QtCore.Qt.Window
| QtCore.Qt.CustomizeWindowHint
| QtCore.Qt.WindowTitleHint
| QtCore.Qt.WindowCloseButtonHint
| QtCore.Qt.WindowStaysOnTopHint
)
self.setWindowTitle("Pype")
workfiles_btn = QtWidgets.QPushButton("Workfiles", self)
create_btn = QtWidgets.QPushButton("Create", self)
publish_btn = QtWidgets.QPushButton("Publish", self)
load_btn = QtWidgets.QPushButton("Load", self)
inventory_btn = QtWidgets.QPushButton("Inventory", self)
libload_btn = QtWidgets.QPushButton("Library", self)
rename_btn = QtWidgets.QPushButton("Rename", self)
set_colorspace_btn = QtWidgets.QPushButton(
"Set colorspace from presets", self
)
reset_resolution_btn = QtWidgets.QPushButton(
"Reset Resolution from peresets", self
)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(10, 20, 10, 20)
layout.addWidget(workfiles_btn)
layout.addWidget(create_btn)
layout.addWidget(publish_btn)
layout.addWidget(load_btn)
layout.addWidget(inventory_btn)
layout.addWidget(Spacer(15, self))
layout.addWidget(libload_btn)
layout.addWidget(Spacer(15, self))
layout.addWidget(rename_btn)
layout.addWidget(Spacer(15, self))
layout.addWidget(set_colorspace_btn)
layout.addWidget(reset_resolution_btn)
self.setLayout(layout)
workfiles_btn.clicked.connect(self.on_workfile_clicked)
create_btn.clicked.connect(self.on_create_clicked)
publish_btn.clicked.connect(self.on_publish_clicked)
load_btn.clicked.connect(self.on_load_clicked)
inventory_btn.clicked.connect(self.on_inventory_clicked)
libload_btn.clicked.connect(self.on_libload_clicked)
rename_btn.clicked.connect(self.on_rename_clicked)
set_colorspace_btn.clicked.connect(self.on_set_colorspace_clicked)
reset_resolution_btn.clicked.connect(self.on_reset_resolution_clicked)
def on_workfile_clicked(self):
print("Clicked Workfile")
launch_workfiles_app()
def on_create_clicked(self):
print("Clicked Create")
creator.show()
def on_publish_clicked(self):
print("Clicked Publish")
publish(None)
def on_load_clicked(self):
print("Clicked Load")
loader.show(use_context=True)
def on_inventory_clicked(self):
print("Clicked Inventory")
sceneinventory.show()
def on_libload_clicked(self):
print("Clicked Library")
libraryloader.show()
def on_rename_clicked(self):
print("Clicked Rename")
def on_set_colorspace_clicked(self):
print("Clicked Set Colorspace")
def on_reset_resolution_clicked(self):
print("Clicked Reset Resolution")
def launch_pype_menu():
app = QtWidgets.QApplication(sys.argv)
pype_menu = PypeMenu()
stylesheet = load_stylesheet()
pype_menu.setStyleSheet(stylesheet)
pype_menu.show()
sys.exit(app.exec_())

View file

@ -0,0 +1,29 @@
QWidget {
background-color: #282828;
border-radius: 3;
}
QPushButton {
border: 1px solid #090909;
background-color: #201f1f;
color: #ffffff;
padding: 5;
}
QPushButton:focus {
background-color: "#171717";
color: #d0d0d0;
}
QPushButton:hover {
background-color: "#171717";
color: #e64b3d;
}
#PypeMenu {
border: 1px solid #fef9ef;
}
#Spacer {
background-color: #282828;
}

View file

@ -0,0 +1,142 @@
"""
Basic avalon integration
"""
import os
# import sys
from avalon.tools import workfiles
from avalon import api as avalon
from pyblish import api as pyblish
from pypeapp import Logger
log = Logger().get_logger(__name__, "resolve")
# self = sys.modules[__name__]
AVALON_CONFIG = os.environ["AVALON_CONFIG"]
PARENT_DIR = os.path.dirname(__file__)
PACKAGE_DIR = os.path.dirname(PARENT_DIR)
PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins")
LOAD_PATH = os.path.join(PLUGINS_DIR, "resolve", "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "resolve", "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "resolve", "inventory")
PUBLISH_PATH = os.path.join(
PLUGINS_DIR, "resolve", "publish"
).replace("\\", "/")
AVALON_CONTAINERS = ":AVALON_CONTAINERS"
# IS_HEADLESS = not hasattr(cmds, "about") or cmds.about(batch=True)
def install():
"""Install resolve-specific functionality of avalon-core.
This is where you install menus and register families, data
and loaders into resolve.
It is called automatically when installing via `api.install(resolve)`.
See the Maya equivalent for inspiration on how to implement this.
"""
# Disable all families except for the ones we explicitly want to see
family_states = [
"imagesequence",
"mov"
]
avalon.data["familiesStateDefault"] = False
avalon.data["familiesStateToggled"] = family_states
log.info("pype.hosts.resolve installed")
pyblish.register_host("resolve")
pyblish.register_plugin_path(PUBLISH_PATH)
log.info("Registering DaVinci Resovle plug-ins..")
avalon.register_plugin_path(avalon.Loader, LOAD_PATH)
avalon.register_plugin_path(avalon.Creator, CREATE_PATH)
avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH)
def uninstall():
"""Uninstall all tha was installed
This is where you undo everything that was done in `install()`.
That means, removing menus, deregistering families and data
and everything. It should be as though `install()` was never run,
because odds are calling this function means the user is interested
in re-installing shortly afterwards. If, for example, he has been
modifying the menu or registered families.
"""
pyblish.deregister_host("resolve")
pyblish.deregister_plugin_path(PUBLISH_PATH)
log.info("Deregistering DaVinci Resovle plug-ins..")
avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH)
avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH)
avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH)
def containerise(obj,
name,
namespace,
context,
loader=None,
data=None):
"""Bundle Resolve's object into an assembly and imprint it with metadata
Containerisation enables a tracking of version, author and origin
for loaded assets.
Arguments:
obj (obj): Resolve's object to imprint as container
name (str): Name of resulting assembly
namespace (str): Namespace under which to host container
context (dict): Asset information
loader (str, optional): Name of node used to produce this container.
Returns:
obj (obj): containerised object
"""
pass
def ls():
"""List available containers.
This function is used by the Container Manager in Nuke. You'll
need to implement a for-loop that then *yields* one Container at
a time.
See the `container.json` schema for details on how it should look,
and the Maya equivalent, which is in `avalon.maya.pipeline`
"""
pass
def parse_container(container):
"""Return the container node's full container data.
Args:
container (str): A container node name.
Returns:
dict: The container schema data for this container node.
"""
pass
def launch_workfiles_app(*args):
workdir = os.environ["AVALON_WORKDIR"]
workfiles.show(workdir)
def publish(parent):
"""Shorthand to publish from within host"""
from avalon.tools import publish
return publish.show(parent)

View file

@ -0,0 +1,75 @@
from avalon import api
# from pype.hosts.resolve import lib as drlib
from avalon.vendor import qargparse
def get_reference_node_parents(ref):
"""Return all parent reference nodes of reference node
Args:
ref (str): reference node.
Returns:
list: The upstream parent reference nodes.
"""
parents = []
return parents
class SequenceLoader(api.Loader):
"""A basic SequenceLoader for Resolve
This will implement the basic behavior for a loader to inherit from that
will containerize the reference and will implement the `remove` and
`update` logic.
"""
options = [
qargparse.Toggle(
"handles",
label="Include handles",
default=0,
help="Load with handles or without?"
),
qargparse.Choice(
"load_to",
label="Where to load clips",
items=[
"Current timeline",
"New timeline"
],
default=0,
help="Where do you want clips to be loaded?"
),
qargparse.Choice(
"load_how",
label="How to load clips",
items=[
"original timing",
"sequential in order"
],
default=0,
help="Would you like to place it at orignal timing?"
)
]
def load(
self,
context,
name=None,
namespace=None,
options=None
):
pass
def update(self, container, representation):
"""Update an existing `container`
"""
pass
def remove(self, container):
"""Remove an existing `container`
"""
pass

View file

@ -0,0 +1,31 @@
#!/usr/bin/env python
import time
from pype.hosts.resolve.utils import get_resolve_module
from pypeapp import Logger
log = Logger().get_logger(__name__, "resolve")
wait_delay = 2.5
wait = 0.00
ready = None
while True:
try:
# Create project and set parameters:
resolve = get_resolve_module()
pm = resolve.GetProjectManager()
if pm:
ready = None
else:
ready = True
except AttributeError:
pass
if ready is None:
time.sleep(wait_delay)
log.info(f"Waiting {wait}s for Resolve to have opened Project Manager")
wait += wait_delay
else:
print(f"Preloaded variables: \n\n\tResolve module: "
f"`resolve` > {type(resolve)} \n\tProject manager: "
f"`pm` > {type(pm)}")
break

View file

@ -0,0 +1,26 @@
import os
import sys
import avalon.api as avalon
import pype
from pypeapp import Logger
log = Logger().get_logger(__name__)
def main(env):
import pype.hosts.resolve as bmdvr
# Registers pype's Global pyblish plugins
pype.install()
# activate resolve from pype
avalon.install(bmdvr)
log.info(f"Avalon registred hosts: {avalon.registered_host()}")
bmdvr.launch_pype_menu()
if __name__ == "__main__":
result = main(os.environ)
sys.exit(not bool(result))

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,65 @@
#! python3
# -*- coding: utf-8 -*-
# convert clip def
def convert_clip(timeline=None):
"""Convert timeline item (clip) into compound clip pype container
Args:
timeline (MediaPool.Timeline): Object of timeline
Returns:
bool: `True` if success
Raises:
Exception: description
"""
pass
# decorator function create_current_timeline_media_bin()
def create_current_timeline_media_bin(timeline=None):
"""Convert timeline item (clip) into compound clip pype container
Args:
timeline (MediaPool.Timeline): Object of timeline
Returns:
bool: `True` if success
Raises:
Exception: description
"""
pass
# decorator function get_selected_track_items()
def get_selected_track_items():
"""Convert timeline item (clip) into compound clip pype container
Args:
timeline (MediaPool.Timeline): Object of timeline
Returns:
bool: `True` if success
Raises:
Exception: description
"""
print("testText")
# PypeCompoundClip() class
class PypeCompoundClip(object):
"""docstring for ."""
def __init__(self, arg):
super(self).__init__()
self.arg = arg
def create_compound_clip(self):
pass

View file

@ -0,0 +1,57 @@
import os
import sys
import pype
import importlib
import pyblish.api
import pyblish.util
import avalon.api
from avalon.tools import publish
from pypeapp import Logger
log = Logger().get_logger(__name__)
def main(env):
# Registers pype's Global pyblish plugins
pype.install()
# Register Host (and it's pyblish plugins)
host_name = env["AVALON_APP"]
# TODO not sure if use "pype." or "avalon." for host import
host_import_str = f"pype.{host_name}"
try:
host_module = importlib.import_module(host_import_str)
except ModuleNotFoundError:
log.error((
f"Host \"{host_name}\" can't be imported."
f" Import string \"{host_import_str}\" failed."
))
return False
avalon.api.install(host_module)
# Register additional paths
addition_paths_str = env.get("PUBLISH_PATHS") or ""
addition_paths = addition_paths_str.split(os.pathsep)
for path in addition_paths:
path = os.path.normpath(path)
if not os.path.exists(path):
continue
pyblish.api.register_plugin_path(path)
# Register project specific plugins
project_name = os.environ["AVALON_PROJECT"]
project_plugins_paths = env.get("PYPE_PROJECT_PLUGINS") or ""
for path in project_plugins_paths.split(os.pathsep):
plugin_path = os.path.join(path, project_name, "plugins")
if os.path.exists(plugin_path):
pyblish.api.register_plugin_path(plugin_path)
return publish.show()
if __name__ == "__main__":
result = main(os.environ)
sys.exit(not bool(result))

View file

@ -0,0 +1,35 @@
#! python3
# -*- coding: utf-8 -*-
import os
from pypeapp import execute, Logger
from pype.hosts.resolve.utils import get_resolve_module
log = Logger().get_logger("Resolve")
CURRENT_DIR = os.getenv("RESOLVE_UTILITY_SCRIPTS_DIR", "")
python_dir = os.getenv("PYTHON36_RESOLVE")
python_exe = os.path.normpath(
os.path.join(python_dir, "python.exe")
)
resolve = get_resolve_module()
PM = resolve.GetProjectManager()
P = PM.GetCurrentProject()
log.info(P.GetName())
# ______________________________________________________
# testing subprocessing Scripts
testing_py = os.path.join(CURRENT_DIR, "ResolvePageSwitcher.py")
testing_py = os.path.normpath(testing_py)
log.info(f"Testing path to script: `{testing_py}`")
returncode = execute(
[python_exe, os.path.normpath(testing_py)],
env=dict(os.environ)
)
# Check if output file exists
if returncode != 0:
log.error("Executing failed!")

136
pype/hosts/resolve/utils.py Normal file
View file

@ -0,0 +1,136 @@
#! python3
"""
Resolve's tools for setting environment
"""
import sys
import os
import shutil
from pypeapp import Logger
log = Logger().get_logger(__name__, "resolve")
self = sys.modules[__name__]
self.bmd = None
def get_resolve_module():
# dont run if already loaded
if self.bmd:
return self.bmd
try:
"""
The PYTHONPATH needs to be set correctly for this import
statement to work. An alternative is to import the
DaVinciResolveScript by specifying absolute path
(see ExceptionHandler logic)
"""
import DaVinciResolveScript as bmd
except ImportError:
if sys.platform.startswith("darwin"):
expected_path = ("/Library/Application Support/Blackmagic Design"
"/DaVinci Resolve/Developer/Scripting/Modules")
elif sys.platform.startswith("win") \
or sys.platform.startswith("cygwin"):
expected_path = os.path.normpath(
os.getenv('PROGRAMDATA') + (
"/Blackmagic Design/DaVinci Resolve/Support/Developer"
"/Scripting/Modules"
)
)
elif sys.platform.startswith("linux"):
expected_path = "/opt/resolve/libs/Fusion/Modules"
# check if the default path has it...
print(("Unable to find module DaVinciResolveScript from "
"$PYTHONPATH - trying default locations"))
module_path = os.path.normpath(
os.path.join(
expected_path,
"DaVinciResolveScript.py"
)
)
try:
import imp
bmd = imp.load_source('DaVinciResolveScript', module_path)
except ImportError:
# No fallbacks ... report error:
log.error(
("Unable to find module DaVinciResolveScript - please "
"ensure that the module DaVinciResolveScript is "
"discoverable by python")
)
log.error(
("For a default DaVinci Resolve installation, the "
f"module is expected to be located in: {expected_path}")
)
sys.exit()
# assign global var and return
self.bmd = bmd.scriptapp("Resolve")
return self.bmd
def _sync_utility_scripts(env=None):
""" Synchronizing basic utlility scripts for resolve.
To be able to run scripts from inside `Resolve/Workspace/Scripts` menu
all scripts has to be accessible from defined folder.
"""
if not env:
env = os.environ
# initiate inputs
scripts = {}
us_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR")
us_dir = env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "")
us_paths = [os.path.join(
os.path.dirname(__file__),
"utility_scripts"
)]
# collect script dirs
if us_env:
log.info(f"Utility Scripts Env: `{us_env}`")
us_paths = us_env.split(
os.pathsep) + us_paths
# collect scripts from dirs
for path in us_paths:
scripts.update({path: os.listdir(path)})
log.info(f"Utility Scripts Dir: `{us_paths}`")
log.info(f"Utility Scripts: `{scripts}`")
# make sure no script file is in folder
if next((s for s in os.listdir(us_dir)), None):
for s in os.listdir(us_dir):
path = os.path.join(us_dir, s)
log.info(f"Removing `{path}`...")
os.remove(path)
# copy scripts into Resolve's utility scripts dir
for d, sl in scripts.items():
# directory and scripts list
for s in sl:
# script in script list
src = os.path.join(d, s)
dst = os.path.join(us_dir, s)
log.info(f"Copying `{src}` to `{dst}`...")
shutil.copy2(src, dst)
def setup(env=None):
""" Wrapper installer started from pype.hooks.resolve.ResolvePrelaunch()
"""
if not env:
env = os.environ
# synchronize resolve utility scripts
_sync_utility_scripts(env)
log.info("Resolve Pype wrapper has been installed")

View file

@ -0,0 +1,92 @@
"""Host API required Work Files tool"""
import os
from pypeapp import Logger
from .lib import (
get_project_manager,
set_project_manager_to_folder_name
)
log = Logger().get_logger(__name__, "resolve")
exported_projet_ext = ".drp"
def file_extensions():
return [exported_projet_ext]
def has_unsaved_changes():
get_project_manager().SaveProject()
return False
def save_file(filepath):
pm = get_project_manager()
file = os.path.basename(filepath)
fname, _ = os.path.splitext(file)
project = pm.GetCurrentProject()
name = project.GetName()
if "Untitled Project" not in name:
log.info("Saving project: `{}` as '{}'".format(name, file))
pm.ExportProject(name, filepath)
else:
log.info("Creating new project...")
pm.CreateProject(fname)
pm.ExportProject(name, filepath)
def open_file(filepath):
"""
Loading project
"""
pm = get_project_manager()
file = os.path.basename(filepath)
fname, _ = os.path.splitext(file)
dname, _ = fname.split("_v")
# deal with current project
project = pm.GetCurrentProject()
log.info(f"Test `pm`: {pm}")
pm.SaveProject()
try:
log.info(f"Test `dname`: {dname}")
if not set_project_manager_to_folder_name(dname):
raise
# load project from input path
project = pm.LoadProject(fname)
log.info(f"Project {project.GetName()} opened...")
return True
except AttributeError:
log.warning((f"Project with name `{fname}` does not exist! It will "
f"be imported from {filepath} and then loaded..."))
if pm.ImportProject(filepath):
# load project from input path
project = pm.LoadProject(fname)
log.info(f"Project imported/loaded {project.GetName()}...")
return True
else:
return False
def current_file():
pm = get_project_manager()
current_dir = os.getenv("AVALON_WORKDIR")
project = pm.GetCurrentProject()
name = project.GetName()
fname = name + exported_projet_ext
current_file = os.path.join(current_dir, fname)
normalised = os.path.normpath(current_file)
# Unsaved current file
if normalised == "":
return None
return normalised
def work_root(session):
return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/")

View file

@ -1,7 +1,7 @@
import os
import threading
from pype.api import Logger
from pypeapp import style
from avalon import style
from Qt import QtWidgets
from . import ClockifySettings, ClockifyAPI, MessageWidget

View file

@ -1,5 +1,5 @@
from Qt import QtCore, QtGui, QtWidgets
from pypeapp import style
from avalon import style
class MessageWidget(QtWidgets.QWidget):

View file

@ -1,6 +1,6 @@
import os
from Qt import QtCore, QtGui, QtWidgets
from pypeapp import style
from avalon import style
class ClockifySettings(QtWidgets.QWidget):

View file

@ -1,7 +1,7 @@
import os
import toml
import time
from pype.modules.ftrack import AppAction
from pype.modules.ftrack.lib import AppAction
from avalon import lib
from pype.api import Logger
from pype.lib import get_all_avalon_projects
@ -72,7 +72,7 @@ def register(session, plugins_presets={}):
for app in apps:
try:
registerApp(app, session, plugins_presets)
if app_counter%5 == 0:
if app_counter % 5 == 0:
time.sleep(0.1)
app_counter += 1
except Exception as exc:

View file

@ -1,284 +0,0 @@
import os
import ftrack_api
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib.io_nonsingleton import DbConnector
class AttributesRemapper(BaseAction):
'''Edit meta data action.'''
ignore_me = True
#: Action identifier.
identifier = 'attributes.remapper'
#: Action label.
label = "Pype Doctor"
variant = '- Attributes Remapper'
#: Action description.
description = 'Remaps attributes in avalon DB'
#: roles that are allowed to register this action
role_list = ["Pypeclub", "Administrator"]
icon = '{}/ftrack/action_icons/PypeDoctor.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
db_con = DbConnector()
keys_to_change = {
"fstart": "frameStart",
"startFrame": "frameStart",
"edit_in": "frameStart",
"fend": "frameEnd",
"endFrame": "frameEnd",
"edit_out": "frameEnd",
"handle_start": "handleStart",
"handle_end": "handleEnd",
"handles": ["handleEnd", "handleStart"],
"frameRate": "fps",
"framerate": "fps",
"resolution_width": "resolutionWidth",
"resolution_height": "resolutionHeight",
"pixel_aspect": "pixelAspect"
}
def discover(self, session, entities, event):
''' Validation '''
return True
def interface(self, session, entities, event):
if event['data'].get('values', {}):
return
title = 'Select Projects where attributes should be remapped'
items = []
selection_enum = {
'label': 'Process type',
'type': 'enumerator',
'name': 'process_type',
'data': [
{
'label': 'Selection',
'value': 'selection'
}, {
'label': 'Inverted selection',
'value': 'except'
}
],
'value': 'selection'
}
selection_label = {
'type': 'label',
'value': (
'Selection based variants:<br/>'
'- `Selection` - '
'NOTHING is processed when nothing is selected<br/>'
'- `Inverted selection` - '
'ALL Projects are processed when nothing is selected'
)
}
items.append(selection_enum)
items.append(selection_label)
item_splitter = {'type': 'label', 'value': '---'}
all_projects = session.query('Project').all()
for project in all_projects:
item_label = {
'type': 'label',
'value': '{} (<i>{}</i>)'.format(
project['full_name'], project['name']
)
}
item = {
'name': project['id'],
'type': 'boolean',
'value': False
}
if len(items) > 0:
items.append(item_splitter)
items.append(item_label)
items.append(item)
if len(items) == 0:
return {
'success': False,
'message': 'Didn\'t found any projects'
}
else:
return {
'items': items,
'title': title
}
def launch(self, session, entities, event):
if 'values' not in event['data']:
return
values = event['data']['values']
process_type = values.pop('process_type')
selection = True
if process_type == 'except':
selection = False
interface_messages = {}
projects_to_update = []
for project_id, update_bool in values.items():
if not update_bool and selection:
continue
if update_bool and not selection:
continue
project = session.query(
'Project where id is "{}"'.format(project_id)
).one()
projects_to_update.append(project)
if not projects_to_update:
self.log.debug('Nothing to update')
return {
'success': True,
'message': 'Nothing to update'
}
self.db_con.install()
relevant_types = ["project", "asset", "version"]
for ft_project in projects_to_update:
self.log.debug(
"Processing project \"{}\"".format(ft_project["full_name"])
)
self.db_con.Session["AVALON_PROJECT"] = ft_project["full_name"]
project = self.db_con.find_one({'type': 'project'})
if not project:
key = "Projects not synchronized to db"
if key not in interface_messages:
interface_messages[key] = []
interface_messages[key].append(ft_project["full_name"])
continue
# Get all entities in project collection from MongoDB
_entities = self.db_con.find({})
for _entity in _entities:
ent_t = _entity.get("type", "*unknown type")
name = _entity.get("name", "*unknown name")
self.log.debug(
"- {} ({})".format(name, ent_t)
)
# Skip types that do not store keys to change
if ent_t.lower() not in relevant_types:
self.log.debug("-- skipping - type is not relevant")
continue
# Get data which will change
updating_data = {}
source_data = _entity["data"]
for key_from, key_to in self.keys_to_change.items():
# continue if final key already exists
if type(key_to) == list:
for key in key_to:
# continue if final key was set in update_data
if key in updating_data:
continue
# continue if source key not exist or value is None
value = source_data.get(key_from)
if value is None:
continue
self.log.debug(
"-- changing key {} to {}".format(
key_from,
key
)
)
updating_data[key] = value
else:
if key_to in source_data:
continue
# continue if final key was set in update_data
if key_to in updating_data:
continue
# continue if source key not exist or value is None
value = source_data.get(key_from)
if value is None:
continue
self.log.debug(
"-- changing key {} to {}".format(key_from, key_to)
)
updating_data[key_to] = value
# Pop out old keys from entity
is_obsolete = False
for key in self.keys_to_change:
if key not in source_data:
continue
is_obsolete = True
source_data.pop(key)
# continue if there is nothing to change
if not is_obsolete and not updating_data:
self.log.debug("-- nothing to change")
continue
source_data.update(updating_data)
self.db_con.update_many(
{"_id": _entity["_id"]},
{"$set": {"data": source_data}}
)
self.db_con.uninstall()
if interface_messages:
self.show_interface_from_dict(
messages=interface_messages,
title="Errors during remapping attributes",
event=event
)
return True
def show_interface_from_dict(self, event, messages, title=""):
items = []
for key, value in messages.items():
if not value:
continue
subtitle = {'type': 'label', 'value': '# {}'.format(key)}
items.append(subtitle)
if isinstance(value, list):
for item in value:
message = {
'type': 'label', 'value': '<p>{}</p>'.format(item)
}
items.append(message)
else:
message = {'type': 'label', 'value': '<p>{}</p>'.format(value)}
items.append(message)
self.show_interface(items=items, title=title, event=event)
def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
AttributesRemapper(session, plugins_presets).register()

View file

@ -1,7 +1,6 @@
import os
import collections
import ftrack_api
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
from pype.modules.ftrack.lib.avalon_sync import get_avalon_attr
@ -11,9 +10,7 @@ class CleanHierarchicalAttrsAction(BaseAction):
variant = "- Clean hierarchical custom attributes"
description = "Unset empty hierarchical attribute values."
role_list = ["Pypeclub", "Administrator", "Project Manager"]
icon = "{}/ftrack/action_icons/PypeAdmin.svg".format(
os.environ.get("PYPE_STATICS_SERVER", "")
)
icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg")
all_project_entities_query = (
"select id, name, parent_id, link"

View file

@ -1,4 +1,4 @@
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction
try:
from functools import cmp_to_key
except Exception:

View file

@ -1,10 +1,7 @@
import os
import sys
import argparse
import logging
import subprocess
import ftrack_api
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
class ComponentOpen(BaseAction):
@ -15,9 +12,7 @@ class ComponentOpen(BaseAction):
# Action label
label = 'Open File'
# Action icon
icon = '{}/ftrack/action_icons/ComponentOpen.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
icon = statics_icon("ftrack", "action_icons", "ComponentOpen.svg")
def discover(self, session, entities, event):
''' Validation '''
@ -69,42 +64,3 @@ def register(session, plugins_presets={}):
'''Register action. Called when used as an event plugin.'''
ComponentOpen(session, plugins_presets).register()
def main(arguments=None):
'''Set up logging and register action.'''
if arguments is None:
arguments = []
parser = argparse.ArgumentParser()
# Allow setting of logging level from arguments.
loggingLevels = {}
for level in (
logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
logging.ERROR, logging.CRITICAL
):
loggingLevels[logging.getLevelName(level).lower()] = level
parser.add_argument(
'-v', '--verbosity',
help='Set the logging output verbosity.',
choices=loggingLevels.keys(),
default='info'
)
namespace = parser.parse_args(arguments)
# Set up basic logging
logging.basicConfig(level=loggingLevels[namespace.verbosity])
session = ftrack_api.Session()
register(session)
# Wait for events
logging.info(
'Registered actions and listening for events. Use Ctrl-C to abort.'
)
session.event_hub.wait()
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))

View file

@ -1,9 +1,8 @@
import os
import collections
import json
import arrow
import ftrack_api
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
from pype.modules.ftrack.lib.avalon_sync import CustAttrIdKey
from pype.api import config
@ -114,9 +113,7 @@ class CustomAttributes(BaseAction):
description = 'Creates Avalon/Mongo ID for double check'
#: roles that are allowed to register this action
role_list = ['Pypeclub', 'Administrator']
icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg")
required_keys = ['key', 'label', 'type']
type_posibilities = [

View file

@ -1,5 +1,5 @@
import os
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
from avalon import lib as avalonlib
from pype.api import config, Anatomy
@ -7,9 +7,7 @@ from pype.api import config, Anatomy
class CreateFolders(BaseAction):
identifier = "create.folders"
label = "Create Folders"
icon = "{}/ftrack/action_icons/CreateFolders.svg".format(
os.environ.get("PYPE_STATICS_SERVER", "")
)
icon = statics_icon("ftrack", "action_icons", "CreateFolders.svg")
def discover(self, session, entities, event):
if len(entities) != 1:

View file

@ -1,7 +1,7 @@
import os
import re
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
from pype.api import config, Anatomy
@ -52,9 +52,7 @@ class CreateProjectFolders(BaseAction):
label = "Create Project Structure"
description = "Creates folder structure"
role_list = ["Pypeclub", "Administrator", "Project Manager"]
icon = "{}/ftrack/action_icons/CreateProjectFolders.svg".format(
os.environ.get("PYPE_STATICS_SERVER", "")
)
icon = statics_icon("ftrack", "action_icons", "CreateProjectFolders.svg")
pattern_array = re.compile(r"\[.*\]")
pattern_ftrack = re.compile(r".*\[[.]*ftrack[.]*")

View file

@ -1,336 +0,0 @@
import os
import sys
import json
import argparse
import logging
import ftrack_api
from pype.modules.ftrack import BaseAction
class CustomAttributeDoctor(BaseAction):
ignore_me = True
#: Action identifier.
identifier = 'custom.attributes.doctor'
#: Action label.
label = "Pype Doctor"
variant = '- Custom Attributes Doctor'
#: Action description.
description = (
'Fix hierarchical custom attributes mainly handles, fstart'
' and fend'
)
icon = '{}/ftrack/action_icons/PypeDoctor.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
hierarchical_ca = ['handleStart', 'handleEnd', 'frameStart', 'frameEnd']
hierarchical_alternatives = {
'handleStart': 'handles',
'handleEnd': 'handles',
"frameStart": "fstart",
"frameEnd": "fend"
}
# Roles for new custom attributes
read_roles = ['ALL',]
write_roles = ['ALL',]
data_ca = {
'handleStart': {
'label': 'Frame handles start',
'type': 'number',
'config': json.dumps({'isdecimal': False})
},
'handleEnd': {
'label': 'Frame handles end',
'type': 'number',
'config': json.dumps({'isdecimal': False})
},
'frameStart': {
'label': 'Frame start',
'type': 'number',
'config': json.dumps({'isdecimal': False})
},
'frameEnd': {
'label': 'Frame end',
'type': 'number',
'config': json.dumps({'isdecimal': False})
}
}
def discover(self, session, entities, event):
''' Validation '''
return True
def interface(self, session, entities, event):
if event['data'].get('values', {}):
return
title = 'Select Project to fix Custom attributes'
items = []
item_splitter = {'type': 'label', 'value': '---'}
all_projects = session.query('Project').all()
for project in all_projects:
item_label = {
'type': 'label',
'value': '{} (<i>{}</i>)'.format(
project['full_name'], project['name']
)
}
item = {
'name': project['id'],
'type': 'boolean',
'value': False
}
if len(items) > 0:
items.append(item_splitter)
items.append(item_label)
items.append(item)
if len(items) == 0:
return {
'success': False,
'message': 'Didn\'t found any projects'
}
else:
return {
'items': items,
'title': title
}
def launch(self, session, entities, event):
if 'values' not in event['data']:
return
values = event['data']['values']
projects_to_update = []
for project_id, update_bool in values.items():
if not update_bool:
continue
project = session.query(
'Project where id is "{}"'.format(project_id)
).one()
projects_to_update.append(project)
if not projects_to_update:
self.log.debug('Nothing to update')
return {
'success': True,
'message': 'Nothing to update'
}
self.security_roles = {}
self.to_process = {}
# self.curent_default_values = {}
existing_attrs = session.query('CustomAttributeConfiguration').all()
self.prepare_custom_attributes(existing_attrs)
self.projects_data = {}
for project in projects_to_update:
self.process_data(project)
return True
def process_data(self, entity):
cust_attrs = entity.get('custom_attributes')
if not cust_attrs:
return
for dst_key, src_key in self.to_process.items():
if src_key in cust_attrs:
value = cust_attrs[src_key]
entity['custom_attributes'][dst_key] = value
self.session.commit()
for child in entity.get('children', []):
self.process_data(child)
def prepare_custom_attributes(self, existing_attrs):
to_process = {}
to_create = []
all_keys = {attr['key']: attr for attr in existing_attrs}
for key in self.hierarchical_ca:
if key not in all_keys:
self.log.debug(
'Custom attribute "{}" does not exist at all'.format(key)
)
to_create.append(key)
if key in self.hierarchical_alternatives:
alt_key = self.hierarchical_alternatives[key]
if alt_key in all_keys:
self.log.debug((
'Custom attribute "{}" will use values from "{}"'
).format(key, alt_key))
to_process[key] = alt_key
obj = all_keys[alt_key]
# if alt_key not in self.curent_default_values:
# self.curent_default_values[alt_key] = obj['default']
obj['default'] = None
self.session.commit()
else:
obj = all_keys[key]
new_key = key + '_old'
if obj['is_hierarchical']:
if new_key not in all_keys:
self.log.info((
'Custom attribute "{}" is already hierarchical'
' and can\'t find old one'
).format(key)
)
continue
to_process[key] = new_key
continue
# default_value = obj['default']
# if new_key not in self.curent_default_values:
# self.curent_default_values[new_key] = default_value
obj['key'] = new_key
obj['label'] = obj['label'] + '(old)'
obj['default'] = None
self.session.commit()
to_create.append(key)
to_process[key] = new_key
self.to_process = to_process
for key in to_create:
data = {
'key': key,
'entity_type': 'show',
'is_hierarchical': True,
'default': None
}
for _key, _value in self.data_ca.get(key, {}).items():
if _key == 'type':
_value = self.session.query((
'CustomAttributeType where name is "{}"'
).format(_value)).first()
data[_key] = _value
avalon_group = self.session.query(
'CustomAttributeGroup where name is "avalon"'
).first()
if avalon_group:
data['group'] = avalon_group
read_roles = self.get_security_role(self.read_roles)
write_roles = self.get_security_role(self.write_roles)
data['read_security_roles'] = read_roles
data['write_security_roles'] = write_roles
self.session.create('CustomAttributeConfiguration', data)
self.session.commit()
# def return_back_defaults(self):
# existing_attrs = self.session.query(
# 'CustomAttributeConfiguration'
# ).all()
#
# for attr_key, default in self.curent_default_values.items():
# for attr in existing_attrs:
# if attr['key'] != attr_key:
# continue
# attr['default'] = default
# self.session.commit()
# break
def get_security_role(self, security_roles):
roles = []
if len(security_roles) == 0 or security_roles[0] == 'ALL':
roles = self.get_role_ALL()
elif security_roles[0] == 'except':
excepts = security_roles[1:]
all = self.get_role_ALL()
for role in all:
if role['name'] not in excepts:
roles.append(role)
if role['name'] not in self.security_roles:
self.security_roles[role['name']] = role
else:
for role_name in security_roles:
if role_name in self.security_roles:
roles.append(self.security_roles[role_name])
continue
try:
query = 'SecurityRole where name is "{}"'.format(role_name)
role = self.session.query(query).one()
self.security_roles[role_name] = role
roles.append(role)
except Exception:
self.log.warning(
'Securit role "{}" does not exist'.format(role_name)
)
continue
return roles
def get_role_ALL(self):
role_name = 'ALL'
if role_name in self.security_roles:
all_roles = self.security_roles[role_name]
else:
all_roles = self.session.query('SecurityRole').all()
self.security_roles[role_name] = all_roles
for role in all_roles:
if role['name'] not in self.security_roles:
self.security_roles[role['name']] = role
return all_roles
def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
CustomAttributeDoctor(session, plugins_presets).register()
def main(arguments=None):
'''Set up logging and register action.'''
if arguments is None:
arguments = []
parser = argparse.ArgumentParser()
# Allow setting of logging level from arguments.
loggingLevels = {}
for level in (
logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
logging.ERROR, logging.CRITICAL
):
loggingLevels[logging.getLevelName(level).lower()] = level
parser.add_argument(
'-v', '--verbosity',
help='Set the logging output verbosity.',
choices=loggingLevels.keys(),
default='info'
)
namespace = parser.parse_args(arguments)
# Set up basic logging
logging.basicConfig(level=loggingLevels[namespace.verbosity])
session = ftrack_api.Session()
register(session)
# Wait for events
logging.info(
'Registered actions and listening for events. Use Ctrl-C to abort.'
)
session.event_hub.wait()
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))

View file

@ -1,11 +1,10 @@
import os
import collections
import uuid
from datetime import datetime
from queue import Queue
from bson.objectid import ObjectId
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
from pype.modules.ftrack.lib.io_nonsingleton import DbConnector
@ -18,9 +17,7 @@ class DeleteAssetSubset(BaseAction):
label = "Delete Asset/Subsets"
#: Action description.
description = "Removes from Avalon with all childs and asset from Ftrack"
icon = "{}/ftrack/action_icons/DeleteAsset.svg".format(
os.environ.get("PYPE_STATICS_SERVER", "")
)
icon = statics_icon("ftrack", "action_icons", "DeleteAsset.svg")
#: roles that are allowed to register this action
role_list = ["Pypeclub", "Administrator", "Project Manager"]
#: Db connection

View file

@ -5,7 +5,7 @@ import uuid
import clique
from pymongo import UpdateOne
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
from pype.modules.ftrack.lib.io_nonsingleton import DbConnector
from pype.api import Anatomy
@ -22,9 +22,7 @@ class DeleteOldVersions(BaseAction):
" archived with only lates versions."
)
role_list = ["Pypeclub", "Project Manager", "Administrator"]
icon = "{}/ftrack/action_icons/PypeAdmin.svg".format(
os.environ.get("PYPE_STATICS_SERVER", "")
)
icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg")
dbcon = DbConnector()

View file

@ -8,11 +8,11 @@ from bson.objectid import ObjectId
from avalon import pipeline
from avalon.vendor import filelink
from avalon.tools.libraryloader.io_nonsingleton import DbConnector
from pype.api import Anatomy
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
from pype.modules.ftrack.lib.avalon_sync import CustAttrIdKey
from pype.modules.ftrack.lib.io_nonsingleton import DbConnector
class Delivery(BaseAction):
@ -21,9 +21,7 @@ class Delivery(BaseAction):
label = "Delivery"
description = "Deliver data to client"
role_list = ["Pypeclub", "Administrator", "Project manager"]
icon = "{}/ftrack/action_icons/Delivery.svg".format(
os.environ.get("PYPE_STATICS_SERVER", "")
)
icon = statics_icon("ftrack", "action_icons", "Delivery.svg")
db_con = DbConnector()
@ -508,6 +506,7 @@ class Delivery(BaseAction):
"message": "Delivery Finished"
}
def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''

View file

@ -1,11 +1,10 @@
import os
import sys
import json
import logging
import subprocess
from operator import itemgetter
import ftrack_api
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
from pype.api import Logger, config
log = Logger().get_logger(__name__)
@ -16,9 +15,8 @@ class DJVViewAction(BaseAction):
identifier = "djvview-launch-action"
label = "DJV View"
description = "DJV View Launcher"
icon = '{}/app_icons/djvView.png'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
icon = statics_icon("app_icons", "djvView.png")
type = 'Application'
def __init__(self, session, plugins_presets):

View file

@ -1,11 +1,5 @@
import os
import sys
import argparse
import logging
import json
import ftrack_api
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
class JobKiller(BaseAction):
@ -20,9 +14,7 @@ class JobKiller(BaseAction):
description = 'Killing selected running jobs'
#: roles that are allowed to register this action
role_list = ['Pypeclub', 'Administrator']
icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg")
def discover(self, session, entities, event):
''' Validation '''
@ -124,43 +116,3 @@ def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
JobKiller(session, plugins_presets).register()
def main(arguments=None):
'''Set up logging and register action.'''
if arguments is None:
arguments = []
parser = argparse.ArgumentParser()
# Allow setting of logging level from arguments.
loggingLevels = {}
for level in (
logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
logging.ERROR, logging.CRITICAL
):
loggingLevels[logging.getLevelName(level).lower()] = level
parser.add_argument(
'-v', '--verbosity',
help='Set the logging output verbosity.',
choices=loggingLevels.keys(),
default='info'
)
namespace = parser.parse_args(arguments)
# Set up basic logging
logging.basicConfig(level=loggingLevels[namespace.verbosity])
session = ftrack_api.Session()
register(session)
# Wait for events
logging.info(
'Registered actions and listening for events. Use Ctrl-C to abort.'
)
session.event_hub.wait()
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))

View file

@ -1,10 +1,4 @@
import os
import sys
import argparse
import logging
import ftrack_api
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
class MultipleNotes(BaseAction):
@ -16,9 +10,7 @@ class MultipleNotes(BaseAction):
label = 'Multiple Notes'
#: Action description.
description = 'Add same note to multiple Asset Versions'
icon = '{}/ftrack/action_icons/MultipleNotes.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
icon = statics_icon("ftrack", "action_icons", "MultipleNotes.svg")
def discover(self, session, entities, event):
''' Validation '''
@ -116,42 +108,3 @@ def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
MultipleNotes(session, plugins_presets).register()
def main(arguments=None):
'''Set up logging and register action.'''
if arguments is None:
arguments = []
parser = argparse.ArgumentParser()
# Allow setting of logging level from arguments.
loggingLevels = {}
for level in (
logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
logging.ERROR, logging.CRITICAL
):
loggingLevels[logging.getLevelName(level).lower()] = level
parser.add_argument(
'-v', '--verbosity',
help='Set the logging output verbosity.',
choices=loggingLevels.keys(),
default='info'
)
namespace = parser.parse_args(arguments)
# Set up basic logging
logging.basicConfig(level=loggingLevels[namespace.verbosity])
session = ftrack_api.Session()
register(session)
# Wait for events
logging.info(
'Registered actions and listening for events. Use Ctrl-C to abort.'
)
session.event_hub.wait()
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))

View file

@ -1,7 +1,7 @@
import os
import json
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
from pype.api import config, Anatomy, project_overrides_dir_path
from pype.modules.ftrack.lib.avalon_sync import get_avalon_attr
@ -17,9 +17,7 @@ class PrepareProject(BaseAction):
description = 'Set basic attributes on the project'
#: roles that are allowed to register this action
role_list = ["Pypeclub", "Administrator", "Project manager"]
icon = '{}/ftrack/action_icons/PrepareProject.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
icon = statics_icon("ftrack", "action_icons", "PrepareProject.svg")
# Key to store info about trigerring create folder structure
create_project_structure_key = "create_folder_structure"

View file

@ -1,17 +1,13 @@
import os
import sys
import subprocess
import logging
import traceback
import json
from pype.api import Logger, config
from pype.modules.ftrack import BaseAction
from pype.api import config
from pype.modules.ftrack.lib import BaseAction, statics_icon
import ftrack_api
from avalon import io, api
log = Logger().get_logger(__name__)
class RVAction(BaseAction):
""" Launch RV action """
@ -19,9 +15,8 @@ class RVAction(BaseAction):
identifier = "rv.launch.action"
label = "rv"
description = "rv Launcher"
icon = '{}/ftrack/action_icons/RV.png'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
icon = statics_icon("ftrack", "action_icons", "RV.png")
type = 'Application'
def __init__(self, session, plugins_presets):
@ -144,7 +139,7 @@ class RVAction(BaseAction):
try:
items = self.get_interface_items(session, entities)
except Exception:
log.error(traceback.format_exc())
self.log.error(traceback.format_exc())
job["status"] = "failed"
else:
job["status"] = "done"
@ -238,7 +233,7 @@ class RVAction(BaseAction):
try:
paths = self.get_file_paths(session, event)
except Exception:
log.error(traceback.format_exc())
self.log.error(traceback.format_exc())
job["status"] = "failed"
else:
job["status"] = "done"
@ -254,7 +249,7 @@ class RVAction(BaseAction):
args.extend(paths)
log.info("Running rv: {}".format(args))
self.log.info("Running rv: {}".format(args))
subprocess.Popen(args)
@ -332,43 +327,3 @@ def register(session, plugins_presets={}):
"""Register hooks."""
RVAction(session, plugins_presets).register()
def main(arguments=None):
'''Set up logging and register action.'''
if arguments is None:
arguments = []
import argparse
parser = argparse.ArgumentParser()
# Allow setting of logging level from arguments.
loggingLevels = {}
for level in (
logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
logging.ERROR, logging.CRITICAL
):
loggingLevels[logging.getLevelName(level).lower()] = level
parser.add_argument(
'-v', '--verbosity',
help='Set the logging output verbosity.',
choices=loggingLevels.keys(),
default='info'
)
namespace = parser.parse_args(arguments)
# Set up basic logging
logging.basicConfig(level=loggingLevels[namespace.verbosity])
session = ftrack_api.Session()
register(session)
# Wait for events
logging.info(
'Registered actions and listening for events. Use Ctrl-C to abort.'
)
session.event_hub.wait()
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))

View file

@ -1,6 +1,6 @@
import os
from operator import itemgetter
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
class SeedDebugProject(BaseAction):
@ -16,9 +16,7 @@ class SeedDebugProject(BaseAction):
priority = 100
#: roles that are allowed to register this action
role_list = ["Pypeclub"]
icon = "{}/ftrack/action_icons/SeedProject.svg".format(
os.environ.get("PYPE_STATICS_SERVER", "")
)
icon = statics_icon("ftrack", "action_icons", "SeedProject.svg")
# Asset names which will be created in `Assets` entity
assets = [
@ -429,6 +427,7 @@ class SeedDebugProject(BaseAction):
self.session.commit()
return True
def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''

View file

@ -1,5 +1,4 @@
import ftrack_api
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction
class StartTimer(BaseAction):

View file

@ -4,7 +4,7 @@ import errno
import json
from bson.objectid import ObjectId
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
from pype.api import Anatomy
from pype.modules.ftrack.lib.io_nonsingleton import DbConnector
@ -22,10 +22,7 @@ class StoreThumbnailsToAvalon(BaseAction):
description = 'Test action'
# roles that are allowed to register this action
role_list = ["Pypeclub", "Administrator", "Project Manager"]
icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg")
thumbnail_key = "AVALON_THUMBNAIL_ROOT"
db_con = DbConnector()

View file

@ -1,8 +1,7 @@
import os
import time
import traceback
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
from pype.modules.ftrack.lib.avalon_sync import SyncEntitiesFactory
@ -43,9 +42,7 @@ class SyncToAvalonLocal(BaseAction):
priority = 200
#: roles that are allowed to register this action
role_list = ["Pypeclub"]
icon = '{}/ftrack/action_icons/PypeAdmin.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View file

@ -1,37 +1,19 @@
import os
import sys
import argparse
import logging
import collections
import json
import re
import ftrack_api
from avalon import io, inventory, schema
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
class TestAction(BaseAction):
'''Edit meta data action.'''
"""Action for testing purpose or as base for new actions."""
ignore_me = True
#: Action identifier.
identifier = 'test.action'
#: Action label.
label = 'Test action'
#: Action description.
description = 'Test action'
#: priority
priority = 10000
#: roles that are allowed to register this action
role_list = ['Pypeclub']
icon = '{}/ftrack/action_icons/TestAction.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
icon = statics_icon("ftrack", "action_icons", "TestAction.svg")
def discover(self, session, entities, event):
''' Validation '''
return True
def launch(self, session, entities, event):
@ -41,45 +23,4 @@ class TestAction(BaseAction):
def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
TestAction(session, plugins_presets).register()
def main(arguments=None):
'''Set up logging and register action.'''
if arguments is None:
arguments = []
parser = argparse.ArgumentParser()
# Allow setting of logging level from arguments.
loggingLevels = {}
for level in (
logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
logging.ERROR, logging.CRITICAL
):
loggingLevels[logging.getLevelName(level).lower()] = level
parser.add_argument(
'-v', '--verbosity',
help='Set the logging output verbosity.',
choices=loggingLevels.keys(),
default='info'
)
namespace = parser.parse_args(arguments)
# Set up basic logging
logging.basicConfig(level=loggingLevels[namespace.verbosity])
session = ftrack_api.Session()
register(session)
# Wait for events
logging.info(
'Registered actions and listening for events. Use Ctrl-C to abort.'
)
session.event_hub.wait()
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))

View file

@ -1,11 +1,5 @@
import os
import sys
import argparse
import logging
import json
import ftrack_api
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
class ThumbToChildren(BaseAction):
@ -18,9 +12,7 @@ class ThumbToChildren(BaseAction):
# Action variant
variant = " to Children"
# Action icon
icon = '{}/ftrack/action_icons/Thumbnail.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
icon = statics_icon("ftrack", "action_icons", "Thumbnail.svg")
def discover(self, session, entities, event):
''' Validation '''
@ -71,42 +63,3 @@ def register(session, plugins_presets={}):
'''Register action. Called when used as an event plugin.'''
ThumbToChildren(session, plugins_presets).register()
def main(arguments=None):
'''Set up logging and register action.'''
if arguments is None:
arguments = []
parser = argparse.ArgumentParser()
# Allow setting of logging level from arguments.
loggingLevels = {}
for level in (
logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
logging.ERROR, logging.CRITICAL
):
loggingLevels[logging.getLevelName(level).lower()] = level
parser.add_argument(
'-v', '--verbosity',
help='Set the logging output verbosity.',
choices=loggingLevels.keys(),
default='info'
)
namespace = parser.parse_args(arguments)
# Set up basic logging
logging.basicConfig(level=loggingLevels[namespace.verbosity])
session = ftrack_api.Session()
register(session)
# Wait for events
logging.info(
'Registered actions and listening for events. Use Ctrl-C to abort.'
)
session.event_hub.wait()
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))

View file

@ -1,10 +1,5 @@
import os
import sys
import argparse
import logging
import json
import ftrack_api
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
class ThumbToParent(BaseAction):
@ -17,9 +12,7 @@ class ThumbToParent(BaseAction):
# Action variant
variant = " to Parent"
# Action icon
icon = '{}/ftrack/action_icons/Thumbnail.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
icon = statics_icon("ftrack", "action_icons", "Thumbnail.svg")
def discover(self, session, entities, event):
'''Return action config if triggered on asset versions.'''
@ -93,42 +86,3 @@ def register(session, plugins_presets={}):
'''Register action. Called when used as an event plugin.'''
ThumbToParent(session, plugins_presets).register()
def main(arguments=None):
'''Set up logging and register action.'''
if arguments is None:
arguments = []
parser = argparse.ArgumentParser()
# Allow setting of logging level from arguments.
loggingLevels = {}
for level in (
logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
logging.ERROR, logging.CRITICAL
):
loggingLevels[logging.getLevelName(level).lower()] = level
parser.add_argument(
'-v', '--verbosity',
help='Set the logging output verbosity.',
choices=loggingLevels.keys(),
default='info'
)
namespace = parser.parse_args(arguments)
# Set up basic logging
logging.basicConfig(level=loggingLevels[namespace.verbosity])
session = ftrack_api.Session()
register(session)
# Wait for events
logging.info(
'Registered actions and listening for events. Use Ctrl-C to abort.'
)
session.event_hub.wait()
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))

View file

@ -1,189 +0,0 @@
import os
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib.io_nonsingleton import DbConnector
class PypeUpdateFromV2_2_0(BaseAction):
"""This action is to remove silo field from database and changes asset
schema to newer version
WARNING: it is NOT for situations when you want to switch from avalon-core
to Pype's avalon-core!!!
"""
#: Action identifier.
identifier = "silos.doctor"
#: Action label.
label = "Pype Update"
variant = "- v2.2.0 to v2.3.0 or higher"
#: Action description.
description = "Use when Pype was updated from v2.2.0 to v2.3.0 or higher"
#: roles that are allowed to register this action
role_list = ["Pypeclub", "Administrator"]
icon = "{}/ftrack/action_icons/PypeUpdate.svg".format(
os.environ.get("PYPE_STATICS_SERVER", "")
)
# connector to MongoDB (Avalon mongo)
db_con = DbConnector()
def discover(self, session, entities, event):
""" Validation """
if len(entities) != 1:
return False
if entities[0].entity_type.lower() != "project":
return False
return True
def interface(self, session, entities, event):
if event['data'].get('values', {}):
return
items = []
item_splitter = {'type': 'label', 'value': '---'}
title = "Updated Pype from v 2.2.0 to v2.3.0 or higher"
items.append({
"type": "label",
"value": (
"NOTE: This doctor action should be used ONLY when Pype"
" was updated from v2.2.0 to v2.3.0 or higher.<br><br><br>"
)
})
items.append({
"type": "label",
"value": (
"Select if want to process <b>all synchronized projects</b>"
" or <b>selection</b>."
)
})
items.append({
"type": "enumerator",
"name": "__process_all__",
"data": [{
"label": "All synchronized projects",
"value": True
}, {
"label": "Selection",
"value": False
}],
"value": False
})
items.append({
"type": "label",
"value": (
"<br/><br/><h2>Synchronized projects:</h2>"
"<i>(ignore if <strong>\"ALL projects\"</strong> selected)</i>"
)
})
self.log.debug("Getting all Ftrack projects")
# Get all Ftrack projects
all_ftrack_projects = [
project["full_name"] for project in session.query("Project").all()
]
self.log.debug("Getting Avalon projects that are also in the Ftrack")
# Get Avalon projects that are in Ftrack
self.db_con.install()
possible_projects = [
project["name"] for project in self.db_con.projects()
if project["name"] in all_ftrack_projects
]
for project in possible_projects:
item_label = {
"type": "label",
"value": project
}
item = {
"label": "- process",
"name": project,
"type": 'boolean',
"value": False
}
items.append(item_splitter)
items.append(item_label)
items.append(item)
if len(possible_projects) == 0:
return {
"success": False,
"message": (
"Nothing to process."
" There are not projects synchronized to avalon."
)
}
else:
return {
"items": items,
"title": title
}
def launch(self, session, entities, event):
if 'values' not in event['data']:
return
projects_selection = {
True: [],
False: []
}
process_all = None
values = event['data']['values']
for key, value in values.items():
if key == "__process_all__":
process_all = value
continue
projects_selection[value].append(key)
# Skip if process_all value is not boolean
# - may happen when user delete string line in combobox
if not isinstance(process_all, bool):
self.log.warning(
"Nothing was processed. User didn't select if want to process"
" selection or all projects!"
)
return {
"success": False,
"message": (
"Nothing was processed. You must select if want to process"
" \"selection\" or \"all projects\"!"
)
}
projects_to_process = projects_selection[True]
if process_all:
projects_to_process.extend(projects_selection[False])
self.db_con.install()
for project in projects_to_process:
self.log.debug("Processing project \"{}\"".format(project))
self.db_con.Session["AVALON_PROJECT"] = project
self.log.debug("- Unsetting silos on assets")
self.db_con.update_many(
{"type": "asset"},
{"$unset": {"silo": ""}}
)
self.log.debug("- setting schema of assets to v.3")
self.db_con.update_many(
{"type": "asset"},
{"$set": {"schema": "avalon-core:asset-3.0"}}
)
return True
def register(session, plugins_presets={}):
"""Register plugin. Called when used as an plugin."""
PypeUpdateFromV2_2_0(session, plugins_presets).register()

View file

@ -1,23 +1,15 @@
import os
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction, statics_icon
class ActionAskWhereIRun(BaseAction):
""" Sometimes user forget where pipeline with his credentials is running.
- this action triggers `ActionShowWhereIRun`
"""
# Action is ignored by default
ignore_me = True
#: Action identifier.
identifier = 'ask.where.i.run'
#: Action label.
label = 'Ask where I run'
#: Action description.
description = 'Triggers PC info where user have running Pype'
#: Action icon
icon = '{}/ftrack/action_icons/ActionAskWhereIRun.svg'.format(
os.environ.get('PYPE_STATICS_SERVER', '')
)
icon = statics_icon("ftrack", "action_icons", "ActionAskWhereIRun.svg")
def discover(self, session, entities, event):
""" Hide by default - Should be enabled only if you want to run.

View file

@ -1,8 +1,7 @@
import platform
import socket
import getpass
import ftrack_api
from pype.modules.ftrack import BaseAction
from pype.modules.ftrack.lib import BaseAction
class ActionShowWhereIRun(BaseAction):

View file

@ -2,7 +2,7 @@ from . import avalon_sync
from . import credentials
from .ftrack_base_handler import BaseHandler
from .ftrack_event_handler import BaseEvent
from .ftrack_action_handler import BaseAction
from .ftrack_action_handler import BaseAction, statics_icon
from .ftrack_app_handler import AppAction
__all__ = [
@ -11,5 +11,6 @@ __all__ = [
"BaseHandler",
"BaseEvent",
"BaseAction",
"statics_icon",
"AppAction"
]

View file

@ -5,27 +5,20 @@ Copy of io module in avalon-core.
- In this case not working as singleton with api.Session!
"""
import os
import time
import errno
import shutil
import logging
import tempfile
import functools
import contextlib
import atexit
import requests
# Third-party dependencies
import pymongo
from pymongo.client_session import ClientSession
class NotActiveTable(Exception):
def __init__(self, *args, **kwargs):
msg = "Active table is not set. (This is bug)"
if not (args or kwargs):
args = (default_message,)
args = [msg]
super().__init__(*args, **kwargs)
@ -120,7 +113,7 @@ class DbConnector:
else:
raise IOError(
"ERROR: Couldn't connect to %s in "
"less than %.3f ms" % (self._mongo_url, timeout)
"less than %.3f ms" % (self._mongo_url, self.timeout)
)
self.log.info("Connected to %s, delay %.3f s" % (

View file

@ -1,6 +1,14 @@
import os
from .ftrack_base_handler import BaseHandler
def statics_icon(*icon_statics_file_parts):
statics_server = os.environ.get("PYPE_STATICS_SERVER")
if not statics_server:
return None
return "/".join((statics_server, *icon_statics_file_parts))
class BaseAction(BaseHandler):
'''Custom Action base class
@ -177,7 +185,9 @@ class BaseAction(BaseHandler):
else:
for key in ('success', 'message'):
if key not in result:
raise KeyError('Missing required key: {0}.'.format(key))
raise KeyError(
"Missing required key: {0}.".format(key)
)
return result
self.log.warning((

View file

@ -1,6 +1,6 @@
import os
import requests
from pypeapp import style
from avalon import style
from pype.modules.ftrack import credentials
from . import login_tools
from Qt import QtCore, QtGui, QtWidgets

View file

@ -1,6 +1,6 @@
from Qt import QtWidgets, QtCore
from .widgets import LogsWidget, LogDetailWidget
from pypeapp import style
from avalon import style
class LogsWindow(QtWidgets.QWidget):

View file

@ -1,5 +1,5 @@
import appdirs
from pypeapp import style
from avalon import style
from Qt import QtWidgets
import os
import json

View file

@ -1,6 +1,6 @@
import os
from Qt import QtCore, QtGui, QtWidgets
from pypeapp import style
from avalon import style
class MusterLogin(QtWidgets.QWidget):

View file

@ -1,7 +1,7 @@
import os
from . import QtCore, QtGui, QtWidgets
from . import get_resource
from pypeapp import style
from avalon import style
class ComponentItem(QtWidgets.QFrame):

View file

@ -1,5 +1,5 @@
from pype.api import Logger
from pypeapp import style
from avalon import style
from Qt import QtCore, QtGui, QtWidgets

View file

@ -1,6 +1,6 @@
import os
from Qt import QtCore, QtGui, QtWidgets
from pypeapp import style, resources
from pype.resources import get_resource
from avalon import style
class UserWidget(QtWidgets.QWidget):
@ -14,7 +14,7 @@ class UserWidget(QtWidgets.QWidget):
self.module = module
# Style
icon = QtGui.QIcon(resources.get_resource("icon.png"))
icon = QtGui.QIcon(get_resource("icon.png"))
self.setWindowIcon(icon)
self.setWindowTitle("Username Settings")
self.setMinimumWidth(self.MIN_WIDTH)

View file

@ -0,0 +1,52 @@
"""Create a layout asset."""
import bpy
from avalon import api
from avalon.blender import Creator, lib
import pype.hosts.blender.plugin
class CreateLayout(Creator):
"""Layout output for character rigs"""
name = "layoutMain"
label = "Layout"
family = "layout"
icon = "cubes"
def process(self):
asset = self.data["asset"]
subset = self.data["subset"]
name = pype.hosts.blender.plugin.asset_name(asset, subset)
collection = bpy.data.collections.new(name=name)
bpy.context.scene.collection.children.link(collection)
self.data['task'] = api.Session.get('AVALON_TASK')
lib.imprint(collection, self.data)
# Add the rig object and all the children meshes to
# a set and link them all at the end to avoid duplicates.
# Blender crashes if trying to link an object that is already linked.
# This links automatically the children meshes if they were not
# selected, and doesn't link them twice if they, insted,
# were manually selected by the user.
objects_to_link = set()
if (self.options or {}).get("useSelection"):
for obj in lib.get_selection():
objects_to_link.add(obj)
if obj.type == 'ARMATURE':
for subobj in obj.children:
objects_to_link.add(subobj)
for obj in objects_to_link:
collection.objects.link(obj)
return collection

View file

@ -0,0 +1,259 @@
"""Load a layout in Blender."""
import logging
from pathlib import Path
from pprint import pformat
from typing import Dict, List, Optional
from avalon import api, blender
import bpy
import pype.hosts.blender.plugin
logger = logging.getLogger("pype").getChild(
"blender").getChild("load_layout")
class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
"""Load animations from a .blend file.
Warning:
Loading the same asset more then once is not properly supported at the
moment.
"""
families = ["layout"]
representations = ["blend"]
label = "Link Layout"
icon = "code-fork"
color = "orange"
@staticmethod
def _remove(self, objects, lib_container):
for obj in objects:
if obj.type == 'ARMATURE':
bpy.data.armatures.remove(obj.data)
elif obj.type == 'MESH':
bpy.data.meshes.remove(obj.data)
bpy.data.collections.remove(bpy.data.collections[lib_container])
@staticmethod
def _process(self, libpath, lib_container, container_name, actions):
relative = bpy.context.preferences.filepaths.use_relative_paths
with bpy.data.libraries.load(
libpath, link=True, relative=relative
) as (_, data_to):
data_to.collections = [lib_container]
scene = bpy.context.scene
scene.collection.children.link(bpy.data.collections[lib_container])
layout_container = scene.collection.children[lib_container].make_local()
meshes = [
obj for obj in layout_container.objects if obj.type == 'MESH']
armatures = [
obj for obj in layout_container.objects if obj.type == 'ARMATURE']
objects_list = []
# Link meshes first, then armatures.
# The armature is unparented for all the non-local meshes,
# when it is made local.
for obj in meshes + armatures:
obj = obj.make_local()
obj.data.make_local()
if not obj.get(blender.pipeline.AVALON_PROPERTY):
obj[blender.pipeline.AVALON_PROPERTY] = dict()
avalon_info = obj[blender.pipeline.AVALON_PROPERTY]
avalon_info.update({"container_name": container_name})
action = actions.get( obj.name, None )
if obj.type == 'ARMATURE' and action is not None:
obj.animation_data.action = action
objects_list.append(obj)
layout_container.pop(blender.pipeline.AVALON_PROPERTY)
bpy.ops.object.select_all(action='DESELECT')
return objects_list
def process_asset(
self, context: dict, name: str, namespace: Optional[str] = None,
options: Optional[Dict] = None
) -> Optional[List]:
"""
Arguments:
name: Use pre-defined name
namespace: Use pre-defined namespace
context: Full parenthood of representation to load
options: Additional settings dictionary
"""
libpath = self.fname
asset = context["asset"]["name"]
subset = context["subset"]["name"]
lib_container = pype.hosts.blender.plugin.asset_name(asset, subset)
container_name = pype.hosts.blender.plugin.asset_name(
asset, subset, namespace
)
container = bpy.data.collections.new(lib_container)
container.name = container_name
blender.pipeline.containerise_existing(
container,
name,
namespace,
context,
self.__class__.__name__,
)
container_metadata = container.get(
blender.pipeline.AVALON_PROPERTY)
container_metadata["libpath"] = libpath
container_metadata["lib_container"] = lib_container
objects_list = self._process(
self, libpath, lib_container, container_name, {})
# Save the list of objects in the metadata container
container_metadata["objects"] = objects_list
nodes = list(container.objects)
nodes.append(container)
self[:] = nodes
return nodes
def update(self, container: Dict, representation: Dict):
"""Update the loaded asset.
This will remove all objects of the current collection, load the new
ones and add them to the collection.
If the objects of the collection are used in another collection they
will not be removed, only unlinked. Normally this should not be the
case though.
Warning:
No nested collections are supported at the moment!
"""
collection = bpy.data.collections.get(
container["objectName"]
)
libpath = Path(api.get_representation_path(representation))
extension = libpath.suffix.lower()
logger.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
)
assert collection, (
f"The asset is not loaded: {container['objectName']}"
)
assert not (collection.children), (
"Nested collections are not supported."
)
assert libpath, (
"No existing library file found for {container['objectName']}"
)
assert libpath.is_file(), (
f"The file doesn't exist: {libpath}"
)
assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, (
f"Unsupported file: {libpath}"
)
collection_metadata = collection.get(
blender.pipeline.AVALON_PROPERTY)
collection_libpath = collection_metadata["libpath"]
normalized_collection_libpath = (
str(Path(bpy.path.abspath(collection_libpath)).resolve())
)
normalized_libpath = (
str(Path(bpy.path.abspath(str(libpath))).resolve())
)
logger.debug(
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
normalized_collection_libpath,
normalized_libpath,
)
if normalized_collection_libpath == normalized_libpath:
logger.info("Library already loaded, not updating...")
return
objects = collection_metadata["objects"]
lib_container = collection_metadata["lib_container"]
actions = {}
for obj in objects:
if obj.type == 'ARMATURE':
actions[obj.name] = obj.animation_data.action
self._remove(self, objects, lib_container)
objects_list = self._process(
self, str(libpath), lib_container, collection.name, actions)
# Save the list of objects in the metadata container
collection_metadata["objects"] = objects_list
collection_metadata["libpath"] = str(libpath)
collection_metadata["representation"] = str(representation["_id"])
bpy.ops.object.select_all(action='DESELECT')
def remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene.
Arguments:
container (avalon-core:container-1.0): Container to remove,
from `host.ls()`.
Returns:
bool: Whether the container was deleted.
Warning:
No nested collections are supported at the moment!
"""
collection = bpy.data.collections.get(
container["objectName"]
)
if not collection:
return False
assert not (collection.children), (
"Nested collections are not supported."
)
collection_metadata = collection.get(
blender.pipeline.AVALON_PROPERTY)
objects = collection_metadata["objects"]
lib_container = collection_metadata["lib_container"]
self._remove(self, objects, lib_container)
bpy.data.collections.remove(collection)
return True

View file

@ -9,7 +9,7 @@ class ExtractBlend(pype.api.Extractor):
label = "Extract Blend"
hosts = ["blender"]
families = ["animation", "model", "rig", "action"]
families = ["animation", "model", "rig", "action", "layout"]
optional = True
def process(self, instance):

View file

@ -0,0 +1,25 @@
import pyblish.api
import avalon.blender.workio
class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
"""Increment current workfile version."""
order = pyblish.api.IntegratorOrder + 0.9
label = "Increment Workfile Version"
optional = True
hosts = ["blender"]
families = ["animation", "model", "rig", "action"]
def process(self, context):
assert all(result["success"] for result in context.data["results"]), (
"Publishing not succesfull so version is not increased.")
from pype.lib import version_up
path = context.data["currentFile"]
filepath = version_up(path)
avalon.blender.workio.save_file(filepath, copy=False)
self.log.info('Incrementing script version')

View file

@ -14,18 +14,28 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin):
families = ["imagesequence", "render", "render2d", "source"]
enabled = False
def process(self, instance):
# presetable attribute
ffmpeg_args = None
def process(self, instance):
self.log.info("subset {}".format(instance.data['subset']))
if 'crypto' in instance.data['subset']:
return
# ffmpeg doesn't support multipart exrs
if instance.data.get("multipartExr") is True:
return
# get representation and loop them
representations = instance.data["representations"]
# filter out mov and img sequences
representations_new = representations[:]
if instance.data.get("multipartExr"):
# ffmpeg doesn't support multipart exrs
return
for repre in representations:
tags = repre.get("tags", [])
self.log.debug(repre)
@ -33,11 +43,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin):
if not valid:
continue
if not isinstance(repre['files'], list):
continue
if instance.data.get("multipartExr") is True:
# ffmpeg doesn't support multipart exrs
if not isinstance(repre['files'], (list, tuple)):
continue
stagingdir = os.path.normpath(repre.get("stagingDir"))
@ -57,21 +63,19 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin):
self.log.info("output {}".format(full_output_path))
config_data = instance.context.data['output_repre_config']
proj_name = os.environ.get('AVALON_PROJECT', '__default__')
profile = config_data.get(proj_name, config_data['__default__'])
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
ffmpeg_args = self.ffmpeg_args or {}
jpeg_items = []
jpeg_items.append(ffmpeg_path)
# override file if already exists
jpeg_items.append("-y")
# use same input args like with mov
jpeg_items.extend(profile.get('input', []))
jpeg_items.extend(ffmpeg_args.get("input") or [])
# input file
jpeg_items.append("-i {}".format(full_input_path))
# output arguments from presets
jpeg_items.extend(ffmpeg_args.get("output") or [])
# output file
jpeg_items.append(full_output_path)

View file

@ -277,7 +277,13 @@ class CollectLook(pyblish.api.InstancePlugin):
if looksets:
for look in looksets:
for at in shaderAttrs:
con = cmds.listConnections("{}.{}".format(look, at))
try:
con = cmds.listConnections("{}.{}".format(look, at))
except ValueError:
# skip attributes that are invalid in current
# context. For example in the case where
# Arnold is not enabled.
continue
if con:
materials.extend(con)

View file

@ -53,6 +53,47 @@ from pype.hosts.maya.expected_files import ExpectedFiles
from pype.hosts.maya import lib
<<<<<<< HEAD
=======
R_SINGLE_FRAME = re.compile(r"^(-?)\d+$")
R_FRAME_RANGE = re.compile(r"^(?P<sf>(-?)\d+)-(?P<ef>(-?)\d+)$")
R_FRAME_NUMBER = re.compile(r".+\.(?P<frame>[0-9]+)\..+")
R_LAYER_TOKEN = re.compile(
r".*((?:%l)|(?:<layer>)|(?:<renderlayer>)).*", re.IGNORECASE
)
R_AOV_TOKEN = re.compile(r".*%a.*|.*<aov>.*|.*<renderpass>.*", re.IGNORECASE)
R_SUBSTITUTE_AOV_TOKEN = re.compile(r"%a|<aov>|<renderpass>", re.IGNORECASE)
R_REMOVE_AOV_TOKEN = re.compile(r"(?:_|\.)((?:%a)|(?:<aov>)|(?:<renderpass>))",
re.IGNORECASE)
# to remove unused renderman tokens
R_CLEAN_FRAME_TOKEN = re.compile(r"\.?<f\d>\.?", re.IGNORECASE)
R_CLEAN_EXT_TOKEN = re.compile(r"\.?<ext>\.?", re.IGNORECASE)
R_SUBSTITUTE_LAYER_TOKEN = re.compile(
r"%l|<layer>|<renderlayer>", re.IGNORECASE
)
R_SUBSTITUTE_CAMERA_TOKEN = re.compile(r"%c|<camera>", re.IGNORECASE)
R_SUBSTITUTE_SCENE_TOKEN = re.compile(r"%s|<scene>", re.IGNORECASE)
RENDERER_NAMES = {
"mentalray": "MentalRay",
"vray": "V-Ray",
"arnold": "Arnold",
"renderman": "Renderman",
"redshift": "Redshift",
}
# not sure about the renderman image prefix
ImagePrefixes = {
"mentalray": "defaultRenderGlobals.imageFilePrefix",
"vray": "vraySettings.fileNamePrefix",
"arnold": "defaultRenderGlobals.imageFilePrefix",
"renderman": "rmanGlobals.imageFileFormat",
"redshift": "defaultRenderGlobals.imageFilePrefix",
}
>>>>>>> origin/develop
class CollectMayaRender(pyblish.api.ContextPlugin):
"""Gather all publishable render layers from renderSetup."""
@ -369,7 +410,218 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
return rset.getOverrides()
def get_render_attribute(self, attr, layer):
<<<<<<< HEAD
"""Get attribute from render options.
=======
return lib.get_attr_in_layer(
"defaultRenderGlobals.{}".format(attr), layer=layer
)
class ExpectedFiles:
multipart = False
def get(self, renderer, layer):
renderSetup.instance().switchToLayerUsingLegacyName(layer)
if renderer.lower() == "arnold":
return self._get_files(ExpectedFilesArnold(layer))
elif renderer.lower() == "vray":
return self._get_files(ExpectedFilesVray(layer))
elif renderer.lower() == "redshift":
return self._get_files(ExpectedFilesRedshift(layer))
elif renderer.lower() == "mentalray":
return self._get_files(ExpectedFilesMentalray(layer))
elif renderer.lower() == "renderman":
return self._get_files(ExpectedFilesRenderman(layer))
else:
raise UnsupportedRendererException(
"unsupported {}".format(renderer)
)
def _get_files(self, renderer):
files = renderer.get_files()
self.multipart = renderer.multipart
return files
@six.add_metaclass(ABCMeta)
class AExpectedFiles:
renderer = None
layer = None
multipart = False
def __init__(self, layer):
self.layer = layer
@abstractmethod
def get_aovs(self):
pass
def get_renderer_prefix(self):
try:
file_prefix = cmds.getAttr(ImagePrefixes[self.renderer])
except KeyError:
raise UnsupportedRendererException(
"Unsupported renderer {}".format(self.renderer)
)
return file_prefix
def _get_layer_data(self):
# ______________________________________________
# ____________________/ ____________________________________________/
# 1 - get scene name /__________________/
# ____________________/
scene_dir, scene_basename = os.path.split(cmds.file(q=True, loc=True))
scene_name, _ = os.path.splitext(scene_basename)
# ______________________________________________
# ____________________/ ____________________________________________/
# 2 - detect renderer /__________________/
# ____________________/
renderer = self.renderer
# ________________________________________________
# __________________/ ______________________________________________/
# 3 - image prefix /__________________/
# __________________/
file_prefix = self.get_renderer_prefix()
if not file_prefix:
raise RuntimeError("Image prefix not set")
default_ext = cmds.getAttr("defaultRenderGlobals.imfPluginKey")
# ________________________________________________
# __________________/ ______________________________________________/
# 4 - get renderable cameras_____________/
# __________________/
# if we have <camera> token in prefix path we'll expect output for
# every renderable camera in layer.
renderable_cameras = self.get_renderable_cameras()
# ________________________________________________
# __________________/ ______________________________________________/
# 5 - get AOVs /____________________/
# __________________/
enabled_aovs = self.get_aovs()
layer_name = self.layer
if self.layer.startswith("rs_"):
layer_name = self.layer[3:]
start_frame = int(self.get_render_attribute("startFrame"))
end_frame = int(self.get_render_attribute("endFrame"))
frame_step = int(self.get_render_attribute("byFrameStep"))
padding = int(self.get_render_attribute("extensionPadding"))
scene_data = {
"frameStart": start_frame,
"frameEnd": end_frame,
"frameStep": frame_step,
"padding": padding,
"cameras": renderable_cameras,
"sceneName": scene_name,
"layerName": layer_name,
"renderer": renderer,
"defaultExt": default_ext,
"filePrefix": file_prefix,
"enabledAOVs": enabled_aovs,
}
return scene_data
def _generate_single_file_sequence(self, layer_data, aov_name=None):
expected_files = []
file_prefix = layer_data["filePrefix"]
for cam in layer_data["cameras"]:
mappings = [
(R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]),
(R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]),
(R_SUBSTITUTE_CAMERA_TOKEN, cam),
(R_CLEAN_FRAME_TOKEN, ""),
(R_CLEAN_EXT_TOKEN, ""),
]
# this is required to remove unfilled aov token, for example
# in Redshift
if aov_name:
mappings.append((R_SUBSTITUTE_AOV_TOKEN, aov_name))
else:
mappings.append((R_REMOVE_AOV_TOKEN, ""))
for regex, value in mappings:
file_prefix = re.sub(regex, value, file_prefix)
for frame in range(
int(layer_data["frameStart"]),
int(layer_data["frameEnd"]) + 1,
int(layer_data["frameStep"]),
):
expected_files.append(
"{}.{}.{}".format(
file_prefix,
str(frame).rjust(layer_data["padding"], "0"),
layer_data["defaultExt"],
)
)
return expected_files
def _generate_aov_file_sequences(self, layer_data):
expected_files = []
aov_file_list = {}
file_prefix = layer_data["filePrefix"]
for aov in layer_data["enabledAOVs"]:
for cam in layer_data["cameras"]:
mappings = (
(R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]),
(R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]),
(R_SUBSTITUTE_CAMERA_TOKEN, cam),
(R_SUBSTITUTE_AOV_TOKEN, aov[0]),
(R_CLEAN_FRAME_TOKEN, ""),
(R_CLEAN_EXT_TOKEN, ""),
)
for regex, value in mappings:
file_prefix = re.sub(regex, value, file_prefix)
aov_files = []
for frame in range(
int(layer_data["frameStart"]),
int(layer_data["frameEnd"]) + 1,
int(layer_data["frameStep"]),
):
aov_files.append(
"{}.{}.{}".format(
file_prefix,
str(frame).rjust(layer_data["padding"], "0"),
aov[1],
)
)
# if we have more then one renderable camera, append
# camera name to AOV to allow per camera AOVs.
aov_name = aov[0]
if len(layer_data["cameras"]) > 1:
aov_name = "{}_{}".format(aov[0], cam)
aov_file_list[aov_name] = aov_files
file_prefix = layer_data["filePrefix"]
expected_files.append(aov_file_list)
return expected_files
def get_files(self):
"""
This method will return list of expected files.
It will translate render token strings ('<RenderPass>', etc.) to
their values. This task is tricky as every renderer deals with this
differently. It depends on `get_aovs()` abstract method implemented
for every supported renderer.
"""
layer_data = self._get_layer_data()
>>>>>>> origin/develop
Args:
attr (str): name of attribute to be looked up.
@ -381,3 +633,179 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
return lib.get_attr_in_layer(
"defaultRenderGlobals.{}".format(attr), layer=layer
)
<<<<<<< HEAD
=======
pass_type = vray_node_attr.rsplit("_", 1)[-1]
# Support V-Ray extratex explicit name (if set by user)
if pass_type == "extratex":
explicit_attr = "{}.vray_explicit_name_extratex".format(node)
explicit_name = cmds.getAttr(explicit_attr)
if explicit_name:
return explicit_name
# Node type is in the attribute name but we need to check if value
# of the attribute as it can be changed
return cmds.getAttr("{}.{}".format(node, vray_node_attr))
class ExpectedFilesRedshift(AExpectedFiles):
# mapping redshift extension dropdown values to strings
ext_mapping = ["iff", "exr", "tif", "png", "tga", "jpg"]
# name of aovs that are not merged into resulting exr and we need
# them specified in expectedFiles output.
unmerged_aovs = ["Cryptomatte"]
def __init__(self, layer):
super(ExpectedFilesRedshift, self).__init__(layer)
self.renderer = "redshift"
def get_renderer_prefix(self):
prefix = super(ExpectedFilesRedshift, self).get_renderer_prefix()
prefix = "{}.<aov>".format(prefix)
return prefix
def get_files(self):
expected_files = super(ExpectedFilesRedshift, self).get_files()
# we need to add one sequence for plain beauty if AOVs are enabled.
# as redshift output beauty without 'beauty' in filename.
layer_data = self._get_layer_data()
if layer_data.get("enabledAOVs"):
expected_files[0][u"beauty"] = self._generate_single_file_sequence(
layer_data
)
# Redshift doesn't merge Cryptomatte AOV to final exr. We need to check
# for such condition and add it to list of expected files.
for aov in layer_data.get("enabledAOVs"):
if aov[0].lower() == "cryptomatte":
aov_name = aov[0]
expected_files.append(
{aov_name: self._generate_single_file_sequence(
layer_data, aov_name=aov_name)})
return expected_files
def get_aovs(self):
enabled_aovs = []
try:
default_ext = self.ext_mapping[
cmds.getAttr("redshiftOptions.imageFormat")
]
except ValueError:
# this occurs when Render Setting windows was not opened yet. In
# such case there are no Redshift options created so query
# will fail.
raise ValueError("Render settings are not initialized")
rs_aovs = [n for n in cmds.ls(type="RedshiftAOV")]
# todo: find out how to detect multichannel exr for redshift
for aov in rs_aovs:
enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov)))
for override in self.get_layer_overrides(
"{}.enabled".format(aov), self.layer
):
enabled = self.maya_is_true(override)
if enabled:
# If AOVs are merged into multipart exr, append AOV only if it
# is in the list of AOVs that renderer cannot (or will not)
# merge into final exr.
if self.maya_is_true(
cmds.getAttr("redshiftOptions.exrForceMultilayer")
):
if cmds.getAttr("%s.name" % aov) in self.unmerged_aovs:
enabled_aovs.append(
(cmds.getAttr("%s.name" % aov), default_ext)
)
else:
enabled_aovs.append(
(cmds.getAttr("%s.name" % aov), default_ext)
)
if self.maya_is_true(
cmds.getAttr("redshiftOptions.exrForceMultilayer")
):
# AOVs are merged in mutli-channel file
self.multipart = True
return enabled_aovs
class ExpectedFilesRenderman(AExpectedFiles):
def __init__(self, layer):
super(ExpectedFilesRenderman, self).__init__(layer)
self.renderer = "renderman"
def get_aovs(self):
enabled_aovs = []
default_ext = "exr"
displays = cmds.listConnections("rmanGlobals.displays")
for aov in displays:
aov_name = str(aov)
if aov_name == "rmanDefaultDisplay":
aov_name = "beauty"
enabled = self.maya_is_true(cmds.getAttr("{}.enable".format(aov)))
for override in self.get_layer_overrides(
"{}.enable".format(aov), self.layer
):
enabled = self.maya_is_true(override)
if enabled:
enabled_aovs.append((aov_name, default_ext))
return enabled_aovs
def get_files(self):
"""
In renderman we hack it with prepending path. This path would
normally be translated from `rmanGlobals.imageOutputDir`. We skip
this and harcode prepend path we expect. There is no place for user
to mess around with this settings anyway and it is enforced in
render settings validator.
"""
layer_data = self._get_layer_data()
new_aovs = {}
expected_files = super(ExpectedFilesRenderman, self).get_files()
# we always get beauty
for aov, files in expected_files[0].items():
new_files = []
for file in files:
new_file = "{}/{}/{}".format(
layer_data["sceneName"], layer_data["layerName"], file
)
new_files.append(new_file)
new_aovs[aov] = new_files
return [new_aovs]
class ExpectedFilesMentalray(AExpectedFiles):
def __init__(self, layer):
raise UnimplementedRendererException("Mentalray not implemented")
def get_aovs(self):
return []
class AOVError(Exception):
pass
class UnsupportedRendererException(Exception):
pass
class UnimplementedRendererException(Exception):
pass
>>>>>>> origin/develop

View file

@ -0,0 +1,17 @@
import pyblish.api
from pype.hosts.resolve.utils import get_resolve_module
class CollectProject(pyblish.api.ContextPlugin):
"""Collect Project object"""
order = pyblish.api.CollectorOrder - 0.1
label = "Collect Project"
hosts = ["resolve"]
def process(self, context):
resolve = get_resolve_module()
PM = resolve.GetProjectManager()
P = PM.GetCurrentProject()
self.log.info(P.GetName())

View file

@ -18,6 +18,9 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
hosts = ["standalonepublisher"]
order = pyblish.api.ExtractorOrder
# Presetable attribute
ffmpeg_args = None
def process(self, instance):
repres = instance.data.get('representations')
if not repres:
@ -66,27 +69,23 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1]
self.log.info("output {}".format(full_thumbnail_path))
config_data = instance.context.data.get("output_repre_config", {})
proj_name = os.environ.get("AVALON_PROJECT", "__default__")
profile = config_data.get(
proj_name,
config_data.get("__default__", {})
)
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
ffmpeg_args = self.ffmpeg_args or {}
jpeg_items = []
jpeg_items.append(ffmpeg_path)
# override file if already exists
jpeg_items.append("-y")
# add input filters from peresets
if profile:
jpeg_items.extend(profile.get('input', []))
jpeg_items.extend(ffmpeg_args.get("input") or [])
# input file
jpeg_items.append("-i {}".format(full_input_path))
# extract only single file
jpeg_items.append("-vframes 1")
jpeg_items.extend(ffmpeg_args.get("output") or [])
# output file
jpeg_items.append(full_thumbnail_path)

View file

@ -0,0 +1,16 @@
import os
def get_resource(*args):
""" Serves to simple resources access
:param *args: should contain *subfolder* names and *filename* of
resource from resources folder
:type *args: list
"""
return os.path.normpath(
os.path.join(
os.path.dirname(__file__),
*args
)
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
pype/resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
pype/resources/icon_dev.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
pype/resources/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View file

@ -0,0 +1,17 @@
<svg version="1.1" id="loader-1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="312px" height="312px" viewBox="0 0 40 40" xml:space="preserve">
<path opacity="0.2" fill="#ffa500" d="M20.201,5.169c-8.254,0-14.946,6.692-14.946,14.946c0,8.255,6.692,14.946,14.946,14.946
s14.946-6.691,14.946-14.946C35.146,11.861,28.455,5.169,20.201,5.169z M20.201,31.749c-6.425,0-11.634-5.208-11.634-11.634
c0-6.425,5.209-11.634,11.634-11.634c6.425,0,11.633,5.209,11.633,11.634C31.834,26.541,26.626,31.749,20.201,31.749z"/>
<path fill="#ffa500" d="M26.013,10.047l1.654-2.866c-2.198-1.272-4.743-2.012-7.466-2.012h0v3.312h0
C22.32,8.481,24.301,9.057,26.013,10.047z">
<animateTransform attributeType="xml"
attributeName="transform"
type="rotate"
from="00 20.2 20.1"
to="360 20.2 20.1"
dur="0.5s"
repeatCount="indefinite"/>
</path>
<text x="3" y="23" fill="#ffa500" font-style="bold" font-size="7px" font-family="sans-serif">Working...</text>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,4 @@
import sys
import pype_tray
sys.exit(pype_tray.PypeTrayApplication().exec_())

View file

@ -0,0 +1,506 @@
import os
import sys
import platform
from avalon import style
from Qt import QtCore, QtGui, QtWidgets, QtSvg
from pype.resources import get_resource
from pype.api import config, Logger
class TrayManager:
"""Cares about context of application.
Load submenus, actions, separators and modules into tray's context.
"""
modules = {}
services = {}
services_submenu = None
errors = []
items = (
config.get_presets(first_run=True)
.get('tray', {})
.get('menu_items', [])
)
available_sourcetypes = ['python', 'file']
def __init__(self, tray_widget, main_window):
self.tray_widget = tray_widget
self.main_window = main_window
self.log = Logger().get_logger(self.__class__.__name__)
self.icon_run = QtGui.QIcon(get_resource('circle_green.png'))
self.icon_stay = QtGui.QIcon(get_resource('circle_orange.png'))
self.icon_failed = QtGui.QIcon(get_resource('circle_red.png'))
self.services_thread = None
def process_presets(self):
"""Add modules to tray by presets.
This is start up method for TrayManager. Loads presets and import
modules described in "menu_items.json". In `item_usage` key you can
specify by item's title or import path if you want to import it.
Example of "menu_items.json" file:
{
"item_usage": {
"Statics Server": false
}
}, {
"item_import": [{
"title": "Ftrack",
"type": "module",
"import_path": "pype.ftrack.tray",
"fromlist": ["pype", "ftrack"]
}, {
"title": "Statics Server",
"type": "module",
"import_path": "pype.services.statics_server",
"fromlist": ["pype","services"]
}]
}
In this case `Statics Server` won't be used.
"""
# Backwards compatible presets loading
if isinstance(self.items, list):
items = self.items
else:
items = []
# Get booleans is module should be used
usages = self.items.get("item_usage") or {}
for item in self.items.get("item_import", []):
import_path = item.get("import_path")
title = item.get("title")
item_usage = usages.get(title)
if item_usage is None:
item_usage = usages.get(import_path, True)
if item_usage:
items.append(item)
else:
if not title:
title = import_path
self.log.debug("{} - Module ignored".format(title))
if items:
self.process_items(items, self.tray_widget.menu)
# Add services if they are
if self.services_submenu is not None:
self.tray_widget.menu.addMenu(self.services_submenu)
# Add separator
if items and self.services_submenu is not None:
self.add_separator(self.tray_widget.menu)
# Add Exit action to menu
aExit = QtWidgets.QAction("&Exit", self.tray_widget)
aExit.triggered.connect(self.tray_widget.exit)
self.tray_widget.menu.addAction(aExit)
# Tell each module which modules were imported
self.connect_modules()
self.start_modules()
def process_items(self, items, parent_menu):
""" Loop through items and add them to parent_menu.
:param items: contains dictionary objects representing each item
:type items: list
:param parent_menu: menu where items will be add
:type parent_menu: QtWidgets.QMenu
"""
for item in items:
i_type = item.get('type', None)
result = False
if i_type is None:
continue
elif i_type == 'module':
result = self.add_module(item, parent_menu)
elif i_type == 'action':
result = self.add_action(item, parent_menu)
elif i_type == 'menu':
result = self.add_menu(item, parent_menu)
elif i_type == 'separator':
result = self.add_separator(parent_menu)
if result is False:
self.errors.append(item)
def add_module(self, item, parent_menu):
"""Inicialize object of module and add it to context.
:param item: item from presets containing information about module
:type item: dict
:param parent_menu: menu where module's submenus/actions will be add
:type parent_menu: QtWidgets.QMenu
:returns: success of module implementation
:rtype: bool
REQUIRED KEYS (item):
:import_path (*str*):
- full import path as python's import
- e.g. *"path.to.module"*
:fromlist (*list*):
- subparts of import_path (as from is used)
- e.g. *["path", "to"]*
OPTIONAL KEYS (item):
:title (*str*):
- represents label shown in services menu
- import_path is used if title is not set
- title is not used at all if module is not a service
.. note::
Module is added as **service** if object does not have
*tray_menu* method.
"""
import_path = item.get('import_path', None)
title = item.get('title', import_path)
fromlist = item.get('fromlist', [])
try:
module = __import__(
"{}".format(import_path),
fromlist=fromlist
)
obj = module.tray_init(self.tray_widget, self.main_window)
name = obj.__class__.__name__
if hasattr(obj, 'tray_menu'):
obj.tray_menu(parent_menu)
else:
if self.services_submenu is None:
self.services_submenu = QtWidgets.QMenu(
'Services', self.tray_widget.menu
)
action = QtWidgets.QAction(title, self.services_submenu)
action.setIcon(self.icon_run)
self.services_submenu.addAction(action)
if hasattr(obj, 'set_qaction'):
obj.set_qaction(action, self.icon_failed)
self.modules[name] = obj
self.log.info("{} - Module imported".format(title))
except ImportError as ie:
if self.services_submenu is None:
self.services_submenu = QtWidgets.QMenu(
'Services', self.tray_widget.menu
)
action = QtWidgets.QAction(title, self.services_submenu)
action.setIcon(self.icon_failed)
self.services_submenu.addAction(action)
self.log.warning(
"{} - Module import Error: {}".format(title, str(ie)),
exc_info=True
)
return False
return True
def add_action(self, item, parent_menu):
"""Adds action to parent_menu.
:param item: item from presets containing information about action
:type item: dictionary
:param parent_menu: menu where action will be added
:type parent_menu: QtWidgets.QMenu
:returns: success of adding item to parent_menu
:rtype: bool
REQUIRED KEYS (item):
:title (*str*):
- represents label shown in menu
:sourcetype (*str*):
- type of action *enum["file", "python"]*
:command (*str*):
- filepath to script *(sourcetype=="file")*
- python code as string *(sourcetype=="python")*
OPTIONAL KEYS (item):
:tooltip (*str*):
- will be shown when hover over action
"""
sourcetype = item.get('sourcetype', None)
command = item.get('command', None)
title = item.get('title', '*ERROR*')
tooltip = item.get('tooltip', None)
if sourcetype not in self.available_sourcetypes:
self.log.error('item "{}" has invalid sourcetype'.format(title))
return False
if command is None or command.strip() == '':
self.log.error('item "{}" has invalid command'.format(title))
return False
new_action = QtWidgets.QAction(title, parent_menu)
if tooltip is not None and tooltip.strip() != '':
new_action.setToolTip(tooltip)
if sourcetype == 'python':
new_action.triggered.connect(
lambda: exec(command)
)
elif sourcetype == 'file':
command = os.path.normpath(command)
if '$' in command:
command_items = command.split(os.path.sep)
for i in range(len(command_items)):
if command_items[i].startswith('$'):
# TODO: raise error if environment was not found?
command_items[i] = os.environ.get(
command_items[i].replace('$', ''), command_items[i]
)
command = os.path.sep.join(command_items)
new_action.triggered.connect(
lambda: exec(open(command).read(), globals())
)
parent_menu.addAction(new_action)
def add_menu(self, item, parent_menu):
""" Adds submenu to parent_menu.
:param item: item from presets containing information about menu
:type item: dictionary
:param parent_menu: menu where submenu will be added
:type parent_menu: QtWidgets.QMenu
:returns: success of adding item to parent_menu
:rtype: bool
REQUIRED KEYS (item):
:title (*str*):
- represents label shown in menu
:items (*list*):
- list of submenus / actions / separators / modules *(dict)*
"""
try:
title = item.get('title', None)
if title is None or title.strip() == '':
self.log.error('Missing title in menu from presets')
return False
new_menu = QtWidgets.QMenu(title, parent_menu)
new_menu.setProperty('submenu', 'on')
parent_menu.addMenu(new_menu)
self.process_items(item.get('items', []), new_menu)
return True
except Exception:
return False
def add_separator(self, parent_menu):
""" Adds separator to parent_menu.
:param parent_menu: menu where submenu will be added
:type parent_menu: QtWidgets.QMenu
:returns: success of adding item to parent_menu
:rtype: bool
"""
try:
parent_menu.addSeparator()
return True
except Exception:
return False
def connect_modules(self):
"""Sends all imported modules to imported modules
which have process_modules method.
"""
for obj in self.modules.values():
if hasattr(obj, 'process_modules'):
obj.process_modules(self.modules)
def start_modules(self):
"""Modules which can be modified by another modules and
must be launched after *connect_modules* should have tray_start
to start their process afterwards. (e.g. Ftrack actions)
"""
for obj in self.modules.values():
if hasattr(obj, 'tray_start'):
obj.tray_start()
def on_exit(self):
for obj in self.modules.values():
if hasattr(obj, 'tray_exit'):
try:
obj.tray_exit()
except Exception:
self.log.error("Failed to exit module {}".format(
obj.__class__.__name__
))
class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
"""Tray widget.
:param parent: Main widget that cares about all GUIs
:type parent: QtWidgets.QMainWindow
"""
def __init__(self, parent):
if os.getenv("PYPE_DEV"):
icon_file_name = "icon_dev.png"
else:
icon_file_name = "icon.png"
self.icon = QtGui.QIcon(get_resource(icon_file_name))
QtWidgets.QSystemTrayIcon.__init__(self, self.icon, parent)
# Store parent - QtWidgets.QMainWindow()
self.parent = parent
# Setup menu in Tray
self.menu = QtWidgets.QMenu()
self.menu.setStyleSheet(style.load_stylesheet())
# Set modules
self.tray_man = TrayManager(self, self.parent)
self.tray_man.process_presets()
# Catch activate event
self.activated.connect(self.on_systray_activated)
# Add menu to Context of SystemTrayIcon
self.setContextMenu(self.menu)
def on_systray_activated(self, reason):
# show contextMenu if left click
if platform.system().lower() == "darwin":
return
if reason == QtWidgets.QSystemTrayIcon.Trigger:
position = QtGui.QCursor().pos()
self.contextMenu().popup(position)
def exit(self):
""" Exit whole application.
- Icon won't stay in tray after exit.
"""
self.hide()
self.tray_man.on_exit()
QtCore.QCoreApplication.exit()
class TrayMainWindow(QtWidgets.QMainWindow):
""" TrayMainWindow is base of Pype application.
Every widget should have set this window as parent because
QSystemTrayIcon widget is not allowed to be a parent of any widget.
:param app: Qt application manages application's control flow
:type app: QtWidgets.QApplication
.. note::
*TrayMainWindow* has ability to show **working** widget.
Calling methods:
- ``show_working()``
- ``hide_working()``
.. todo:: Hide working widget if idle is too long
"""
def __init__(self, app):
super().__init__()
self.app = app
self.set_working_widget()
self.trayIcon = SystemTrayIcon(self)
self.trayIcon.show()
def set_working_widget(self):
image_file = get_resource('working.svg')
img_pix = QtGui.QPixmap(image_file)
if image_file.endswith('.svg'):
widget = QtSvg.QSvgWidget(image_file)
else:
widget = QtWidgets.QLabel()
widget.setPixmap(img_pix)
# Set widget properties
widget.setGeometry(img_pix.rect())
widget.setMask(img_pix.mask())
widget.setWindowFlags(
QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint
)
widget.setAttribute(QtCore.Qt.WA_TranslucentBackground, True)
self.center_widget(widget)
self._working_widget = widget
self.helper = DragAndDropHelper(self._working_widget)
def center_widget(self, widget):
frame_geo = widget.frameGeometry()
screen = self.app.desktop().cursor().pos()
center_point = self.app.desktop().screenGeometry(
self.app.desktop().screenNumber(screen)
).center()
frame_geo.moveCenter(center_point)
widget.move(frame_geo.topLeft())
def show_working(self):
self._working_widget.show()
def hide_working(self):
self.center_widget(self._working_widget)
self._working_widget.hide()
class DragAndDropHelper:
""" Helper adds to widget drag and drop ability
:param widget: Qt Widget where drag and drop ability will be added
"""
def __init__(self, widget):
self.widget = widget
self.widget.mousePressEvent = self.mousePressEvent
self.widget.mouseMoveEvent = self.mouseMoveEvent
self.widget.mouseReleaseEvent = self.mouseReleaseEvent
def mousePressEvent(self, event):
self.__mousePressPos = None
self.__mouseMovePos = None
if event.button() == QtCore.Qt.LeftButton:
self.__mousePressPos = event.globalPos()
self.__mouseMovePos = event.globalPos()
def mouseMoveEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
# adjust offset from clicked point to origin of widget
currPos = self.widget.mapToGlobal(
self.widget.pos()
)
globalPos = event.globalPos()
diff = globalPos - self.__mouseMovePos
newPos = self.widget.mapFromGlobal(currPos + diff)
self.widget.move(newPos)
self.__mouseMovePos = globalPos
def mouseReleaseEvent(self, event):
if self.__mousePressPos is not None:
moved = event.globalPos() - self.__mousePressPos
if moved.manhattanLength() > 3:
event.ignore()
return
class PypeTrayApplication(QtWidgets.QApplication):
"""Qt application manages application's control flow."""
def __init__(self):
super(self.__class__, self).__init__(sys.argv)
# Allows to close widgets without exiting app
self.setQuitOnLastWindowClosed(False)
# Sets up splash
splash_widget = self.set_splash()
splash_widget.show()
self.processEvents()
self.main_window = TrayMainWindow(self)
splash_widget.hide()
def set_splash(self):
if os.getenv("PYPE_DEV"):
splash_file_name = "splash_dev.png"
else:
splash_file_name = "splash.png"
splash_pix = QtGui.QPixmap(get_resource(splash_file_name))
splash = QtWidgets.QSplashScreen(splash_pix)
splash.setMask(splash_pix.mask())
splash.setEnabled(False)
splash.setWindowFlags(
QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint
)
return splash

BIN
res/app_icons/resolve.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB