mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into feature/igniter-improvements
This commit is contained in:
commit
5c441aaf98
45 changed files with 3052 additions and 1525 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -5,9 +5,6 @@
|
|||
[submodule "repos/avalon-unreal-integration"]
|
||||
path = repos/avalon-unreal-integration
|
||||
url = git@github.com:pypeclub/avalon-unreal-integration.git
|
||||
[submodule "repos/maya-look-assigner"]
|
||||
path = repos/maya-look-assigner
|
||||
url = git@github.com:pypeclub/maya-look-assigner.git
|
||||
[submodule "pype/modules/ftrack/python2_vendor/ftrack-python-api"]
|
||||
path = pype/modules/ftrack/python2_vendor/ftrack-python-api
|
||||
url = https://bitbucket.org/ftrack/ftrack-python-api.git
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from maya import utils, cmds
|
|||
from avalon import api as avalon
|
||||
from avalon import pipeline
|
||||
from avalon.maya import suspended_refresh
|
||||
from avalon.maya.pipeline import IS_HEADLESS, _on_task_changed
|
||||
from avalon.maya.pipeline import IS_HEADLESS
|
||||
from avalon.tools import workfiles
|
||||
from pyblish import api as pyblish
|
||||
from pype.lib import any_outdated
|
||||
|
|
@ -45,9 +45,7 @@ def install():
|
|||
avalon.on("open", on_open)
|
||||
avalon.on("new", on_new)
|
||||
avalon.before("save", on_before_save)
|
||||
|
||||
log.info("Overriding existing event 'taskChanged'")
|
||||
override_event("taskChanged", on_task_changed)
|
||||
avalon.on("taskChanged", on_task_changed)
|
||||
|
||||
log.info("Setting default family states for loader..")
|
||||
avalon.data["familiesStateToggled"] = ["imagesequence"]
|
||||
|
|
@ -61,24 +59,6 @@ def uninstall():
|
|||
menu.uninstall()
|
||||
|
||||
|
||||
def override_event(event, callback):
|
||||
"""
|
||||
Override existing event callback
|
||||
Args:
|
||||
event (str): name of the event
|
||||
callback (function): callback to be triggered
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
|
||||
ref = weakref.WeakSet()
|
||||
ref.add(callback)
|
||||
|
||||
pipeline._registered_event_handlers[event] = ref
|
||||
|
||||
|
||||
def on_init(_):
|
||||
avalon.logger.info("Running callback on init..")
|
||||
|
||||
|
|
@ -215,7 +195,6 @@ def on_new(_):
|
|||
def on_task_changed(*args):
|
||||
"""Wrapped function of app initialize and maya's on task changed"""
|
||||
# Run
|
||||
_on_task_changed()
|
||||
with suspended_refresh():
|
||||
lib.set_context_settings()
|
||||
lib.update_content_on_context_change()
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class GenerateUUIDsOnInvalidAction(pyblish.api.Action):
|
|||
nodes (list): all nodes to regenerate ids on
|
||||
"""
|
||||
|
||||
from pype.hosts.maya import lib
|
||||
from pype.hosts.maya.api import lib
|
||||
import avalon.io as io
|
||||
|
||||
asset = instance.data['asset']
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ def override_toolbox_ui():
|
|||
log.warning("Could not import Workfiles tool")
|
||||
|
||||
try:
|
||||
import mayalookassigner
|
||||
from pype.tools import mayalookassigner
|
||||
except Exception:
|
||||
log.warning("Could not import Maya Look assigner tool")
|
||||
|
||||
|
|
|
|||
|
|
@ -2127,15 +2127,9 @@ def bake_to_world_space(nodes,
|
|||
|
||||
|
||||
def load_capture_preset(path=None, data=None):
|
||||
import capture_gui
|
||||
import capture
|
||||
|
||||
if data:
|
||||
preset = data
|
||||
else:
|
||||
path = path
|
||||
preset = capture_gui.lib.load_json(path)
|
||||
print(preset)
|
||||
preset = data
|
||||
|
||||
options = dict()
|
||||
|
||||
|
|
@ -2177,29 +2171,27 @@ def load_capture_preset(path=None, data=None):
|
|||
|
||||
temp_options2 = {}
|
||||
id = 'Viewport Options'
|
||||
light_options = {
|
||||
0: "default",
|
||||
1: 'all',
|
||||
2: 'selected',
|
||||
3: 'flat',
|
||||
4: 'nolights'}
|
||||
for key in preset[id]:
|
||||
if key == 'high_quality':
|
||||
if preset[id][key] == True:
|
||||
temp_options2['multiSampleEnable'] = True
|
||||
temp_options2['multiSampleCount'] = 4
|
||||
temp_options2['textureMaxResolution'] = 1024
|
||||
if key == 'textureMaxResolution':
|
||||
if preset[id][key] > 0:
|
||||
temp_options2['textureMaxResolution'] = preset[id][key]
|
||||
temp_options2['enableTextureMaxRes'] = True
|
||||
temp_options2['textureMaxResMode'] = 1
|
||||
else:
|
||||
temp_options2['multiSampleEnable'] = False
|
||||
temp_options2['multiSampleCount'] = 4
|
||||
temp_options2['textureMaxResolution'] = 512
|
||||
temp_options2['enableTextureMaxRes'] = True
|
||||
temp_options2['textureMaxResolution'] = preset[id][key]
|
||||
temp_options2['enableTextureMaxRes'] = False
|
||||
temp_options2['textureMaxResMode'] = 0
|
||||
|
||||
if key == 'multiSample':
|
||||
if preset[id][key] > 0:
|
||||
temp_options2['multiSampleEnable'] = True
|
||||
temp_options2['multiSampleCount'] = preset[id][key]
|
||||
else:
|
||||
temp_options2['multiSampleEnable'] = False
|
||||
temp_options2['multiSampleCount'] = preset[id][key]
|
||||
|
||||
if key == 'ssaoEnable':
|
||||
if preset[id][key] == True:
|
||||
if preset[id][key] is True:
|
||||
temp_options2['ssaoEnable'] = True
|
||||
else:
|
||||
temp_options2['ssaoEnable'] = False
|
||||
|
|
@ -2211,18 +2203,17 @@ def load_capture_preset(path=None, data=None):
|
|||
if key == 'headsUpDisplay':
|
||||
temp_options['headsUpDisplay'] = True
|
||||
|
||||
if key == 'displayLights':
|
||||
temp_options[str(key)] = light_options[preset[id][key]]
|
||||
else:
|
||||
temp_options[str(key)] = preset[id][key]
|
||||
|
||||
for key in ['override_viewport_options',
|
||||
'high_quality',
|
||||
'alphaCut',
|
||||
'gpuCacheDisplayFilter']:
|
||||
temp_options.pop(key, None)
|
||||
|
||||
for key in ['ssaoEnable']:
|
||||
'gpuCacheDisplayFilter',
|
||||
'multiSample',
|
||||
'ssaoEnable',
|
||||
'textureMaxResolution'
|
||||
]:
|
||||
temp_options.pop(key, None)
|
||||
|
||||
options['viewport_options'] = temp_options
|
||||
|
|
@ -2686,7 +2677,7 @@ def update_content_on_context_change():
|
|||
|
||||
def show_message(title, msg):
|
||||
from avalon.vendor.Qt import QtWidgets
|
||||
from ...widgets import message_window
|
||||
from pype.widgets import message_window
|
||||
|
||||
# Find maya main window
|
||||
top_level_widgets = {w.objectName(): w for w in
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ class ExtractPlayblast(pype.api.Extractor):
|
|||
hosts = ["maya"]
|
||||
families = ["review"]
|
||||
optional = True
|
||||
capture_preset = {}
|
||||
|
||||
def process(self, instance):
|
||||
self.log.info("Extracting capture..")
|
||||
|
|
@ -43,15 +44,9 @@ class ExtractPlayblast(pype.api.Extractor):
|
|||
|
||||
# get cameras
|
||||
camera = instance.data['review_camera']
|
||||
capture_preset = (
|
||||
instance.context.data['project_settings']['maya']['capture']
|
||||
)
|
||||
|
||||
try:
|
||||
preset = lib.load_capture_preset(data=capture_preset)
|
||||
except Exception:
|
||||
preset = {}
|
||||
self.log.info('using viewport preset: {}'.format(preset))
|
||||
preset = lib.load_capture_preset(data=self.capture_preset)
|
||||
|
||||
|
||||
preset['camera'] = camera
|
||||
preset['format'] = "image"
|
||||
|
|
@ -101,6 +96,9 @@ class ExtractPlayblast(pype.api.Extractor):
|
|||
# Remove panel key since it's internal value to capture_gui
|
||||
preset.pop("panel", None)
|
||||
|
||||
|
||||
self.log.info('using viewport preset: {}'.format(preset))
|
||||
|
||||
path = capture.capture(**preset)
|
||||
playblast = self._fix_playblast_output_path(path)
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class ExtractThumbnail(pype.api.Extractor):
|
|||
|
||||
capture_preset = ""
|
||||
capture_preset = (
|
||||
instance.context.data["project_settings"]['maya']['capture']
|
||||
instance.context.data["project_settings"]['maya']['publish']['ExtractPlayblast']
|
||||
)
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ class LoadMov(api.Loader):
|
|||
containerise,
|
||||
viewer_update_and_undo_stop
|
||||
)
|
||||
|
||||
version = context['version']
|
||||
version_data = version.get("data", {})
|
||||
repr_id = context["representation"]["_id"]
|
||||
|
|
|
|||
|
|
@ -73,5 +73,17 @@ class CreateImage(pype.api.Creator):
|
|||
groups.append(group)
|
||||
|
||||
for group in groups:
|
||||
long_names = []
|
||||
if group.long_name:
|
||||
for directory in group.long_name[::-1]:
|
||||
name = directory.replace(stub.PUBLISH_ICON, '').\
|
||||
replace(stub.LOADED_ICON, '')
|
||||
long_names.append(name)
|
||||
|
||||
self.data.update({"subset": "image" + group.name})
|
||||
self.data.update({"uuid": str(group.id)})
|
||||
self.data.update({"long_name": "_".join(long_names)})
|
||||
stub.imprint(group, self.data)
|
||||
# reusing existing group, need to rename afterwards
|
||||
if not create_group:
|
||||
stub.rename_layer(group.id, stub.PUBLISH_ICON + group.name)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
stub = photoshop.stub()
|
||||
layers = stub.get_layers()
|
||||
layers_meta = stub.get_layers_metadata()
|
||||
instance_names = []
|
||||
for layer in layers:
|
||||
layer_data = stub.read(layer, layers_meta)
|
||||
|
||||
|
|
@ -41,14 +42,20 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
# self.log.info("%s skipped, it was empty." % layer.Name)
|
||||
# continue
|
||||
|
||||
instance = context.create_instance(layer.name)
|
||||
instance = context.create_instance(layer_data["subset"])
|
||||
instance.append(layer)
|
||||
instance.data.update(layer_data)
|
||||
instance.data["families"] = self.families_mapping[
|
||||
layer_data["family"]
|
||||
]
|
||||
instance.data["publish"] = layer.visible
|
||||
instance_names.append(layer_data["subset"])
|
||||
|
||||
# Produce diagnostic message for any graphical
|
||||
# user interface interested in visualising it.
|
||||
self.log.info("Found: \"%s\" " % instance.data["name"])
|
||||
self.log.info("instance: {} ".format(instance.data))
|
||||
|
||||
if len(instance_names) != len(set(instance_names)):
|
||||
self.log.warning("Duplicate instances found. " +
|
||||
"Remove unwanted via SubsetManager")
|
||||
|
|
|
|||
|
|
@ -31,9 +31,10 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
|
|||
})
|
||||
|
||||
# creating representation
|
||||
_, ext = os.path.splitext(file_path)
|
||||
instance.data["representations"].append({
|
||||
"name": "psd",
|
||||
"ext": "psd",
|
||||
"name": ext[1:],
|
||||
"ext": ext[1:],
|
||||
"files": base_name,
|
||||
"stagingDir": staging_dir,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
from pype.action import get_errored_plugins_from_data
|
||||
from pype.lib import version_up
|
||||
|
|
@ -25,6 +26,7 @@ class IncrementWorkfile(pyblish.api.InstancePlugin):
|
|||
)
|
||||
|
||||
scene_path = version_up(instance.context.data["currentFile"])
|
||||
photoshop.stub().saveAs(scene_path, 'psd', True)
|
||||
_, ext = os.path.splitext(scene_path)
|
||||
photoshop.stub().saveAs(scene_path, ext[1:], True)
|
||||
|
||||
self.log.info("Incremented workfile to: {}".format(scene_path))
|
||||
|
|
|
|||
|
|
@ -25,11 +25,15 @@ class ValidateNamingRepair(pyblish.api.Action):
|
|||
for instance in instances:
|
||||
self.log.info("validate_naming instance {}".format(instance))
|
||||
name = instance.data["name"].replace(" ", "_")
|
||||
name = name.replace(instance.data["family"], '')
|
||||
instance[0].Name = name
|
||||
data = stub.read(instance[0])
|
||||
data["subset"] = "image" + name
|
||||
stub.imprint(instance[0], data)
|
||||
|
||||
name = stub.PUBLISH_ICON + name
|
||||
stub.rename_layer(instance.data["uuid"], name)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
@ -46,8 +50,11 @@ class ValidateNaming(pyblish.api.InstancePlugin):
|
|||
actions = [ValidateNamingRepair]
|
||||
|
||||
def process(self, instance):
|
||||
msg = "Name \"{}\" is not allowed.".format(instance.data["name"])
|
||||
help_msg = ' Use Repair action (A) in Pyblish to fix it.'
|
||||
msg = "Name \"{}\" is not allowed.{}".format(instance.data["name"],
|
||||
help_msg)
|
||||
assert " " not in instance.data["name"], msg
|
||||
|
||||
msg = "Subset \"{}\" is not allowed.".format(instance.data["subset"])
|
||||
msg = "Subset \"{}\" is not allowed.{}".format(instance.data["subset"],
|
||||
help_msg)
|
||||
assert " " not in instance.data["subset"], msg
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import pyblish.api
|
||||
import pype.api
|
||||
|
||||
|
||||
class ValidateSubsetUniqueness(pyblish.api.ContextPlugin):
|
||||
"""
|
||||
Validate that all subset's names are unique.
|
||||
"""
|
||||
|
||||
label = "Validate Subset Uniqueness"
|
||||
hosts = ["photoshop"]
|
||||
order = pype.api.ValidateContentsOrder
|
||||
families = ["image"]
|
||||
|
||||
def process(self, context):
|
||||
subset_names = []
|
||||
|
||||
for instance in context:
|
||||
if instance.data.get('publish'):
|
||||
subset_names.append(instance.data.get('subset'))
|
||||
|
||||
msg = (
|
||||
"Instance subset names are not unique. " +
|
||||
"Remove duplicates via SubsetManager."
|
||||
)
|
||||
assert len(subset_names) == len(set(subset_names)), msg
|
||||
20
pype/hosts/tvpaint/api/lib.py
Normal file
20
pype/hosts/tvpaint/api/lib.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from PIL import Image
|
||||
|
||||
|
||||
def composite_images(input_image_paths, output_filepath):
|
||||
"""Composite images in order from passed list.
|
||||
|
||||
Raises:
|
||||
ValueError: When entered list is empty.
|
||||
"""
|
||||
if not input_image_paths:
|
||||
raise ValueError("Nothing to composite.")
|
||||
|
||||
img_obj = None
|
||||
for image_filepath in input_image_paths:
|
||||
_img_obj = Image.open(image_filepath)
|
||||
if img_obj is None:
|
||||
img_obj = _img_obj
|
||||
else:
|
||||
img_obj.alpha_composite(_img_obj)
|
||||
img_obj.save(output_filepath)
|
||||
|
|
@ -48,7 +48,10 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
instance_data["subset"] = new_subset_name
|
||||
|
||||
instance = context.create_instance(**instance_data)
|
||||
instance.data["layers"] = context.data["layersData"]
|
||||
|
||||
instance.data["layers"] = copy.deepcopy(
|
||||
context.data["layersData"]
|
||||
)
|
||||
# Add ftrack family
|
||||
instance.data["families"].append("ftrack")
|
||||
|
||||
|
|
@ -70,15 +73,8 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
if instance is None:
|
||||
continue
|
||||
|
||||
frame_start = context.data["frameStart"]
|
||||
frame_end = frame_start
|
||||
for layer in instance.data["layers"]:
|
||||
_frame_end = layer["frame_end"]
|
||||
if _frame_end > frame_end:
|
||||
frame_end = _frame_end
|
||||
|
||||
instance.data["frameStart"] = frame_start
|
||||
instance.data["frameEnd"] = frame_end
|
||||
instance.data["frameStart"] = context.data["frameStart"]
|
||||
instance.data["frameEnd"] = context.data["frameEnd"]
|
||||
|
||||
self.log.debug("Created instance: {}\n{}".format(
|
||||
instance, json.dumps(instance.data, indent=4)
|
||||
|
|
|
|||
|
|
@ -113,7 +113,8 @@ class CollectWorkfileData(pyblish.api.ContextPlugin):
|
|||
self.log.info("Collecting scene data from workfile")
|
||||
workfile_info_parts = lib.execute_george("tv_projectinfo").split(" ")
|
||||
|
||||
frame_start = int(workfile_info_parts.pop(-1))
|
||||
# Project frame start - not used
|
||||
workfile_info_parts.pop(-1)
|
||||
field_order = workfile_info_parts.pop(-1)
|
||||
frame_rate = float(workfile_info_parts.pop(-1))
|
||||
pixel_apsect = float(workfile_info_parts.pop(-1))
|
||||
|
|
@ -121,21 +122,14 @@ class CollectWorkfileData(pyblish.api.ContextPlugin):
|
|||
width = int(workfile_info_parts.pop(-1))
|
||||
workfile_path = " ".join(workfile_info_parts).replace("\"", "")
|
||||
|
||||
# TODO This is not porper way of getting last frame
|
||||
# - but don't know better
|
||||
last_frame = frame_start
|
||||
for layer in layers_data:
|
||||
frame_end = layer["frame_end"]
|
||||
if frame_end > last_frame:
|
||||
last_frame = frame_end
|
||||
|
||||
frame_start, frame_end = self.collect_clip_frames()
|
||||
scene_data = {
|
||||
"currentFile": workfile_path,
|
||||
"sceneWidth": width,
|
||||
"sceneHeight": height,
|
||||
"pixelAspect": pixel_apsect,
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": last_frame,
|
||||
"frameEnd": frame_end,
|
||||
"fps": frame_rate,
|
||||
"fieldOrder": field_order
|
||||
}
|
||||
|
|
@ -143,3 +137,21 @@ class CollectWorkfileData(pyblish.api.ContextPlugin):
|
|||
"Scene data: {}".format(json.dumps(scene_data, indent=4))
|
||||
)
|
||||
context.data.update(scene_data)
|
||||
|
||||
def collect_clip_frames(self):
|
||||
clip_info_str = lib.execute_george("tv_clipinfo")
|
||||
self.log.debug("Clip info: {}".format(clip_info_str))
|
||||
clip_info_items = clip_info_str.split(" ")
|
||||
# Color index - not used
|
||||
clip_info_items.pop(-1)
|
||||
clip_info_items.pop(-1)
|
||||
|
||||
mark_out = int(clip_info_items.pop(-1))
|
||||
frame_end = mark_out + 1
|
||||
clip_info_items.pop(-1)
|
||||
|
||||
mark_in = int(clip_info_items.pop(-1))
|
||||
frame_start = mark_in + 1
|
||||
clip_info_items.pop(-1)
|
||||
|
||||
return frame_start, frame_end
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import os
|
||||
import shutil
|
||||
import time
|
||||
import tempfile
|
||||
import multiprocessing
|
||||
|
||||
import pyblish.api
|
||||
from avalon.tvpaint import lib
|
||||
from pype.hosts.tvpaint.api.lib import composite_images
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
class ExtractSequence(pyblish.api.Extractor):
|
||||
|
|
@ -11,47 +15,6 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
hosts = ["tvpaint"]
|
||||
families = ["review", "renderPass", "renderLayer"]
|
||||
|
||||
save_mode_to_ext = {
|
||||
"avi": ".avi",
|
||||
"bmp": ".bmp",
|
||||
"cin": ".cin",
|
||||
"deep": ".dip",
|
||||
"dps": ".dps",
|
||||
"dpx": ".dpx",
|
||||
"flc": ".fli",
|
||||
"gif": ".gif",
|
||||
"ilbm": ".iff",
|
||||
"jpg": ".jpg",
|
||||
"jpeg": ".jpg",
|
||||
"pcx": ".pcx",
|
||||
"png": ".png",
|
||||
"psd": ".psd",
|
||||
"qt": ".qt",
|
||||
"rtv": ".rtv",
|
||||
"sun": ".ras",
|
||||
"tiff": ".tiff",
|
||||
"tga": ".tga",
|
||||
"vpb": ".vpb"
|
||||
}
|
||||
sequential_save_mode = {
|
||||
"bmp",
|
||||
"dpx",
|
||||
"ilbm",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"png",
|
||||
"sun",
|
||||
"tiff",
|
||||
"tga"
|
||||
}
|
||||
|
||||
default_save_mode = "\"PNG\""
|
||||
save_mode_for_family = {
|
||||
"review": "\"PNG\"",
|
||||
"renderPass": "\"PNG\"",
|
||||
"renderLayer": "\"PNG\"",
|
||||
}
|
||||
|
||||
def process(self, instance):
|
||||
self.log.info(
|
||||
"* Processing instance \"{}\"".format(instance.data["label"])
|
||||
|
|
@ -67,7 +30,7 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
layer_names = [str(layer["name"]) for layer in filtered_layers]
|
||||
if not layer_names:
|
||||
self.log.info(
|
||||
f"None of the layers from the instance"
|
||||
"None of the layers from the instance"
|
||||
" are visible. Extraction skipped."
|
||||
)
|
||||
return
|
||||
|
|
@ -80,34 +43,15 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
len(layer_names), joined_layer_names
|
||||
)
|
||||
)
|
||||
# This is plugin attribe cleanup method
|
||||
self._prepare_save_modes()
|
||||
|
||||
family_lowered = instance.data["family"].lower()
|
||||
save_mode = self.save_mode_for_family.get(
|
||||
family_lowered, self.default_save_mode
|
||||
)
|
||||
save_mode_type = self._get_save_mode_type(save_mode)
|
||||
|
||||
if not bool(save_mode_type in self.sequential_save_mode):
|
||||
raise AssertionError((
|
||||
"Plugin can export only sequential frame output"
|
||||
" but save mode for family \"{}\" is not for sequence > {} <"
|
||||
).format(instance.data["family"], save_mode))
|
||||
|
||||
frame_start = instance.data["frameStart"]
|
||||
frame_end = instance.data["frameEnd"]
|
||||
|
||||
filename_template = self._get_filename_template(
|
||||
save_mode_type, save_mode, frame_end
|
||||
)
|
||||
filename_template = self._get_filename_template(frame_end)
|
||||
ext = os.path.splitext(filename_template)[1].replace(".", "")
|
||||
|
||||
self.log.debug(
|
||||
"Using save mode > {} < and file template \"{}\"".format(
|
||||
save_mode, filename_template
|
||||
)
|
||||
)
|
||||
self.log.debug("Using file template \"{}\"".format(filename_template))
|
||||
|
||||
# Save to staging dir
|
||||
output_dir = instance.data.get("stagingDir")
|
||||
|
|
@ -120,34 +64,22 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
"Files will be rendered to folder: {}".format(output_dir)
|
||||
)
|
||||
|
||||
thumbnail_filename = "thumbnail"
|
||||
|
||||
# Render output
|
||||
output_files_by_frame = self.render(
|
||||
save_mode, filename_template, output_dir,
|
||||
filtered_layers, frame_start, frame_end, thumbnail_filename
|
||||
)
|
||||
thumbnail_fullpath = output_files_by_frame.pop(
|
||||
thumbnail_filename, None
|
||||
)
|
||||
|
||||
# Fill gaps in sequence
|
||||
self.fill_missing_frames(
|
||||
output_files_by_frame,
|
||||
frame_start,
|
||||
frame_end,
|
||||
filename_template
|
||||
)
|
||||
if instance.data["family"] == "review":
|
||||
repre_files, thumbnail_fullpath = self.render_review(
|
||||
filename_template, output_dir, frame_start, frame_end
|
||||
)
|
||||
else:
|
||||
# Render output
|
||||
repre_files, thumbnail_fullpath = self.render(
|
||||
filename_template, output_dir, frame_start, frame_end,
|
||||
filtered_layers
|
||||
)
|
||||
|
||||
# Fill tags and new families
|
||||
tags = []
|
||||
if family_lowered in ("review", "renderlayer"):
|
||||
tags.append("review")
|
||||
|
||||
repre_files = [
|
||||
os.path.basename(filepath)
|
||||
for filepath in output_files_by_frame.values()
|
||||
]
|
||||
# Sequence of one frame
|
||||
if len(repre_files) == 1:
|
||||
repre_files = repre_files[0]
|
||||
|
|
@ -157,8 +89,8 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
"ext": ext,
|
||||
"files": repre_files,
|
||||
"stagingDir": output_dir,
|
||||
"frameStart": frame_start + 1,
|
||||
"frameEnd": frame_end + 1,
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"tags": tags
|
||||
}
|
||||
self.log.debug("Creating new representation: {}".format(new_repre))
|
||||
|
|
@ -186,33 +118,7 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
}
|
||||
instance.data["representations"].append(thumbnail_repre)
|
||||
|
||||
def _prepare_save_modes(self):
|
||||
"""Lower family names in keys and skip empty values."""
|
||||
new_specifications = {}
|
||||
for key, value in self.save_mode_for_family.items():
|
||||
if value:
|
||||
new_specifications[key.lower()] = value
|
||||
else:
|
||||
self.log.warning((
|
||||
"Save mode for family \"{}\" has empty value."
|
||||
" The family will use default save mode: > {} <."
|
||||
).format(key, self.default_save_mode))
|
||||
self.save_mode_for_family = new_specifications
|
||||
|
||||
def _get_save_mode_type(self, save_mode):
|
||||
"""Extract type of save mode.
|
||||
|
||||
Helps to define output files extension.
|
||||
"""
|
||||
save_mode_type = (
|
||||
save_mode.lower()
|
||||
.split(" ")[0]
|
||||
.replace("\"", "")
|
||||
)
|
||||
self.log.debug("Save mode type is \"{}\"".format(save_mode_type))
|
||||
return save_mode_type
|
||||
|
||||
def _get_filename_template(self, save_mode_type, save_mode, frame_end):
|
||||
def _get_filename_template(self, frame_end):
|
||||
"""Get filetemplate for rendered files.
|
||||
|
||||
This is simple template contains `{frame}{ext}` for sequential outputs
|
||||
|
|
@ -220,145 +126,504 @@ class ExtractSequence(pyblish.api.Extractor):
|
|||
temporary folder so filename should not matter as integrator change
|
||||
them.
|
||||
"""
|
||||
ext = self.save_mode_to_ext.get(save_mode_type)
|
||||
if ext is None:
|
||||
raise AssertionError((
|
||||
"Couldn't find file extension for TVPaint's save mode: > {} <"
|
||||
).format(save_mode))
|
||||
|
||||
frame_padding = 4
|
||||
frame_end_str_len = len(str(frame_end))
|
||||
if frame_end_str_len > frame_padding:
|
||||
frame_padding = frame_end_str_len
|
||||
|
||||
return "{{frame:0>{}}}".format(frame_padding) + ext
|
||||
return "{{frame:0>{}}}".format(frame_padding) + ".png"
|
||||
|
||||
def render(
|
||||
self, save_mode, filename_template, output_dir, layers,
|
||||
first_frame, last_frame, thumbnail_filename
|
||||
def render_review(
|
||||
self, filename_template, output_dir, frame_start, frame_end
|
||||
):
|
||||
""" Export images from TVPaint.
|
||||
""" Export images from TVPaint using `tv_savesequence` command.
|
||||
|
||||
Args:
|
||||
save_mode (str): Argument for `tv_savemode` george script function.
|
||||
More about save mode in documentation.
|
||||
filename_template (str): Filename template of an output. Template
|
||||
should already contain extension. Template may contain only
|
||||
keyword argument `{frame}` or index argument (for same value).
|
||||
Extension in template must match `save_mode`.
|
||||
layers (list): List of layers to be exported.
|
||||
output_dir (str): Directory where files will be stored.
|
||||
first_frame (int): Starting frame from which export will begin.
|
||||
last_frame (int): On which frame export will end.
|
||||
|
||||
Retruns:
|
||||
dict: Mapping frame to output filepath.
|
||||
tuple: With 2 items first is list of filenames second is path to
|
||||
thumbnail.
|
||||
"""
|
||||
self.log.debug("Preparing data for rendering.")
|
||||
first_frame_filepath = os.path.join(
|
||||
output_dir,
|
||||
filename_template.format(frame=frame_start)
|
||||
)
|
||||
mark_in = frame_start - 1
|
||||
mark_out = frame_end - 1
|
||||
|
||||
# Add save mode arguments to function
|
||||
save_mode = "tv_SaveMode {}".format(save_mode)
|
||||
george_script_lines = [
|
||||
"tv_SaveMode \"PNG\"",
|
||||
"export_path = \"{}\"".format(
|
||||
first_frame_filepath.replace("\\", "/")
|
||||
),
|
||||
"tv_savesequence '\"'export_path'\"' {} {}".format(
|
||||
mark_in, mark_out
|
||||
)
|
||||
]
|
||||
lib.execute_george_through_file("\n".join(george_script_lines))
|
||||
|
||||
output = []
|
||||
first_frame_filepath = None
|
||||
for frame in range(frame_start, frame_end + 1):
|
||||
filename = filename_template.format(frame=frame)
|
||||
output.append(filename)
|
||||
if first_frame_filepath is None:
|
||||
first_frame_filepath = os.path.join(output_dir, filename)
|
||||
|
||||
thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg")
|
||||
if first_frame_filepath and os.path.exists(first_frame_filepath):
|
||||
source_img = Image.open(first_frame_filepath)
|
||||
thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255))
|
||||
thumbnail_obj.paste(source_img)
|
||||
thumbnail_obj.save(thumbnail_filepath)
|
||||
return output, thumbnail_filepath
|
||||
|
||||
def render(
|
||||
self, filename_template, output_dir, frame_start, frame_end, layers
|
||||
):
|
||||
""" Export images from TVPaint.
|
||||
|
||||
Args:
|
||||
filename_template (str): Filename template of an output. Template
|
||||
should already contain extension. Template may contain only
|
||||
keyword argument `{frame}` or index argument (for same value).
|
||||
Extension in template must match `save_mode`.
|
||||
output_dir (str): Directory where files will be stored.
|
||||
first_frame (int): Starting frame from which export will begin.
|
||||
last_frame (int): On which frame export will end.
|
||||
layers (list): List of layers to be exported.
|
||||
|
||||
Retruns:
|
||||
tuple: With 2 items first is list of filenames second is path to
|
||||
thumbnail.
|
||||
"""
|
||||
self.log.debug("Preparing data for rendering.")
|
||||
|
||||
# Map layers by position
|
||||
layers_by_position = {
|
||||
layer["position"]: layer
|
||||
for layer in layers
|
||||
}
|
||||
layers_by_position = {}
|
||||
layer_ids = []
|
||||
for layer in layers:
|
||||
position = layer["position"]
|
||||
layers_by_position[position] = layer
|
||||
|
||||
layer_ids.append(layer["layer_id"])
|
||||
|
||||
# Sort layer positions in reverse order
|
||||
sorted_positions = list(reversed(sorted(layers_by_position.keys())))
|
||||
if not sorted_positions:
|
||||
return
|
||||
|
||||
# Create temporary layer
|
||||
new_layer_id = lib.execute_george("tv_layercreate _tmp_layer")
|
||||
self.log.debug("Collecting pre/post behavior of individual layers.")
|
||||
behavior_by_layer_id = lib.get_layers_pre_post_behavior(layer_ids)
|
||||
|
||||
# Merge layers to temp layer
|
||||
george_script_lines = []
|
||||
# Set duplicated layer as current
|
||||
george_script_lines.append("tv_layerset {}".format(new_layer_id))
|
||||
mark_in_index = frame_start - 1
|
||||
mark_out_index = frame_end - 1
|
||||
|
||||
tmp_filename_template = "pos_{pos}." + filename_template
|
||||
|
||||
files_by_position = {}
|
||||
for position in sorted_positions:
|
||||
layer = layers_by_position[position]
|
||||
george_script_lines.append(
|
||||
"tv_layermerge {}".format(layer["layer_id"])
|
||||
behavior = behavior_by_layer_id[layer["layer_id"]]
|
||||
|
||||
files_by_frames = self._render_layer(
|
||||
layer,
|
||||
tmp_filename_template,
|
||||
output_dir,
|
||||
behavior,
|
||||
mark_in_index,
|
||||
mark_out_index
|
||||
)
|
||||
files_by_position[position] = files_by_frames
|
||||
|
||||
lib.execute_george_through_file("\n".join(george_script_lines))
|
||||
output_filepaths = self._composite_files(
|
||||
files_by_position,
|
||||
mark_in_index,
|
||||
mark_out_index,
|
||||
filename_template,
|
||||
output_dir
|
||||
)
|
||||
self._cleanup_tmp_files(files_by_position)
|
||||
|
||||
# Frames with keyframe
|
||||
thumbnail_src_filepath = None
|
||||
thumbnail_filepath = None
|
||||
if output_filepaths:
|
||||
thumbnail_src_filepath = tuple(sorted(output_filepaths))[0]
|
||||
|
||||
if thumbnail_src_filepath and os.path.exists(thumbnail_src_filepath):
|
||||
source_img = Image.open(thumbnail_src_filepath)
|
||||
thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg")
|
||||
thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255))
|
||||
thumbnail_obj.paste(source_img)
|
||||
thumbnail_obj.save(thumbnail_filepath)
|
||||
|
||||
repre_files = [
|
||||
os.path.basename(path)
|
||||
for path in output_filepaths
|
||||
]
|
||||
return repre_files, thumbnail_filepath
|
||||
|
||||
def _render_layer(
|
||||
self,
|
||||
layer,
|
||||
tmp_filename_template,
|
||||
output_dir,
|
||||
behavior,
|
||||
mark_in_index,
|
||||
mark_out_index
|
||||
):
|
||||
layer_id = layer["layer_id"]
|
||||
frame_start_index = layer["frame_start"]
|
||||
frame_end_index = layer["frame_end"]
|
||||
exposure_frames = lib.get_exposure_frames(
|
||||
new_layer_id, first_frame, last_frame
|
||||
layer_id, frame_start_index, frame_end_index
|
||||
)
|
||||
|
||||
# TODO what if there is not exposue frames?
|
||||
# - this force to have first frame all the time
|
||||
if first_frame not in exposure_frames:
|
||||
exposure_frames.insert(0, first_frame)
|
||||
if frame_start_index not in exposure_frames:
|
||||
exposure_frames.append(frame_start_index)
|
||||
|
||||
# Restart george script lines
|
||||
george_script_lines = []
|
||||
george_script_lines.append(save_mode)
|
||||
layer_files_by_frame = {}
|
||||
george_script_lines = [
|
||||
"tv_SaveMode \"PNG\""
|
||||
]
|
||||
layer_position = layer["position"]
|
||||
|
||||
all_output_files = {}
|
||||
for frame in exposure_frames:
|
||||
filename = filename_template.format(frame, frame=frame)
|
||||
for frame_idx in exposure_frames:
|
||||
filename = tmp_filename_template.format(
|
||||
pos=layer_position,
|
||||
frame=frame_idx
|
||||
)
|
||||
dst_path = "/".join([output_dir, filename])
|
||||
all_output_files[frame] = os.path.normpath(dst_path)
|
||||
layer_files_by_frame[frame_idx] = os.path.normpath(dst_path)
|
||||
|
||||
# Go to frame
|
||||
george_script_lines.append("tv_layerImage {}".format(frame))
|
||||
george_script_lines.append("tv_layerImage {}".format(frame_idx))
|
||||
# Store image to output
|
||||
george_script_lines.append("tv_saveimage \"{}\"".format(dst_path))
|
||||
|
||||
# Export thumbnail
|
||||
if thumbnail_filename:
|
||||
basename, ext = os.path.splitext(thumbnail_filename)
|
||||
if not ext:
|
||||
ext = ".jpg"
|
||||
thumbnail_fullpath = "/".join([output_dir, basename + ext])
|
||||
all_output_files[thumbnail_filename] = thumbnail_fullpath
|
||||
# Force save mode to png for thumbnail
|
||||
george_script_lines.append("tv_SaveMode \"JPG\"")
|
||||
# Go to frame
|
||||
george_script_lines.append("tv_layerImage {}".format(first_frame))
|
||||
# Store image to output
|
||||
george_script_lines.append(
|
||||
"tv_saveimage \"{}\"".format(thumbnail_fullpath)
|
||||
)
|
||||
|
||||
# Delete temporary layer
|
||||
george_script_lines.append("tv_layerkill {}".format(new_layer_id))
|
||||
|
||||
self.log.debug("Rendering Exposure frames {} of layer {} ({})".format(
|
||||
str(exposure_frames), layer_id, layer["name"]
|
||||
))
|
||||
# Let TVPaint render layer's image
|
||||
lib.execute_george_through_file("\n".join(george_script_lines))
|
||||
|
||||
return all_output_files
|
||||
# Fill frames between `frame_start_index` and `frame_end_index`
|
||||
self.log.debug((
|
||||
"Filling frames between first and last frame of layer ({} - {})."
|
||||
).format(frame_start_index + 1, frame_end_index + 1))
|
||||
|
||||
def fill_missing_frames(
|
||||
self, filepaths_by_frame, first_frame, last_frame, filename_template
|
||||
):
|
||||
"""Fill not rendered frames with previous frame.
|
||||
|
||||
Extractor is rendering only frames with keyframes (exposure frames) to
|
||||
get output faster which means there may be gaps between frames.
|
||||
This function fill the missing frames.
|
||||
"""
|
||||
output_dir = None
|
||||
previous_frame_filepath = None
|
||||
for frame in range(first_frame, last_frame + 1):
|
||||
if frame in filepaths_by_frame:
|
||||
previous_frame_filepath = filepaths_by_frame[frame]
|
||||
_debug_filled_frames = []
|
||||
prev_filepath = None
|
||||
for frame_idx in range(frame_start_index, frame_end_index + 1):
|
||||
if frame_idx in layer_files_by_frame:
|
||||
prev_filepath = layer_files_by_frame[frame_idx]
|
||||
continue
|
||||
|
||||
elif previous_frame_filepath is None:
|
||||
self.log.warning(
|
||||
"No frames to fill. Seems like nothing was exported."
|
||||
if prev_filepath is None:
|
||||
raise ValueError("BUG: First frame of layer was not rendered!")
|
||||
_debug_filled_frames.append(frame_idx)
|
||||
filename = tmp_filename_template.format(
|
||||
pos=layer_position,
|
||||
frame=frame_idx
|
||||
)
|
||||
new_filepath = "/".join([output_dir, filename])
|
||||
self._copy_image(prev_filepath, new_filepath)
|
||||
layer_files_by_frame[frame_idx] = new_filepath
|
||||
|
||||
self.log.debug("Filled frames {}".format(str(_debug_filled_frames)))
|
||||
|
||||
# Fill frames by pre/post behavior of layer
|
||||
pre_behavior = behavior["pre"]
|
||||
post_behavior = behavior["post"]
|
||||
self.log.debug((
|
||||
"Completing image sequence of layer by pre/post behavior."
|
||||
" PRE: {} | POST: {}"
|
||||
).format(pre_behavior, post_behavior))
|
||||
|
||||
# Pre behavior
|
||||
self._fill_frame_by_pre_behavior(
|
||||
layer,
|
||||
pre_behavior,
|
||||
mark_in_index,
|
||||
layer_files_by_frame,
|
||||
tmp_filename_template,
|
||||
output_dir
|
||||
)
|
||||
self._fill_frame_by_post_behavior(
|
||||
layer,
|
||||
post_behavior,
|
||||
mark_out_index,
|
||||
layer_files_by_frame,
|
||||
tmp_filename_template,
|
||||
output_dir
|
||||
)
|
||||
return layer_files_by_frame
|
||||
|
||||
def _fill_frame_by_pre_behavior(
|
||||
self,
|
||||
layer,
|
||||
pre_behavior,
|
||||
mark_in_index,
|
||||
layer_files_by_frame,
|
||||
filename_template,
|
||||
output_dir
|
||||
):
|
||||
layer_position = layer["position"]
|
||||
frame_start_index = layer["frame_start"]
|
||||
frame_end_index = layer["frame_end"]
|
||||
frame_count = frame_end_index - frame_start_index + 1
|
||||
if mark_in_index >= frame_start_index:
|
||||
self.log.debug((
|
||||
"Skipping pre-behavior."
|
||||
" All frames after Mark In are rendered."
|
||||
))
|
||||
return
|
||||
|
||||
if pre_behavior == "none":
|
||||
# Empty frames are handled during `_composite_files`
|
||||
pass
|
||||
|
||||
elif pre_behavior == "hold":
|
||||
# Keep first frame for whole time
|
||||
eq_frame_filepath = layer_files_by_frame[frame_start_index]
|
||||
for frame_idx in range(mark_in_index, frame_start_index):
|
||||
filename = filename_template.format(
|
||||
pos=layer_position,
|
||||
frame=frame_idx
|
||||
)
|
||||
new_filepath = "/".join([output_dir, filename])
|
||||
self._copy_image(eq_frame_filepath, new_filepath)
|
||||
layer_files_by_frame[frame_idx] = new_filepath
|
||||
|
||||
elif pre_behavior == "loop":
|
||||
# Loop backwards from last frame of layer
|
||||
for frame_idx in reversed(range(mark_in_index, frame_start_index)):
|
||||
eq_frame_idx_offset = (
|
||||
(frame_end_index - frame_idx) % frame_count
|
||||
)
|
||||
eq_frame_idx = frame_end_index - eq_frame_idx_offset
|
||||
eq_frame_filepath = layer_files_by_frame[eq_frame_idx]
|
||||
|
||||
filename = filename_template.format(
|
||||
pos=layer_position,
|
||||
frame=frame_idx
|
||||
)
|
||||
new_filepath = "/".join([output_dir, filename])
|
||||
self._copy_image(eq_frame_filepath, new_filepath)
|
||||
layer_files_by_frame[frame_idx] = new_filepath
|
||||
|
||||
elif pre_behavior == "pingpong":
|
||||
half_seq_len = frame_count - 1
|
||||
seq_len = half_seq_len * 2
|
||||
for frame_idx in reversed(range(mark_in_index, frame_start_index)):
|
||||
eq_frame_idx_offset = (frame_start_index - frame_idx) % seq_len
|
||||
if eq_frame_idx_offset > half_seq_len:
|
||||
eq_frame_idx_offset = (seq_len - eq_frame_idx_offset)
|
||||
eq_frame_idx = frame_start_index + eq_frame_idx_offset
|
||||
|
||||
eq_frame_filepath = layer_files_by_frame[eq_frame_idx]
|
||||
|
||||
filename = filename_template.format(
|
||||
pos=layer_position,
|
||||
frame=frame_idx
|
||||
)
|
||||
new_filepath = "/".join([output_dir, filename])
|
||||
self._copy_image(eq_frame_filepath, new_filepath)
|
||||
layer_files_by_frame[frame_idx] = new_filepath
|
||||
|
||||
def _fill_frame_by_post_behavior(
|
||||
self,
|
||||
layer,
|
||||
post_behavior,
|
||||
mark_out_index,
|
||||
layer_files_by_frame,
|
||||
filename_template,
|
||||
output_dir
|
||||
):
|
||||
layer_position = layer["position"]
|
||||
frame_start_index = layer["frame_start"]
|
||||
frame_end_index = layer["frame_end"]
|
||||
frame_count = frame_end_index - frame_start_index + 1
|
||||
if mark_out_index <= frame_end_index:
|
||||
self.log.debug((
|
||||
"Skipping post-behavior."
|
||||
" All frames up to Mark Out are rendered."
|
||||
))
|
||||
return
|
||||
|
||||
if post_behavior == "none":
|
||||
# Empty frames are handled during `_composite_files`
|
||||
pass
|
||||
|
||||
elif post_behavior == "hold":
|
||||
# Keep first frame for whole time
|
||||
eq_frame_filepath = layer_files_by_frame[frame_end_index]
|
||||
for frame_idx in range(frame_end_index + 1, mark_out_index + 1):
|
||||
filename = filename_template.format(
|
||||
pos=layer_position,
|
||||
frame=frame_idx
|
||||
)
|
||||
new_filepath = "/".join([output_dir, filename])
|
||||
self._copy_image(eq_frame_filepath, new_filepath)
|
||||
layer_files_by_frame[frame_idx] = new_filepath
|
||||
|
||||
elif post_behavior == "loop":
|
||||
# Loop backwards from last frame of layer
|
||||
for frame_idx in range(frame_end_index + 1, mark_out_index + 1):
|
||||
eq_frame_idx = frame_idx % frame_count
|
||||
eq_frame_filepath = layer_files_by_frame[eq_frame_idx]
|
||||
|
||||
filename = filename_template.format(
|
||||
pos=layer_position,
|
||||
frame=frame_idx
|
||||
)
|
||||
new_filepath = "/".join([output_dir, filename])
|
||||
self._copy_image(eq_frame_filepath, new_filepath)
|
||||
layer_files_by_frame[frame_idx] = new_filepath
|
||||
|
||||
elif post_behavior == "pingpong":
|
||||
half_seq_len = frame_count - 1
|
||||
seq_len = half_seq_len * 2
|
||||
for frame_idx in range(frame_end_index + 1, mark_out_index + 1):
|
||||
eq_frame_idx_offset = (frame_idx - frame_end_index) % seq_len
|
||||
if eq_frame_idx_offset > half_seq_len:
|
||||
eq_frame_idx_offset = seq_len - eq_frame_idx_offset
|
||||
eq_frame_idx = frame_end_index - eq_frame_idx_offset
|
||||
|
||||
eq_frame_filepath = layer_files_by_frame[eq_frame_idx]
|
||||
|
||||
filename = filename_template.format(
|
||||
pos=layer_position,
|
||||
frame=frame_idx
|
||||
)
|
||||
new_filepath = "/".join([output_dir, filename])
|
||||
self._copy_image(eq_frame_filepath, new_filepath)
|
||||
layer_files_by_frame[frame_idx] = new_filepath
|
||||
|
||||
def _composite_files(
|
||||
self, files_by_position, frame_start, frame_end,
|
||||
filename_template, output_dir
|
||||
):
|
||||
"""Composite frames when more that one layer was exported.
|
||||
|
||||
This method is used when more than one layer is rendered out so and
|
||||
output should be composition of each frame of rendered layers.
|
||||
Missing frames are filled with transparent images.
|
||||
"""
|
||||
self.log.debug("Preparing files for compisiting.")
|
||||
# Prepare paths to images by frames into list where are stored
|
||||
# in order of compositing.
|
||||
images_by_frame = {}
|
||||
for frame_idx in range(frame_start, frame_end + 1):
|
||||
images_by_frame[frame_idx] = []
|
||||
for position in sorted(files_by_position.keys(), reverse=True):
|
||||
position_data = files_by_position[position]
|
||||
if frame_idx in position_data:
|
||||
filepath = position_data[frame_idx]
|
||||
images_by_frame[frame_idx].append(filepath)
|
||||
|
||||
process_count = os.cpu_count()
|
||||
if process_count > 1:
|
||||
process_count -= 1
|
||||
|
||||
processes = {}
|
||||
output_filepaths = []
|
||||
missing_frame_paths = []
|
||||
random_frame_path = None
|
||||
for frame_idx in sorted(images_by_frame.keys()):
|
||||
image_filepaths = images_by_frame[frame_idx]
|
||||
output_filename = filename_template.format(frame=frame_idx + 1)
|
||||
output_filepath = os.path.join(output_dir, output_filename)
|
||||
output_filepaths.append(output_filepath)
|
||||
|
||||
# Store information about missing frame and skip
|
||||
if not image_filepaths:
|
||||
missing_frame_paths.append(output_filepath)
|
||||
continue
|
||||
|
||||
# Just rename the file if is no need of compositing
|
||||
if len(image_filepaths) == 1:
|
||||
os.rename(image_filepaths[0], output_filepath)
|
||||
|
||||
# Prepare process for compositing of images
|
||||
else:
|
||||
processes[frame_idx] = multiprocessing.Process(
|
||||
target=composite_images,
|
||||
args=(image_filepaths, output_filepath)
|
||||
)
|
||||
|
||||
# Store path of random output image that will 100% exist after all
|
||||
# multiprocessing as mockup for missing frames
|
||||
if random_frame_path is None:
|
||||
random_frame_path = output_filepath
|
||||
|
||||
self.log.info(
|
||||
"Running {} compositing processes - this mey take a while.".format(
|
||||
len(processes)
|
||||
)
|
||||
)
|
||||
# Wait until all compositing processes are done
|
||||
running_processes = {}
|
||||
while True:
|
||||
for idx in tuple(running_processes.keys()):
|
||||
process = running_processes[idx]
|
||||
if not process.is_alive():
|
||||
running_processes.pop(idx).join()
|
||||
|
||||
if processes and len(running_processes) != process_count:
|
||||
indexes = list(processes.keys())
|
||||
for _ in range(process_count - len(running_processes)):
|
||||
if not indexes:
|
||||
break
|
||||
idx = indexes.pop(0)
|
||||
running_processes[idx] = processes.pop(idx)
|
||||
running_processes[idx].start()
|
||||
|
||||
if not running_processes and not processes:
|
||||
break
|
||||
|
||||
if output_dir is None:
|
||||
output_dir = os.path.dirname(previous_frame_filepath)
|
||||
time.sleep(0.01)
|
||||
|
||||
filename = filename_template.format(frame=frame)
|
||||
space_filepath = os.path.normpath(
|
||||
os.path.join(output_dir, filename)
|
||||
self.log.debug(
|
||||
"Creating transparent images for frames without render {}.".format(
|
||||
str(missing_frame_paths)
|
||||
)
|
||||
filepaths_by_frame[frame] = space_filepath
|
||||
shutil.copy(previous_frame_filepath, space_filepath)
|
||||
)
|
||||
# Fill the sequence with transparent frames
|
||||
transparent_filepath = None
|
||||
for filepath in missing_frame_paths:
|
||||
if transparent_filepath is None:
|
||||
img_obj = Image.open(random_frame_path)
|
||||
painter = ImageDraw.Draw(img_obj)
|
||||
painter.rectangle((0, 0, *img_obj.size), fill=(0, 0, 0, 0))
|
||||
img_obj.save(filepath)
|
||||
transparent_filepath = filepath
|
||||
else:
|
||||
self._copy_image(transparent_filepath, filepath)
|
||||
return output_filepaths
|
||||
|
||||
def _cleanup_tmp_files(self, files_by_position):
|
||||
"""Remove temporary files that were used for compositing."""
|
||||
for data in files_by_position.values():
|
||||
for filepath in data.values():
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
|
||||
def _copy_image(self, src_path, dst_path):
|
||||
"""Create a copy of an image.
|
||||
|
||||
This was added to be able easier change copy method.
|
||||
"""
|
||||
# Create hardlink of image instead of copying if possible
|
||||
if hasattr(os, "link"):
|
||||
os.link(src_path, dst_path)
|
||||
else:
|
||||
shutil.copy(src_path, dst_path)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import pyblish.api
|
||||
|
||||
|
||||
class ValidateLayersVisiblity(pyblish.api.InstancePlugin):
|
||||
"""Validate existence of renderPass layers."""
|
||||
|
||||
label = "Validate Layers Visibility"
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["review", "renderPass", "renderLayer"]
|
||||
|
||||
def process(self, instance):
|
||||
for layer in instance.data["layers"]:
|
||||
if layer["visible"]:
|
||||
return
|
||||
|
||||
raise AssertionError("All layers of instance are not visible.")
|
||||
87
pype/lib/pype_info.py
Normal file
87
pype/lib/pype_info.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import os
|
||||
import json
|
||||
import datetime
|
||||
import platform
|
||||
import getpass
|
||||
import socket
|
||||
|
||||
import pype.version
|
||||
from pype.settings.lib import get_local_settings
|
||||
from .execute import get_pype_execute_args
|
||||
from .local_settings import get_local_site_id
|
||||
|
||||
|
||||
def get_pype_version():
|
||||
"""Version of pype that is currently used."""
|
||||
return pype.version.__version__
|
||||
|
||||
|
||||
def get_pype_info():
|
||||
"""Information about currently used Pype process."""
|
||||
executable_args = get_pype_execute_args()
|
||||
if len(executable_args) == 1:
|
||||
version_type = "build"
|
||||
else:
|
||||
version_type = "code"
|
||||
|
||||
return {
|
||||
"version": get_pype_version(),
|
||||
"version_type": version_type,
|
||||
"executable": executable_args[-1],
|
||||
"pype_root": os.environ["PYPE_ROOT"],
|
||||
"mongo_url": os.environ["PYPE_MONGO"]
|
||||
}
|
||||
|
||||
|
||||
def get_workstation_info():
|
||||
"""Basic information about workstation."""
|
||||
host_name = socket.gethostname()
|
||||
try:
|
||||
host_ip = socket.gethostbyname(host_name)
|
||||
except socket.gaierror:
|
||||
host_ip = "127.0.0.1"
|
||||
|
||||
return {
|
||||
"hostname": host_name,
|
||||
"hostip": host_ip,
|
||||
"username": getpass.getuser(),
|
||||
"system_name": platform.system(),
|
||||
"local_id": get_local_site_id()
|
||||
}
|
||||
|
||||
|
||||
def get_all_current_info():
|
||||
"""All information about current process in one dictionary."""
|
||||
return {
|
||||
"pype": get_pype_info(),
|
||||
"workstation": get_workstation_info(),
|
||||
"env": os.environ.copy(),
|
||||
"local_settings": get_local_settings()
|
||||
}
|
||||
|
||||
|
||||
def extract_pype_info_to_file(dirpath):
|
||||
"""Extract all current info to a file.
|
||||
|
||||
It is possible to define onpy directory path. Filename is concatenated with
|
||||
pype version, workstation site id and timestamp.
|
||||
|
||||
Args:
|
||||
dirpath (str): Path to directory where file will be stored.
|
||||
|
||||
Returns:
|
||||
filepath (str): Full path to file where data were extracted.
|
||||
"""
|
||||
filename = "{}_{}_{}.json".format(
|
||||
get_pype_version(),
|
||||
get_local_site_id(),
|
||||
datetime.datetime.now().strftime("%y%m%d%H%M%S")
|
||||
)
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
data = get_all_current_info()
|
||||
if not os.path.exists(dirpath):
|
||||
os.makedirs(dirpath)
|
||||
|
||||
with open(filepath, "w") as file_stream:
|
||||
json.dump(data, file_stream, indent=4)
|
||||
return filepath
|
||||
|
|
@ -108,6 +108,7 @@ class ITrayModule:
|
|||
would do nothing.
|
||||
"""
|
||||
tray_initialized = False
|
||||
_tray_manager = None
|
||||
|
||||
@abstractmethod
|
||||
def tray_init(self):
|
||||
|
|
@ -138,6 +139,20 @@ class ITrayModule:
|
|||
"""
|
||||
pass
|
||||
|
||||
def show_tray_message(self, title, message, icon=None, msecs=None):
|
||||
"""Show tray message.
|
||||
|
||||
Args:
|
||||
title (str): Title of message.
|
||||
message (str): Content of message.
|
||||
icon (QSystemTrayIcon.MessageIcon): Message's icon. Default is
|
||||
Information icon, may differ by Qt version.
|
||||
msecs (int): Duration of message visibility in miliseconds.
|
||||
Default is 10000 msecs, may differ by Qt version.
|
||||
"""
|
||||
if self._tray_manager:
|
||||
self._tray_manager.show_tray_message(title, message, icon, msecs)
|
||||
|
||||
|
||||
class ITrayAction(ITrayModule):
|
||||
"""Implementation of Tray action.
|
||||
|
|
@ -638,8 +653,10 @@ class TrayModulesManager(ModulesManager):
|
|||
self.modules_by_id = {}
|
||||
self.modules_by_name = {}
|
||||
self._report = {}
|
||||
self.tray_manager = None
|
||||
|
||||
def initialize(self, tray_menu):
|
||||
def initialize(self, tray_manager, tray_menu):
|
||||
self.tray_manager = tray_manager
|
||||
self.initialize_modules()
|
||||
self.tray_init()
|
||||
self.connect_modules()
|
||||
|
|
@ -658,6 +675,7 @@ class TrayModulesManager(ModulesManager):
|
|||
prev_start_time = time_start
|
||||
for module in self.get_enabled_tray_modules():
|
||||
try:
|
||||
module._tray_manager = self.tray_manager
|
||||
module.tray_init()
|
||||
module.tray_initialized = True
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,36 @@
|
|||
import sys
|
||||
import collections
|
||||
import six
|
||||
import pyblish.api
|
||||
from avalon import io
|
||||
|
||||
try:
|
||||
from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_AUTO_SYNC
|
||||
except Exception:
|
||||
CUST_ATTR_AUTO_SYNC = "avalon_auto_sync"
|
||||
# Copy of constant `pype.modules.ftrack.lib.avalon_sync.CUST_ATTR_AUTO_SYNC`
|
||||
CUST_ATTR_AUTO_SYNC = "avalon_auto_sync"
|
||||
|
||||
|
||||
# Copy of `get_pype_attr` from pype.modules.ftrack.lib
|
||||
def get_pype_attr(session, split_hierarchical=True):
|
||||
custom_attributes = []
|
||||
hier_custom_attributes = []
|
||||
# TODO remove deprecated "avalon" group from query
|
||||
cust_attrs_query = (
|
||||
"select id, entity_type, object_type_id, is_hierarchical, default"
|
||||
" from CustomAttributeConfiguration"
|
||||
" where group.name in (\"avalon\", \"pype\")"
|
||||
)
|
||||
all_avalon_attr = session.query(cust_attrs_query).all()
|
||||
for cust_attr in all_avalon_attr:
|
||||
if split_hierarchical and cust_attr["is_hierarchical"]:
|
||||
hier_custom_attributes.append(cust_attr)
|
||||
continue
|
||||
|
||||
custom_attributes.append(cust_attr)
|
||||
|
||||
if split_hierarchical:
|
||||
# return tuple
|
||||
return custom_attributes, hier_custom_attributes
|
||||
|
||||
return custom_attributes
|
||||
|
||||
|
||||
class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
|
||||
|
|
@ -36,7 +60,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
|
|||
order = pyblish.api.IntegratorOrder - 0.04
|
||||
label = 'Integrate Hierarchy To Ftrack'
|
||||
families = ["shot"]
|
||||
hosts = ["hiero", "resolve"]
|
||||
hosts = ["hiero", "resolve", "standalonepublisher"]
|
||||
optional = False
|
||||
|
||||
def process(self, context):
|
||||
|
|
@ -74,6 +98,15 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
|
|||
self.auto_sync_on(project)
|
||||
|
||||
def import_to_ftrack(self, input_data, parent=None):
|
||||
# Prequery hiearchical custom attributes
|
||||
hier_custom_attributes = get_pype_attr(self.session)[1]
|
||||
hier_attr_by_key = {
|
||||
attr["key"]: attr
|
||||
for attr in hier_custom_attributes
|
||||
}
|
||||
# Get ftrack api module (as they are different per python version)
|
||||
ftrack_api = self.context.data["ftrackPythonModule"]
|
||||
|
||||
for entity_name in input_data:
|
||||
entity_data = input_data[entity_name]
|
||||
entity_type = entity_data['entity_type']
|
||||
|
|
@ -116,12 +149,33 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
|
|||
i for i in self.context if i.data['asset'] in entity['name']
|
||||
]
|
||||
for key in custom_attributes:
|
||||
assert (key in entity['custom_attributes']), (
|
||||
'Missing custom attribute key: `{0}` in attrs: '
|
||||
'`{1}`'.format(key, entity['custom_attributes'].keys())
|
||||
)
|
||||
hier_attr = hier_attr_by_key.get(key)
|
||||
# Use simple method if key is not hierarchical
|
||||
if not hier_attr:
|
||||
assert (key in entity['custom_attributes']), (
|
||||
'Missing custom attribute key: `{0}` in attrs: '
|
||||
'`{1}`'.format(key, entity['custom_attributes'].keys())
|
||||
)
|
||||
|
||||
entity['custom_attributes'][key] = custom_attributes[key]
|
||||
entity['custom_attributes'][key] = custom_attributes[key]
|
||||
|
||||
else:
|
||||
# Use ftrack operations method to set hiearchical
|
||||
# attribute value.
|
||||
# - this is because there may be non hiearchical custom
|
||||
# attributes with different properties
|
||||
entity_key = collections.OrderedDict()
|
||||
entity_key["configuration_id"] = hier_attr["id"]
|
||||
entity_key["entity_id"] = entity["id"]
|
||||
self.session.recorded_operations.push(
|
||||
ftrack_api.operation.UpdateEntityOperation(
|
||||
"ContextCustomAttributeValue",
|
||||
entity_key,
|
||||
"value",
|
||||
ftrack_api.symbol.NOT_SET,
|
||||
custom_attributes[key]
|
||||
)
|
||||
)
|
||||
|
||||
for instance in instances:
|
||||
instance.data['ftrackEntity'] = entity
|
||||
|
|
|
|||
|
|
@ -1,331 +0,0 @@
|
|||
import sys
|
||||
import six
|
||||
import collections
|
||||
import pyblish.api
|
||||
from avalon import io
|
||||
|
||||
from pype.modules.ftrack.lib.avalon_sync import (
|
||||
CUST_ATTR_AUTO_SYNC,
|
||||
get_pype_attr
|
||||
)
|
||||
|
||||
|
||||
class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
|
||||
"""
|
||||
Create entities in ftrack based on collected data from premiere
|
||||
Example of entry data:
|
||||
{
|
||||
"ProjectXS": {
|
||||
"entity_type": "Project",
|
||||
"custom_attributes": {
|
||||
"fps": 24,...
|
||||
},
|
||||
"tasks": [
|
||||
"Compositing",
|
||||
"Lighting",... *task must exist as task type in project schema*
|
||||
],
|
||||
"childs": {
|
||||
"sq01": {
|
||||
"entity_type": "Sequence",
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
order = pyblish.api.IntegratorOrder - 0.04
|
||||
label = 'Integrate Hierarchy To Ftrack'
|
||||
families = ["shot"]
|
||||
hosts = ["standalonepublisher"]
|
||||
optional = False
|
||||
|
||||
def process(self, context):
|
||||
self.context = context
|
||||
if "hierarchyContext" not in self.context.data:
|
||||
return
|
||||
|
||||
hierarchy_context = self.context.data["hierarchyContext"]
|
||||
|
||||
self.session = self.context.data["ftrackSession"]
|
||||
project_name = self.context.data["projectEntity"]["name"]
|
||||
query = 'Project where full_name is "{}"'.format(project_name)
|
||||
project = self.session.query(query).one()
|
||||
auto_sync_state = project[
|
||||
"custom_attributes"][CUST_ATTR_AUTO_SYNC]
|
||||
|
||||
if not io.Session:
|
||||
io.install()
|
||||
|
||||
self.ft_project = None
|
||||
|
||||
input_data = hierarchy_context
|
||||
|
||||
# disable termporarily ftrack project's autosyncing
|
||||
if auto_sync_state:
|
||||
self.auto_sync_off(project)
|
||||
|
||||
try:
|
||||
# import ftrack hierarchy
|
||||
self.import_to_ftrack(input_data)
|
||||
except Exception:
|
||||
raise
|
||||
finally:
|
||||
if auto_sync_state:
|
||||
self.auto_sync_on(project)
|
||||
|
||||
def import_to_ftrack(self, input_data, parent=None):
|
||||
# Prequery hiearchical custom attributes
|
||||
hier_custom_attributes = get_pype_attr(self.session)[1]
|
||||
hier_attr_by_key = {
|
||||
attr["key"]: attr
|
||||
for attr in hier_custom_attributes
|
||||
}
|
||||
# Get ftrack api module (as they are different per python version)
|
||||
ftrack_api = self.context.data["ftrackPythonModule"]
|
||||
|
||||
for entity_name in input_data:
|
||||
entity_data = input_data[entity_name]
|
||||
entity_type = entity_data['entity_type']
|
||||
self.log.debug(entity_data)
|
||||
self.log.debug(entity_type)
|
||||
|
||||
if entity_type.lower() == 'project':
|
||||
query = 'Project where full_name is "{}"'.format(entity_name)
|
||||
entity = self.session.query(query).one()
|
||||
self.ft_project = entity
|
||||
self.task_types = self.get_all_task_types(entity)
|
||||
|
||||
elif self.ft_project is None or parent is None:
|
||||
raise AssertionError(
|
||||
"Collected items are not in right order!"
|
||||
)
|
||||
|
||||
# try to find if entity already exists
|
||||
else:
|
||||
query = (
|
||||
'TypedContext where name is "{0}" and '
|
||||
'project_id is "{1}"'
|
||||
).format(entity_name, self.ft_project["id"])
|
||||
try:
|
||||
entity = self.session.query(query).one()
|
||||
except Exception:
|
||||
entity = None
|
||||
|
||||
# Create entity if not exists
|
||||
if entity is None:
|
||||
entity = self.create_entity(
|
||||
name=entity_name,
|
||||
type=entity_type,
|
||||
parent=parent
|
||||
)
|
||||
# self.log.info('entity: {}'.format(dict(entity)))
|
||||
# CUSTOM ATTRIBUTES
|
||||
custom_attributes = entity_data.get('custom_attributes', [])
|
||||
instances = [
|
||||
i for i in self.context if i.data['asset'] in entity['name']
|
||||
]
|
||||
for key in custom_attributes:
|
||||
hier_attr = hier_attr_by_key.get(key)
|
||||
# Use simple method if key is not hierarchical
|
||||
if not hier_attr:
|
||||
assert (key in entity['custom_attributes']), (
|
||||
'Missing custom attribute key: `{0}` in attrs: '
|
||||
'`{1}`'.format(key, entity['custom_attributes'].keys())
|
||||
)
|
||||
|
||||
entity['custom_attributes'][key] = custom_attributes[key]
|
||||
|
||||
else:
|
||||
# Use ftrack operations method to set hiearchical
|
||||
# attribute value.
|
||||
# - this is because there may be non hiearchical custom
|
||||
# attributes with different properties
|
||||
entity_key = collections.OrderedDict({
|
||||
"configuration_id": hier_attr["id"],
|
||||
"entity_id": entity["id"]
|
||||
})
|
||||
self.session.recorded_operations.push(
|
||||
ftrack_api.operation.UpdateEntityOperation(
|
||||
"ContextCustomAttributeValue",
|
||||
entity_key,
|
||||
"value",
|
||||
ftrack_api.symbol.NOT_SET,
|
||||
custom_attributes[key]
|
||||
)
|
||||
)
|
||||
|
||||
for instance in instances:
|
||||
instance.data['ftrackEntity'] = entity
|
||||
|
||||
try:
|
||||
self.session.commit()
|
||||
except Exception:
|
||||
tp, value, tb = sys.exc_info()
|
||||
self.session.rollback()
|
||||
self.session._configure_locations()
|
||||
six.reraise(tp, value, tb)
|
||||
|
||||
# TASKS
|
||||
tasks = entity_data.get('tasks', [])
|
||||
existing_tasks = []
|
||||
tasks_to_create = []
|
||||
for child in entity['children']:
|
||||
if child.entity_type.lower() == 'task':
|
||||
existing_tasks.append(child['name'].lower())
|
||||
# existing_tasks.append(child['type']['name'])
|
||||
|
||||
for task_name in tasks:
|
||||
task_type = tasks[task_name]["type"]
|
||||
if task_name.lower() in existing_tasks:
|
||||
print("Task {} already exists".format(task_name))
|
||||
continue
|
||||
tasks_to_create.append((task_name, task_type))
|
||||
|
||||
for task_name, task_type in tasks_to_create:
|
||||
self.create_task(
|
||||
name=task_name,
|
||||
task_type=task_type,
|
||||
parent=entity
|
||||
)
|
||||
try:
|
||||
self.session.commit()
|
||||
except Exception:
|
||||
tp, value, tb = sys.exc_info()
|
||||
self.session.rollback()
|
||||
self.session._configure_locations()
|
||||
six.reraise(tp, value, tb)
|
||||
|
||||
# Incoming links.
|
||||
self.create_links(entity_data, entity)
|
||||
try:
|
||||
self.session.commit()
|
||||
except Exception:
|
||||
tp, value, tb = sys.exc_info()
|
||||
self.session.rollback()
|
||||
self.session._configure_locations()
|
||||
six.reraise(tp, value, tb)
|
||||
|
||||
# Create notes.
|
||||
user = self.session.query(
|
||||
"User where username is \"{}\"".format(self.session.api_user)
|
||||
).first()
|
||||
if user:
|
||||
for comment in entity_data.get("comments", []):
|
||||
entity.create_note(comment, user)
|
||||
else:
|
||||
self.log.warning(
|
||||
"Was not able to query current User {}".format(
|
||||
self.session.api_user
|
||||
)
|
||||
)
|
||||
try:
|
||||
self.session.commit()
|
||||
except Exception:
|
||||
tp, value, tb = sys.exc_info()
|
||||
self.session.rollback()
|
||||
self.session._configure_locations()
|
||||
six.reraise(tp, value, tb)
|
||||
|
||||
# Import children.
|
||||
if 'childs' in entity_data:
|
||||
self.import_to_ftrack(
|
||||
entity_data['childs'], entity)
|
||||
|
||||
def create_links(self, entity_data, entity):
|
||||
# Clear existing links.
|
||||
for link in entity.get("incoming_links", []):
|
||||
self.session.delete(link)
|
||||
try:
|
||||
self.session.commit()
|
||||
except Exception:
|
||||
tp, value, tb = sys.exc_info()
|
||||
self.session.rollback()
|
||||
self.session._configure_locations()
|
||||
six.reraise(tp, value, tb)
|
||||
|
||||
# Create new links.
|
||||
for input in entity_data.get("inputs", []):
|
||||
input_id = io.find_one({"_id": input})["data"]["ftrackId"]
|
||||
assetbuild = self.session.get("AssetBuild", input_id)
|
||||
self.log.debug(
|
||||
"Creating link from {0} to {1}".format(
|
||||
assetbuild["name"], entity["name"]
|
||||
)
|
||||
)
|
||||
self.session.create(
|
||||
"TypedContextLink", {"from": assetbuild, "to": entity}
|
||||
)
|
||||
|
||||
def get_all_task_types(self, project):
|
||||
tasks = {}
|
||||
proj_template = project['project_schema']
|
||||
temp_task_types = proj_template['_task_type_schema']['types']
|
||||
|
||||
for type in temp_task_types:
|
||||
if type['name'] not in tasks:
|
||||
tasks[type['name']] = type
|
||||
|
||||
return tasks
|
||||
|
||||
def create_task(self, name, task_type, parent):
|
||||
task = self.session.create('Task', {
|
||||
'name': name,
|
||||
'parent': parent
|
||||
})
|
||||
# TODO not secured!!! - check if task_type exists
|
||||
self.log.info(task_type)
|
||||
self.log.info(self.task_types)
|
||||
task['type'] = self.task_types[task_type]
|
||||
|
||||
try:
|
||||
self.session.commit()
|
||||
except Exception:
|
||||
tp, value, tb = sys.exc_info()
|
||||
self.session.rollback()
|
||||
self.session._configure_locations()
|
||||
six.reraise(tp, value, tb)
|
||||
|
||||
return task
|
||||
|
||||
def create_entity(self, name, type, parent):
|
||||
entity = self.session.create(type, {
|
||||
'name': name,
|
||||
'parent': parent
|
||||
})
|
||||
try:
|
||||
self.session.commit()
|
||||
except Exception:
|
||||
tp, value, tb = sys.exc_info()
|
||||
self.session.rollback()
|
||||
self.session._configure_locations()
|
||||
six.reraise(tp, value, tb)
|
||||
|
||||
return entity
|
||||
|
||||
def auto_sync_off(self, project):
|
||||
project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = False
|
||||
|
||||
self.log.info("Ftrack autosync swithed off")
|
||||
|
||||
try:
|
||||
self.session.commit()
|
||||
except Exception:
|
||||
tp, value, tb = sys.exc_info()
|
||||
self.session.rollback()
|
||||
self.session._configure_locations()
|
||||
six.reraise(tp, value, tb)
|
||||
|
||||
def auto_sync_on(self, project):
|
||||
|
||||
project["custom_attributes"][CUST_ATTR_AUTO_SYNC] = True
|
||||
|
||||
self.log.info("Ftrack autosync swithed on")
|
||||
|
||||
try:
|
||||
self.session.commit()
|
||||
except Exception:
|
||||
tp, value, tb = sys.exc_info()
|
||||
self.session.rollback()
|
||||
self.session._configure_locations()
|
||||
six.reraise(tp, value, tb)
|
||||
|
|
@ -54,6 +54,9 @@ class Photoshop(WebSocketRoute):
|
|||
async def projectmanager_route(self):
|
||||
self._tool_route("projectmanager")
|
||||
|
||||
async def subsetmanager_route(self):
|
||||
self._tool_route("subsetmanager")
|
||||
|
||||
def _tool_route(self, tool_name):
|
||||
"""The address accessed when clicking on the buttons."""
|
||||
partial_method = functools.partial(photoshop.show, tool_name)
|
||||
|
|
|
|||
|
|
@ -4,16 +4,37 @@ from pype.modules.websocket_server import WebSocketServer
|
|||
Used anywhere solution is calling client methods.
|
||||
"""
|
||||
import json
|
||||
from collections import namedtuple
|
||||
import attr
|
||||
|
||||
|
||||
class PhotoshopServerStub():
|
||||
@attr.s
|
||||
class PSItem(object):
|
||||
"""
|
||||
Object denoting layer or group item in PS. Each item is created in
|
||||
PS by any Loader, but contains same fields, which are being used
|
||||
in later processing.
|
||||
"""
|
||||
# metadata
|
||||
id = attr.ib() # id created by AE, could be used for querying
|
||||
name = attr.ib() # name of item
|
||||
group = attr.ib(default=None) # item type (footage, folder, comp)
|
||||
parents = attr.ib(factory=list)
|
||||
visible = attr.ib(default=True)
|
||||
type = attr.ib(default=None)
|
||||
# all imported elements, single for
|
||||
members = attr.ib(factory=list)
|
||||
long_name = attr.ib(default=None)
|
||||
|
||||
|
||||
class PhotoshopServerStub:
|
||||
"""
|
||||
Stub for calling function on client (Photoshop js) side.
|
||||
Expects that client is already connected (started when avalon menu
|
||||
is opened).
|
||||
'self.websocketserver.call' is used as async wrapper
|
||||
"""
|
||||
PUBLISH_ICON = '\u2117 '
|
||||
LOADED_ICON = '\u25bc'
|
||||
|
||||
def __init__(self):
|
||||
self.websocketserver = WebSocketServer.get_instance()
|
||||
|
|
@ -34,7 +55,7 @@ class PhotoshopServerStub():
|
|||
"""
|
||||
Parses layer metadata from Headline field of active document
|
||||
Args:
|
||||
layer: <namedTuple Layer("id":XX, "name":"YYY")
|
||||
layer: (PSItem)
|
||||
layers_meta: full list from Headline (for performance in loops)
|
||||
Returns:
|
||||
"""
|
||||
|
|
@ -46,10 +67,33 @@ class PhotoshopServerStub():
|
|||
def imprint(self, layer, data, all_layers=None, layers_meta=None):
|
||||
"""
|
||||
Save layer metadata to Headline field of active document
|
||||
|
||||
Stores metadata in format:
|
||||
[{
|
||||
"active":true,
|
||||
"subset":"imageBG",
|
||||
"family":"image",
|
||||
"id":"pyblish.avalon.instance",
|
||||
"asset":"Town",
|
||||
"uuid": "8"
|
||||
}] - for created instances
|
||||
OR
|
||||
[{
|
||||
"schema": "avalon-core:container-2.0",
|
||||
"id": "pyblish.avalon.instance",
|
||||
"name": "imageMG",
|
||||
"namespace": "Jungle_imageMG_001",
|
||||
"loader": "ImageLoader",
|
||||
"representation": "5fbfc0ee30a946093c6ff18a",
|
||||
"members": [
|
||||
"40"
|
||||
]
|
||||
}] - for loaded instances
|
||||
|
||||
Args:
|
||||
layer (namedtuple): Layer("id": XXX, "name":'YYY')
|
||||
layer (PSItem):
|
||||
data(string): json representation for single layer
|
||||
all_layers (list of namedtuples): for performance, could be
|
||||
all_layers (list of PSItem): for performance, could be
|
||||
injected for usage in loop, if not, single call will be
|
||||
triggered
|
||||
layers_meta(string): json representation from Headline
|
||||
|
|
@ -59,6 +103,7 @@ class PhotoshopServerStub():
|
|||
"""
|
||||
if not layers_meta:
|
||||
layers_meta = self.get_layers_metadata()
|
||||
|
||||
# json.dumps writes integer values in a dictionary to string, so
|
||||
# anticipating it here.
|
||||
if str(layer.id) in layers_meta and layers_meta[str(layer.id)]:
|
||||
|
|
@ -73,11 +118,11 @@ class PhotoshopServerStub():
|
|||
if not all_layers:
|
||||
all_layers = self.get_layers()
|
||||
layer_ids = [layer.id for layer in all_layers]
|
||||
cleaned_data = {}
|
||||
cleaned_data = []
|
||||
|
||||
for id in layers_meta:
|
||||
if int(id) in layer_ids:
|
||||
cleaned_data[id] = layers_meta[id]
|
||||
cleaned_data.append(layers_meta[id])
|
||||
|
||||
payload = json.dumps(cleaned_data, indent=4)
|
||||
|
||||
|
|
@ -89,7 +134,7 @@ class PhotoshopServerStub():
|
|||
"""
|
||||
Returns JSON document with all(?) layers in active document.
|
||||
|
||||
Returns: <list of namedtuples>
|
||||
Returns: <list of PSItem>
|
||||
Format of tuple: { 'id':'123',
|
||||
'name': 'My Layer 1',
|
||||
'type': 'GUIDE'|'FG'|'BG'|'OBJ'
|
||||
|
|
@ -100,12 +145,26 @@ class PhotoshopServerStub():
|
|||
|
||||
return self._to_records(res)
|
||||
|
||||
def get_layer(self, layer_id):
|
||||
"""
|
||||
Returns PSItem for specific 'layer_id' or None if not found
|
||||
Args:
|
||||
layer_id (string): unique layer id, stored in 'uuid' field
|
||||
|
||||
Returns:
|
||||
(PSItem) or None
|
||||
"""
|
||||
layers = self.get_layers()
|
||||
for layer in layers:
|
||||
if str(layer.id) == str(layer_id):
|
||||
return layer
|
||||
|
||||
def get_layers_in_layers(self, layers):
|
||||
"""
|
||||
Return all layers that belong to layers (might be groups).
|
||||
Args:
|
||||
layers <list of namedTuples>:
|
||||
Returns: <list of namedTuples>
|
||||
layers <list of PSItem>:
|
||||
Returns: <list of PSItem>
|
||||
"""
|
||||
all_layers = self.get_layers()
|
||||
ret = []
|
||||
|
|
@ -123,28 +182,30 @@ class PhotoshopServerStub():
|
|||
def create_group(self, name):
|
||||
"""
|
||||
Create new group (eg. LayerSet)
|
||||
Returns: <namedTuple Layer("id":XX, "name":"YYY")>
|
||||
Returns: <PSItem>
|
||||
"""
|
||||
enhanced_name = self.PUBLISH_ICON + name
|
||||
ret = self.websocketserver.call(self.client.call
|
||||
('Photoshop.create_group',
|
||||
name=name))
|
||||
name=enhanced_name))
|
||||
# create group on PS is asynchronous, returns only id
|
||||
layer = {"id": ret, "name": name, "group": True}
|
||||
return namedtuple('Layer', layer.keys())(*layer.values())
|
||||
return PSItem(id=ret, name=name, group=True)
|
||||
|
||||
def group_selected_layers(self, name):
|
||||
"""
|
||||
Group selected layers into new LayerSet (eg. group)
|
||||
Returns: (Layer)
|
||||
"""
|
||||
enhanced_name = self.PUBLISH_ICON + name
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('Photoshop.group_selected_layers',
|
||||
name=name)
|
||||
name=enhanced_name)
|
||||
)
|
||||
res = self._to_records(res)
|
||||
|
||||
if res:
|
||||
return res.pop()
|
||||
rec = res.pop()
|
||||
rec.name = rec.name.replace(self.PUBLISH_ICON, '')
|
||||
return rec
|
||||
raise ValueError("No group record returned")
|
||||
|
||||
def get_selected_layers(self):
|
||||
|
|
@ -163,11 +224,10 @@ class PhotoshopServerStub():
|
|||
layers: <list of Layer('id':XX, 'name':"YYY")>
|
||||
Returns: None
|
||||
"""
|
||||
layer_ids = [layer.id for layer in layers]
|
||||
|
||||
layers_id = [str(lay.id) for lay in layers]
|
||||
self.websocketserver.call(self.client.call
|
||||
('Photoshop.get_layers',
|
||||
layers=layer_ids)
|
||||
('Photoshop.select_layers',
|
||||
layers=json.dumps(layers_id))
|
||||
)
|
||||
|
||||
def get_active_document_full_name(self):
|
||||
|
|
@ -238,7 +298,14 @@ class PhotoshopServerStub():
|
|||
"""
|
||||
Reads layers metadata from Headline from active document in PS.
|
||||
(Headline accessible by File > File Info)
|
||||
Returns(string): - json documents
|
||||
|
||||
Returns:
|
||||
(string): - json documents
|
||||
example:
|
||||
{"8":{"active":true,"subset":"imageBG",
|
||||
"family":"image","id":"pyblish.avalon.instance",
|
||||
"asset":"Town"}}
|
||||
8 is layer(group) id - used for deletion, update etc.
|
||||
"""
|
||||
layers_data = {}
|
||||
res = self.websocketserver.call(self.client.call('Photoshop.read'))
|
||||
|
|
@ -246,6 +313,23 @@ class PhotoshopServerStub():
|
|||
layers_data = json.loads(res)
|
||||
except json.decoder.JSONDecodeError:
|
||||
pass
|
||||
# format of metadata changed from {} to [] because of standardization
|
||||
# keep current implementation logic as its working
|
||||
if not isinstance(layers_data, dict):
|
||||
temp_layers_meta = {}
|
||||
for layer_meta in layers_data:
|
||||
layer_id = layer_meta.get("uuid") or \
|
||||
(layer_meta.get("members")[0])
|
||||
temp_layers_meta[layer_id] = layer_meta
|
||||
layers_data = temp_layers_meta
|
||||
else:
|
||||
# legacy version of metadata
|
||||
for layer_id, layer_meta in layers_data.items():
|
||||
if layer_meta.get("schema") != "avalon-core:container-2.0":
|
||||
layer_meta["uuid"] = str(layer_id)
|
||||
else:
|
||||
layer_meta["members"] = [str(layer_id)]
|
||||
|
||||
return layers_data
|
||||
|
||||
def import_smart_object(self, path, layer_name):
|
||||
|
|
@ -257,11 +341,14 @@ class PhotoshopServerStub():
|
|||
layer_name (str): Unique layer name to differentiate how many times
|
||||
same smart object was loaded
|
||||
"""
|
||||
enhanced_name = self.LOADED_ICON + layer_name
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('Photoshop.import_smart_object',
|
||||
path=path, name=layer_name))
|
||||
|
||||
return self._to_records(res).pop()
|
||||
path=path, name=enhanced_name))
|
||||
rec = self._to_records(res).pop()
|
||||
if rec:
|
||||
rec.name = rec.name.replace(self.LOADED_ICON, '')
|
||||
return rec
|
||||
|
||||
def replace_smart_object(self, layer, path, layer_name):
|
||||
"""
|
||||
|
|
@ -270,13 +357,14 @@ class PhotoshopServerStub():
|
|||
same smart object was loaded
|
||||
|
||||
Args:
|
||||
layer (namedTuple): Layer("id":XX, "name":"YY"..).
|
||||
layer (PSItem):
|
||||
path (str): File to import.
|
||||
"""
|
||||
enhanced_name = self.LOADED_ICON + layer_name
|
||||
self.websocketserver.call(self.client.call
|
||||
('Photoshop.replace_smart_object',
|
||||
layer_id=layer.id,
|
||||
path=path, name=layer_name))
|
||||
path=path, name=enhanced_name))
|
||||
|
||||
def delete_layer(self, layer_id):
|
||||
"""
|
||||
|
|
@ -288,24 +376,62 @@ class PhotoshopServerStub():
|
|||
('Photoshop.delete_layer',
|
||||
layer_id=layer_id))
|
||||
|
||||
def rename_layer(self, layer_id, name):
|
||||
"""
|
||||
Renames specific layer by it's id.
|
||||
Args:
|
||||
layer_id (int): id of layer to delete
|
||||
name (str): new name
|
||||
"""
|
||||
self.websocketserver.call(self.client.call
|
||||
('Photoshop.rename_layer',
|
||||
layer_id=layer_id,
|
||||
name=name))
|
||||
|
||||
def remove_instance(self, instance_id):
|
||||
cleaned_data = {}
|
||||
|
||||
for key, instance in self.get_layers_metadata().items():
|
||||
if key != instance_id:
|
||||
cleaned_data[key] = instance
|
||||
|
||||
payload = json.dumps(cleaned_data, indent=4)
|
||||
|
||||
self.websocketserver.call(self.client.call
|
||||
('Photoshop.imprint', payload=payload)
|
||||
)
|
||||
|
||||
def close(self):
|
||||
self.client.close()
|
||||
|
||||
def _to_records(self, res):
|
||||
"""
|
||||
Converts string json representation into list of named tuples for
|
||||
Converts string json representation into list of PSItem for
|
||||
dot notation access to work.
|
||||
Returns: <list of named tuples>
|
||||
res(string): - json representation
|
||||
Args:
|
||||
res (string): valid json
|
||||
Returns:
|
||||
<list of PSItem>
|
||||
"""
|
||||
try:
|
||||
layers_data = json.loads(res)
|
||||
except json.decoder.JSONDecodeError:
|
||||
raise ValueError("Received broken JSON {}".format(res))
|
||||
ret = []
|
||||
# convert to namedtuple to use dot donation
|
||||
if isinstance(layers_data, dict): # TODO refactore
|
||||
|
||||
# convert to AEItem to use dot donation
|
||||
if isinstance(layers_data, dict):
|
||||
layers_data = [layers_data]
|
||||
for d in layers_data:
|
||||
ret.append(namedtuple('Layer', d.keys())(*d.values()))
|
||||
# currently implemented and expected fields
|
||||
item = PSItem(d.get('id'),
|
||||
d.get('name'),
|
||||
d.get('group'),
|
||||
d.get('parents'),
|
||||
d.get('visible'),
|
||||
d.get('type'),
|
||||
d.get('members'),
|
||||
d.get('long_name'))
|
||||
|
||||
ret.append(item)
|
||||
return ret
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
<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>
|
||||
|
Before Width: | Height: | Size: 1 KiB |
|
|
@ -1,111 +1,11 @@
|
|||
{
|
||||
"capture": {
|
||||
"Codec": {
|
||||
"compression": "jpg",
|
||||
"format": "image",
|
||||
"quality": 95
|
||||
},
|
||||
"Display Options": {
|
||||
"background": [
|
||||
0.7,
|
||||
0.7,
|
||||
0.7
|
||||
],
|
||||
"backgroundBottom": [
|
||||
0.7,
|
||||
0.7,
|
||||
0.7
|
||||
],
|
||||
"backgroundTop": [
|
||||
0.7,
|
||||
0.7,
|
||||
0.7
|
||||
],
|
||||
"override_display": true
|
||||
},
|
||||
"Generic": {
|
||||
"isolate_view": true,
|
||||
"off_screen": true
|
||||
},
|
||||
"IO": {
|
||||
"name": "",
|
||||
"open_finished": true,
|
||||
"raw_frame_numbers": true,
|
||||
"recent_playblasts": [],
|
||||
"save_file": true
|
||||
},
|
||||
"PanZoom": {
|
||||
"pan_zoom": true
|
||||
},
|
||||
"Renderer": {
|
||||
"rendererName": "vp2Renderer"
|
||||
},
|
||||
"Resolution": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"percent": 1.0,
|
||||
"mode": "Custom"
|
||||
},
|
||||
"Time Range": {
|
||||
"start_frame": 0,
|
||||
"end_frame": 0,
|
||||
"frame": "",
|
||||
"time": "Time Slider"
|
||||
},
|
||||
"Viewport Options": {
|
||||
"cameras": false,
|
||||
"clipGhosts": false,
|
||||
"controlVertices": false,
|
||||
"deformers": false,
|
||||
"dimensions": false,
|
||||
"displayLights": 0,
|
||||
"dynamicConstraints": false,
|
||||
"dynamics": false,
|
||||
"fluids": false,
|
||||
"follicles": false,
|
||||
"gpuCacheDisplayFilter": false,
|
||||
"greasePencils": false,
|
||||
"grid": false,
|
||||
"hairSystems": true,
|
||||
"handles": false,
|
||||
"high_quality": true,
|
||||
"hud": false,
|
||||
"hulls": false,
|
||||
"ikHandles": false,
|
||||
"imagePlane": true,
|
||||
"joints": false,
|
||||
"lights": false,
|
||||
"locators": false,
|
||||
"manipulators": false,
|
||||
"motionTrails": false,
|
||||
"nCloths": false,
|
||||
"nParticles": false,
|
||||
"nRigids": false,
|
||||
"nurbsCurves": false,
|
||||
"nurbsSurfaces": false,
|
||||
"override_viewport_options": true,
|
||||
"particleInstancers": false,
|
||||
"pivots": false,
|
||||
"planes": false,
|
||||
"pluginShapes": false,
|
||||
"polymeshes": true,
|
||||
"shadows": true,
|
||||
"strokes": false,
|
||||
"subdivSurfaces": false,
|
||||
"textures": false,
|
||||
"twoSidedLighting": true
|
||||
},
|
||||
"Camera Options": {
|
||||
"displayGateMask": false,
|
||||
"displayResolution": false,
|
||||
"displayFilmGate": false,
|
||||
"displayFieldChart": false,
|
||||
"displaySafeAction": false,
|
||||
"displaySafeTitle": false,
|
||||
"displayFilmPivot": false,
|
||||
"displayFilmOrigin": false,
|
||||
"overscan": 1.0
|
||||
}
|
||||
"ext_mapping": {
|
||||
"model": "ma",
|
||||
"mayaAscii": "ma",
|
||||
"camera": "ma",
|
||||
"rig": "ma",
|
||||
"workfile": "ma",
|
||||
"yetiRig": "ma"
|
||||
},
|
||||
"create": {
|
||||
"CreateAnimation": {
|
||||
|
|
@ -299,6 +199,10 @@
|
|||
"enabled": false,
|
||||
"optional": true
|
||||
},
|
||||
"ValidateMeshNormalsUnlocked": {
|
||||
"enabled": false,
|
||||
"optional": true
|
||||
},
|
||||
"ValidateMeshUVSetMap1": {
|
||||
"enabled": false,
|
||||
"optional": true
|
||||
|
|
@ -336,7 +240,7 @@
|
|||
"optional": true
|
||||
},
|
||||
"ValidateTransformZero": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"optional": true
|
||||
},
|
||||
"ValidateCameraAttributes": {
|
||||
|
|
@ -351,6 +255,105 @@
|
|||
"enabled": true,
|
||||
"optional": true
|
||||
},
|
||||
"ExtractPlayblast": {
|
||||
"capture_preset": {
|
||||
"Codec": {
|
||||
"compression": "jpg",
|
||||
"format": "image",
|
||||
"quality": 95
|
||||
},
|
||||
"Display Options": {
|
||||
"background": [
|
||||
0.7,
|
||||
0.7,
|
||||
0.7
|
||||
],
|
||||
"backgroundBottom": [
|
||||
0.7,
|
||||
0.7,
|
||||
0.7
|
||||
],
|
||||
"backgroundTop": [
|
||||
0.7,
|
||||
0.7,
|
||||
0.7
|
||||
],
|
||||
"override_display": true
|
||||
},
|
||||
"Generic": {
|
||||
"isolate_view": true,
|
||||
"off_screen": true
|
||||
},
|
||||
"PanZoom": {
|
||||
"pan_zoom": true
|
||||
},
|
||||
"Renderer": {
|
||||
"rendererName": "vp2Renderer"
|
||||
},
|
||||
"Resolution": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"percent": 1.0,
|
||||
"mode": "Custom"
|
||||
},
|
||||
"Viewport Options": {
|
||||
"override_viewport_options": true,
|
||||
"displayLights": "0",
|
||||
"textureMaxResolution": 1024,
|
||||
"multiSample": 4,
|
||||
"shadows": true,
|
||||
"textures": true,
|
||||
"twoSidedLighting": true,
|
||||
"ssaoEnable": true,
|
||||
"cameras": false,
|
||||
"clipGhosts": false,
|
||||
"controlVertices": false,
|
||||
"deformers": false,
|
||||
"dimensions": false,
|
||||
"dynamicConstraints": false,
|
||||
"dynamics": false,
|
||||
"fluids": false,
|
||||
"follicles": false,
|
||||
"gpuCacheDisplayFilter": false,
|
||||
"greasePencils": false,
|
||||
"grid": false,
|
||||
"hairSystems": true,
|
||||
"handles": false,
|
||||
"hud": false,
|
||||
"hulls": false,
|
||||
"ikHandles": false,
|
||||
"imagePlane": true,
|
||||
"joints": false,
|
||||
"lights": false,
|
||||
"locators": false,
|
||||
"manipulators": false,
|
||||
"motionTrails": false,
|
||||
"nCloths": false,
|
||||
"nParticles": false,
|
||||
"nRigids": false,
|
||||
"nurbsCurves": false,
|
||||
"nurbsSurfaces": false,
|
||||
"particleInstancers": false,
|
||||
"pivots": false,
|
||||
"planes": false,
|
||||
"pluginShapes": false,
|
||||
"polymeshes": true,
|
||||
"strokes": false,
|
||||
"subdivSurfaces": false
|
||||
},
|
||||
"Camera Options": {
|
||||
"displayGateMask": false,
|
||||
"displayResolution": false,
|
||||
"displayFilmGate": false,
|
||||
"displayFieldChart": false,
|
||||
"displaySafeAction": false,
|
||||
"displaySafeTitle": false,
|
||||
"displayFilmPivot": false,
|
||||
"displayFilmOrigin": false,
|
||||
"overscan": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"ExtractCameraAlembic": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,30 @@
|
|||
"PreCollectNukeInstances": {
|
||||
"sync_workfile_version": true
|
||||
},
|
||||
"ValidateKnobs": {
|
||||
"enabled": false,
|
||||
"knobs": {
|
||||
"render": {
|
||||
"review": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ValidateOutputResolution": {
|
||||
"enabled": true,
|
||||
"optional": true
|
||||
},
|
||||
"ValidateGizmo": {
|
||||
"enabled": true,
|
||||
"optional": true
|
||||
},
|
||||
"ValidateScript": {
|
||||
"enabled": true,
|
||||
"optional": true
|
||||
},
|
||||
"ValidateNukeWriteBoundingBox": {
|
||||
"enabled": true,
|
||||
"optional": true
|
||||
},
|
||||
"ExtractThumbnail": {
|
||||
"enabled": true,
|
||||
"nodes": {
|
||||
|
|
@ -38,14 +62,6 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"ValidateKnobs": {
|
||||
"enabled": false,
|
||||
"knobs": {
|
||||
"render": {
|
||||
"review": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ExtractReviewDataLut": {
|
||||
"enabled": false
|
||||
},
|
||||
|
|
@ -61,22 +77,25 @@
|
|||
"deadline_pool": "",
|
||||
"deadline_pool_secondary": "",
|
||||
"deadline_chunk_size": 1
|
||||
},
|
||||
"ValidateOutputResolution": {
|
||||
}
|
||||
},
|
||||
"load": {
|
||||
"LoadImage": {
|
||||
"enabled": true,
|
||||
"optional": true
|
||||
"representations": []
|
||||
},
|
||||
"ValidateGizmo": {
|
||||
"LoadMov": {
|
||||
"enabled": true,
|
||||
"optional": true
|
||||
"representations": []
|
||||
},
|
||||
"ValidateScript": {
|
||||
"LoadSequence": {
|
||||
"enabled": true,
|
||||
"optional": true
|
||||
},
|
||||
"ValidateNukeWriteBoundingBox": {
|
||||
"enabled": true,
|
||||
"optional": true
|
||||
"representations": [
|
||||
"png",
|
||||
"jpg",
|
||||
"exr",
|
||||
""
|
||||
]
|
||||
}
|
||||
},
|
||||
"workfile_build": {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,4 @@
|
|||
{
|
||||
"publish": {
|
||||
"ExtractThumbnailSP": {
|
||||
"ffmpeg_args": {
|
||||
"input": [
|
||||
"gamma 2.2"
|
||||
],
|
||||
"output": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"create": {
|
||||
"create_workfile": {
|
||||
"name": "workfile",
|
||||
|
|
@ -121,5 +111,15 @@
|
|||
"create_image": "Image",
|
||||
"create_matchmove": "Matchmove"
|
||||
}
|
||||
},
|
||||
"publish": {
|
||||
"ExtractThumbnailSP": {
|
||||
"ffmpeg_args": {
|
||||
"input": [
|
||||
"gamma 2.2"
|
||||
],
|
||||
"output": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,13 @@
|
|||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_maya_capture"
|
||||
"type": "dict-modifiable",
|
||||
"key": "ext_mapping",
|
||||
"label": "Extension Mapping",
|
||||
"use_label_wrap": true,
|
||||
"object_type": {
|
||||
"type": "text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,11 @@
|
|||
"type": "schema",
|
||||
"name": "schema_nuke_publish",
|
||||
"template_data": []
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_nuke_load",
|
||||
"template_data": []
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
|
|
|
|||
|
|
@ -1,581 +0,0 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "capture",
|
||||
"label": "Maya Playblast settings",
|
||||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "Codec",
|
||||
"children": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>Codec</b>"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "compression",
|
||||
"label": "Compression type"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "format",
|
||||
"label": "Data format"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "quality",
|
||||
"label": "Quality",
|
||||
"decimal": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
},
|
||||
|
||||
{
|
||||
"type": "splitter"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "Display Options",
|
||||
"children": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>Display Options</b>"
|
||||
},
|
||||
{
|
||||
"type": "list-strict",
|
||||
"key": "background",
|
||||
"label": "Background Color: ",
|
||||
"object_types": [
|
||||
{
|
||||
"label": "Red",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
},
|
||||
{
|
||||
"label": "Green",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
},
|
||||
{
|
||||
"label": "Blue",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "list-strict",
|
||||
"key": "backgroundBottom",
|
||||
"label": "Background Bottom: ",
|
||||
"object_types": [
|
||||
{
|
||||
"label": "Red",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
},
|
||||
{
|
||||
"label": "Green",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
},
|
||||
{
|
||||
"label": "Blue",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "list-strict",
|
||||
"key": "backgroundTop",
|
||||
"label": "Background Top: ",
|
||||
"object_types": [
|
||||
{
|
||||
"label": "Red",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
},
|
||||
{
|
||||
"label": "Green",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
},
|
||||
{
|
||||
"label": "Blue",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "override_display",
|
||||
"label": "Override display options"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "Generic",
|
||||
"children": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>Generic</b>"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "isolate_view",
|
||||
"label": " Isolate view"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "off_screen",
|
||||
"label": " Off Screen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "IO",
|
||||
"children": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>IO</b>"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "name",
|
||||
"label": "Name"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "open_finished",
|
||||
"label": "Open finished"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "raw_frame_numbers",
|
||||
"label": "Raw frame numbers"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "recent_playblasts",
|
||||
"label": "Recent Playblasts",
|
||||
"object_type": "text"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "save_file",
|
||||
"label": "Save file"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "PanZoom",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "pan_zoom",
|
||||
"label": " Pan Zoom"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "Renderer",
|
||||
"children": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>Renderer</b>"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "rendererName",
|
||||
"label": " Renderer name"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "Resolution",
|
||||
"children": [
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>Resolution</b>"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "width",
|
||||
"label": " Width",
|
||||
"decimal": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 99999
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "height",
|
||||
"label": "Height",
|
||||
"decimal": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 99999
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "percent",
|
||||
"label": "percent",
|
||||
"decimal": 1,
|
||||
"minimum": 0,
|
||||
"maximum": 200
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "mode",
|
||||
"label": "Mode"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "Time Range",
|
||||
"children": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>Time Range</b>"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "start_frame",
|
||||
"label": " Start frame",
|
||||
"decimal": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 999999
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "end_frame",
|
||||
"label": "End frame",
|
||||
"decimal": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 999999
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "frame",
|
||||
"label": "Frame"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "time",
|
||||
"label": "Time"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "Viewport Options",
|
||||
"label": "Viewport Options",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "cameras",
|
||||
"label": "cameras"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "clipGhosts",
|
||||
"label": "clipGhosts"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "controlVertices",
|
||||
"label": "controlVertices"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "deformers",
|
||||
"label": "deformers"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "dimensions",
|
||||
"label": "dimensions"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "displayLights",
|
||||
"label": "displayLights",
|
||||
"decimal": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 10
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "dynamicConstraints",
|
||||
"label": "dynamicConstraints"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "dynamics",
|
||||
"label": "dynamics"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "fluids",
|
||||
"label": "fluids"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "follicles",
|
||||
"label": "follicles"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "gpuCacheDisplayFilter",
|
||||
"label": "gpuCacheDisplayFilter"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "greasePencils",
|
||||
"label": "greasePencils"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "grid",
|
||||
"label": "grid"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "hairSystems",
|
||||
"label": "hairSystems"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "handles",
|
||||
"label": "handles"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "high_quality",
|
||||
"label": "high_quality"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "hud",
|
||||
"label": "hud"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "hulls",
|
||||
"label": "hulls"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "ikHandles",
|
||||
"label": "ikHandles"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "imagePlane",
|
||||
"label": "imagePlane"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "joints",
|
||||
"label": "joints"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "lights",
|
||||
"label": "lights"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "locators",
|
||||
"label": "locators"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "manipulators",
|
||||
"label": "manipulators"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "motionTrails",
|
||||
"label": "motionTrails"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "nCloths",
|
||||
"label": "nCloths"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "nParticles",
|
||||
"label": "nParticles"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "nRigids",
|
||||
"label": "nRigids"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "nurbsCurves",
|
||||
"label": "nurbsCurves"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "nurbsSurfaces",
|
||||
"label": "nurbsSurfaces"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "override_viewport_options",
|
||||
"label": "override_viewport_options"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "particleInstancers",
|
||||
"label": "particleInstancers"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "pivots",
|
||||
"label": "pivots"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "planes",
|
||||
"label": "planes"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "pluginShapes",
|
||||
"label": "pluginShapes"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "polymeshes",
|
||||
"label": "polymeshes"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "shadows",
|
||||
"label": "shadows"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "strokes",
|
||||
"label": "strokes"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "subdivSurfaces",
|
||||
"label": "subdivSurfaces"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "textures",
|
||||
"label": "textures"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "twoSidedLighting",
|
||||
"label": "twoSidedLighting"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "Camera Options",
|
||||
"label": "Camera Options",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayGateMask",
|
||||
"label": "displayGateMask"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayResolution",
|
||||
"label": "displayResolution"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayFilmGate",
|
||||
"label": "displayFilmGate"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayFieldChart",
|
||||
"label": "displayFieldChart"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displaySafeAction",
|
||||
"label": "displaySafeAction"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displaySafeTitle",
|
||||
"label": "displaySafeTitle"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayFilmPivot",
|
||||
"label": "displayFilmPivot"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayFilmOrigin",
|
||||
"label": "displayFilmOrigin"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "overscan",
|
||||
"label": "overscan",
|
||||
"decimal": 1,
|
||||
"minimum": 0,
|
||||
"maximum": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -170,6 +170,10 @@
|
|||
"key": "ValidateMeshNonManifold",
|
||||
"label": "ValidateMeshNonManifold"
|
||||
},
|
||||
{
|
||||
"key": "ValidateMeshNormalsUnlocked",
|
||||
"label": "ValidateMeshNormalsUnlocked"
|
||||
},
|
||||
{
|
||||
"key": "ValidateMeshUVSetMap1",
|
||||
"label": "ValidateMeshUVSetMap1",
|
||||
|
|
@ -242,6 +246,10 @@
|
|||
"type": "label",
|
||||
"label": "Extractors"
|
||||
},
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_maya_capture"
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "load",
|
||||
"label": "Loader plugins",
|
||||
"children": [
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_loader_plugin",
|
||||
"template_data": [
|
||||
{
|
||||
"key": "LoadImage",
|
||||
"label": "Image Loader"
|
||||
},
|
||||
{
|
||||
"key": "LoadMov",
|
||||
"label": "Movie Loader"
|
||||
},
|
||||
{
|
||||
"key": "LoadSequence",
|
||||
"label": "Image Sequence Loader"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,541 @@
|
|||
[
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "ExtractPlayblast",
|
||||
"label": "Extract Playblast settings",
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "capture_preset",
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "Codec",
|
||||
"children": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>Codec</b>"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "compression",
|
||||
"label": "Compression type"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "format",
|
||||
"label": "Data format"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "quality",
|
||||
"label": "Quality",
|
||||
"decimal": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
},
|
||||
|
||||
{
|
||||
"type": "splitter"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "Display Options",
|
||||
"children": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>Display Options</b>"
|
||||
},
|
||||
{
|
||||
"type": "list-strict",
|
||||
"key": "background",
|
||||
"label": "Background Color: ",
|
||||
"object_types": [
|
||||
{
|
||||
"label": "Red",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
},
|
||||
{
|
||||
"label": "Green",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
},
|
||||
{
|
||||
"label": "Blue",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "list-strict",
|
||||
"key": "backgroundBottom",
|
||||
"label": "Background Bottom: ",
|
||||
"object_types": [
|
||||
{
|
||||
"label": "Red",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
},
|
||||
{
|
||||
"label": "Green",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
},
|
||||
{
|
||||
"label": "Blue",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "list-strict",
|
||||
"key": "backgroundTop",
|
||||
"label": "Background Top: ",
|
||||
"object_types": [
|
||||
{
|
||||
"label": "Red",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
},
|
||||
{
|
||||
"label": "Green",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
},
|
||||
{
|
||||
"label": "Blue",
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"decimal": 3
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "override_display",
|
||||
"label": "Override display options"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "Generic",
|
||||
"children": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>Generic</b>"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "isolate_view",
|
||||
"label": " Isolate view"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "off_screen",
|
||||
"label": " Off Screen"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "PanZoom",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "pan_zoom",
|
||||
"label": " Pan Zoom"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "Renderer",
|
||||
"children": [
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>Renderer</b>"
|
||||
},
|
||||
{
|
||||
"type": "enum",
|
||||
"key": "rendererName",
|
||||
"label": "Renderer name",
|
||||
"enum_items": [
|
||||
{ "vp2Renderer": "Viewport 2.0" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "Resolution",
|
||||
"children": [
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "<b>Resolution</b>"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "width",
|
||||
"label": " Width",
|
||||
"decimal": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 99999
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "height",
|
||||
"label": "Height",
|
||||
"decimal": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 99999
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "percent",
|
||||
"label": "percent",
|
||||
"decimal": 1,
|
||||
"minimum": 0,
|
||||
"maximum": 200
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "mode",
|
||||
"label": "Mode"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "Viewport Options",
|
||||
"label": "Viewport Options",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "override_viewport_options",
|
||||
"label": "override_viewport_options"
|
||||
},
|
||||
{
|
||||
"type": "enum",
|
||||
"key": "displayLights",
|
||||
"label": "Display Lights",
|
||||
"enum_items": [
|
||||
{ "default": "Default Lighting"},
|
||||
{ "all": "All Lights"},
|
||||
{ "selected": "Selected Lights"},
|
||||
{ "flat": "Flat Lighting"},
|
||||
{ "nolights": "No Lights"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "textureMaxResolution",
|
||||
"label": "Texture Clamp Resolution",
|
||||
"decimal": 0
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "multiSample",
|
||||
"label": "Anti Aliasing Samples",
|
||||
"decimal": 0,
|
||||
"minimum": 0,
|
||||
"maximum": 32
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "shadows",
|
||||
"label": "Display Shadows"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "textures",
|
||||
"label": "Display Textures"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "twoSidedLighting",
|
||||
"label": "Two Sided Lighting"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "ssaoEnable",
|
||||
"label": "Screen Space Ambient Occlusion"
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "cameras",
|
||||
"label": "cameras"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "clipGhosts",
|
||||
"label": "clipGhosts"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "controlVertices",
|
||||
"label": "controlVertices"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "deformers",
|
||||
"label": "deformers"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "dimensions",
|
||||
"label": "dimensions"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "dynamicConstraints",
|
||||
"label": "dynamicConstraints"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "dynamics",
|
||||
"label": "dynamics"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "fluids",
|
||||
"label": "fluids"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "follicles",
|
||||
"label": "follicles"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "gpuCacheDisplayFilter",
|
||||
"label": "gpuCacheDisplayFilter"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "greasePencils",
|
||||
"label": "greasePencils"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "grid",
|
||||
"label": "grid"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "hairSystems",
|
||||
"label": "hairSystems"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "handles",
|
||||
"label": "handles"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "hud",
|
||||
"label": "hud"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "hulls",
|
||||
"label": "hulls"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "ikHandles",
|
||||
"label": "ikHandles"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "imagePlane",
|
||||
"label": "imagePlane"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "joints",
|
||||
"label": "joints"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "lights",
|
||||
"label": "lights"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "locators",
|
||||
"label": "locators"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "manipulators",
|
||||
"label": "manipulators"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "motionTrails",
|
||||
"label": "motionTrails"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "nCloths",
|
||||
"label": "nCloths"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "nParticles",
|
||||
"label": "nParticles"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "nRigids",
|
||||
"label": "nRigids"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "nurbsCurves",
|
||||
"label": "nurbsCurves"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "nurbsSurfaces",
|
||||
"label": "nurbsSurfaces"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "particleInstancers",
|
||||
"label": "particleInstancers"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "pivots",
|
||||
"label": "pivots"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "planes",
|
||||
"label": "planes"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "pluginShapes",
|
||||
"label": "pluginShapes"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "polymeshes",
|
||||
"label": "polymeshes"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "strokes",
|
||||
"label": "strokes"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "subdivSurfaces",
|
||||
"label": "subdivSurfaces"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "Camera Options",
|
||||
"label": "Camera Options",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayGateMask",
|
||||
"label": "displayGateMask"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayResolution",
|
||||
"label": "displayResolution"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayFilmGate",
|
||||
"label": "displayFilmGate"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayFieldChart",
|
||||
"label": "displayFieldChart"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displaySafeAction",
|
||||
"label": "displaySafeAction"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displaySafeTitle",
|
||||
"label": "displaySafeTitle"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayFilmPivot",
|
||||
"label": "displayFilmPivot"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "displayFilmOrigin",
|
||||
"label": "displayFilmOrigin"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"key": "overscan",
|
||||
"label": "overscan",
|
||||
"decimal": 1,
|
||||
"minimum": 0,
|
||||
"maximum": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
21
pype/tools/mayalookassigner/LICENSE
Normal file
21
pype/tools/mayalookassigner/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 Colorbleed
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
9
pype/tools/mayalookassigner/__init__.py
Normal file
9
pype/tools/mayalookassigner/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from .app import (
|
||||
App,
|
||||
show
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"App",
|
||||
"show"]
|
||||
248
pype/tools/mayalookassigner/app.py
Normal file
248
pype/tools/mayalookassigner/app.py
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
import sys
|
||||
import time
|
||||
import logging
|
||||
|
||||
from pype.hosts.maya.api.lib import assign_look_by_version
|
||||
|
||||
from avalon import style, io
|
||||
from avalon.tools import lib
|
||||
from avalon.vendor.Qt import QtWidgets, QtCore
|
||||
|
||||
from maya import cmds
|
||||
# old api for MFileIO
|
||||
import maya.OpenMaya
|
||||
import maya.api.OpenMaya as om
|
||||
|
||||
from . import widgets
|
||||
from . import commands
|
||||
|
||||
module = sys.modules[__name__]
|
||||
module.window = None
|
||||
|
||||
|
||||
class App(QtWidgets.QWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QtWidgets.QWidget.__init__(self, parent=parent)
|
||||
|
||||
self.log = logging.getLogger(__name__)
|
||||
|
||||
# Store callback references
|
||||
self._callbacks = []
|
||||
|
||||
filename = commands.get_workfile()
|
||||
|
||||
self.setObjectName("lookManager")
|
||||
self.setWindowTitle("Look Manager 1.3.0 - [{}]".format(filename))
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setParent(parent)
|
||||
|
||||
# Force to delete the window on close so it triggers
|
||||
# closeEvent only once. Otherwise it's retriggered when
|
||||
# the widget gets garbage collected.
|
||||
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
|
||||
|
||||
self.resize(750, 500)
|
||||
|
||||
self.setup_ui()
|
||||
|
||||
self.setup_connections()
|
||||
|
||||
# Force refresh check on initialization
|
||||
self._on_renderlayer_switch()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Build the UI"""
|
||||
|
||||
# Assets (left)
|
||||
asset_outliner = widgets.AssetOutliner()
|
||||
|
||||
# Looks (right)
|
||||
looks_widget = QtWidgets.QWidget()
|
||||
looks_layout = QtWidgets.QVBoxLayout(looks_widget)
|
||||
|
||||
look_outliner = widgets.LookOutliner() # Database look overview
|
||||
|
||||
assign_selected = QtWidgets.QCheckBox("Assign to selected only")
|
||||
assign_selected.setToolTip("Whether to assign only to selected nodes "
|
||||
"or to the full asset")
|
||||
remove_unused_btn = QtWidgets.QPushButton("Remove Unused Looks")
|
||||
|
||||
looks_layout.addWidget(look_outliner)
|
||||
looks_layout.addWidget(assign_selected)
|
||||
looks_layout.addWidget(remove_unused_btn)
|
||||
|
||||
# Footer
|
||||
status = QtWidgets.QStatusBar()
|
||||
status.setSizeGripEnabled(False)
|
||||
status.setFixedHeight(25)
|
||||
warn_layer = QtWidgets.QLabel("Current Layer is not "
|
||||
"defaultRenderLayer")
|
||||
warn_layer.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
warn_layer.setStyleSheet("color: #DD5555; font-weight: bold;")
|
||||
warn_layer.setFixedHeight(25)
|
||||
|
||||
footer = QtWidgets.QHBoxLayout()
|
||||
footer.setContentsMargins(0, 0, 0, 0)
|
||||
footer.addWidget(status)
|
||||
footer.addWidget(warn_layer)
|
||||
|
||||
# Build up widgets
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setSpacing(0)
|
||||
main_splitter = QtWidgets.QSplitter()
|
||||
main_splitter.setStyleSheet("QSplitter{ border: 0px; }")
|
||||
main_splitter.addWidget(asset_outliner)
|
||||
main_splitter.addWidget(looks_widget)
|
||||
main_splitter.setSizes([350, 200])
|
||||
main_layout.addWidget(main_splitter)
|
||||
main_layout.addLayout(footer)
|
||||
|
||||
# Set column width
|
||||
asset_outliner.view.setColumnWidth(0, 200)
|
||||
look_outliner.view.setColumnWidth(0, 150)
|
||||
|
||||
# Open widgets
|
||||
self.asset_outliner = asset_outliner
|
||||
self.look_outliner = look_outliner
|
||||
self.status = status
|
||||
self.warn_layer = warn_layer
|
||||
|
||||
# Buttons
|
||||
self.remove_unused = remove_unused_btn
|
||||
self.assign_selected = assign_selected
|
||||
|
||||
def setup_connections(self):
|
||||
"""Connect interactive widgets with actions"""
|
||||
|
||||
self.asset_outliner.selection_changed.connect(
|
||||
self.on_asset_selection_changed)
|
||||
|
||||
self.asset_outliner.refreshed.connect(
|
||||
lambda: self.echo("Loaded assets.."))
|
||||
|
||||
self.look_outliner.menu_apply_action.connect(self.on_process_selected)
|
||||
self.remove_unused.clicked.connect(commands.remove_unused_looks)
|
||||
|
||||
# Maya renderlayer switch callback
|
||||
callback = om.MEventMessage.addEventCallback(
|
||||
"renderLayerManagerChange",
|
||||
self._on_renderlayer_switch
|
||||
)
|
||||
self._callbacks.append(callback)
|
||||
|
||||
def closeEvent(self, event):
|
||||
|
||||
# Delete callbacks
|
||||
for callback in self._callbacks:
|
||||
om.MMessage.removeCallback(callback)
|
||||
|
||||
return super(App, self).closeEvent(event)
|
||||
|
||||
def _on_renderlayer_switch(self, *args):
|
||||
"""Callback that updates on Maya renderlayer switch"""
|
||||
|
||||
if maya.OpenMaya.MFileIO.isNewingFile():
|
||||
# Don't perform a check during file open or file new as
|
||||
# the renderlayers will not be in a valid state yet.
|
||||
return
|
||||
|
||||
layer = cmds.editRenderLayerGlobals(query=True,
|
||||
currentRenderLayer=True)
|
||||
if layer != "defaultRenderLayer":
|
||||
self.warn_layer.show()
|
||||
else:
|
||||
self.warn_layer.hide()
|
||||
|
||||
def echo(self, message):
|
||||
self.status.showMessage(message, 1500)
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the content"""
|
||||
|
||||
# Get all containers and information
|
||||
self.asset_outliner.clear()
|
||||
found_items = self.asset_outliner.get_all_assets()
|
||||
if not found_items:
|
||||
self.look_outliner.clear()
|
||||
|
||||
def on_asset_selection_changed(self):
|
||||
"""Get selected items from asset loader and fill look outliner"""
|
||||
|
||||
items = self.asset_outliner.get_selected_items()
|
||||
self.look_outliner.clear()
|
||||
self.look_outliner.add_items(items)
|
||||
|
||||
def on_process_selected(self):
|
||||
"""Process all selected looks for the selected assets"""
|
||||
|
||||
assets = self.asset_outliner.get_selected_items()
|
||||
assert assets, "No asset selected"
|
||||
|
||||
# Collect the looks we want to apply (by name)
|
||||
look_items = self.look_outliner.get_selected_items()
|
||||
looks = {look["subset"] for look in look_items}
|
||||
|
||||
selection = self.assign_selected.isChecked()
|
||||
asset_nodes = self.asset_outliner.get_nodes(selection=selection)
|
||||
|
||||
start = time.time()
|
||||
for i, (asset, item) in enumerate(asset_nodes.items()):
|
||||
|
||||
# Label prefix
|
||||
prefix = "({}/{})".format(i+1, len(asset_nodes))
|
||||
|
||||
# Assign the first matching look relevant for this asset
|
||||
# (since assigning multiple to the same nodes makes no sense)
|
||||
assign_look = next((subset for subset in item["looks"]
|
||||
if subset["name"] in looks), None)
|
||||
if not assign_look:
|
||||
self.echo("{} No matching selected "
|
||||
"look for {}".format(prefix, asset))
|
||||
continue
|
||||
|
||||
# Get the latest version of this asset's look subset
|
||||
version = io.find_one({"type": "version",
|
||||
"parent": assign_look["_id"]},
|
||||
sort=[("name", -1)])
|
||||
|
||||
subset_name = assign_look["name"]
|
||||
self.echo("{} Assigning {} to {}\t".format(prefix,
|
||||
subset_name,
|
||||
asset))
|
||||
|
||||
# Assign look
|
||||
assign_look_by_version(nodes=item["nodes"],
|
||||
version_id=version["_id"])
|
||||
|
||||
end = time.time()
|
||||
|
||||
self.echo("Finished assigning.. ({0:.3f}s)".format(end - start))
|
||||
|
||||
|
||||
def show():
|
||||
"""Display Loader GUI
|
||||
|
||||
Arguments:
|
||||
debug (bool, optional): Run loader in debug-mode,
|
||||
defaults to False
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
module.window.close()
|
||||
del module.window
|
||||
except (RuntimeError, AttributeError):
|
||||
pass
|
||||
|
||||
# Get Maya main window
|
||||
top_level_widgets = QtWidgets.QApplication.topLevelWidgets()
|
||||
mainwindow = next(widget for widget in top_level_widgets
|
||||
if widget.objectName() == "MayaWindow")
|
||||
|
||||
with lib.application():
|
||||
window = App(parent=mainwindow)
|
||||
window.setStyleSheet(style.load_stylesheet())
|
||||
window.show()
|
||||
|
||||
module.window = window
|
||||
191
pype/tools/mayalookassigner/commands.py
Normal file
191
pype/tools/mayalookassigner/commands.py
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
from collections import defaultdict
|
||||
import logging
|
||||
import os
|
||||
|
||||
import maya.cmds as cmds
|
||||
|
||||
from pype.hosts.maya.api import lib
|
||||
|
||||
from avalon import io, api
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_workfile():
|
||||
path = cmds.file(query=True, sceneName=True) or "untitled"
|
||||
return os.path.basename(path)
|
||||
|
||||
|
||||
def get_workfolder():
|
||||
return os.path.dirname(cmds.file(query=True, sceneName=True))
|
||||
|
||||
|
||||
def select(nodes):
|
||||
cmds.select(nodes)
|
||||
|
||||
|
||||
def get_namespace_from_node(node):
|
||||
"""Get the namespace from the given node
|
||||
|
||||
Args:
|
||||
node (str): name of the node
|
||||
|
||||
Returns:
|
||||
namespace (str)
|
||||
|
||||
"""
|
||||
parts = node.rsplit("|", 1)[-1].rsplit(":", 1)
|
||||
return parts[0] if len(parts) > 1 else u":"
|
||||
|
||||
|
||||
def list_descendents(nodes):
|
||||
"""Include full descendant hierarchy of given nodes.
|
||||
|
||||
This is a workaround to cmds.listRelatives(allDescendents=True) because
|
||||
this way correctly keeps children instance paths (see Maya documentation)
|
||||
|
||||
This fixes LKD-26: assignments not working as expected on instanced shapes.
|
||||
|
||||
Return:
|
||||
list: List of children descendents of nodes
|
||||
|
||||
"""
|
||||
result = []
|
||||
while True:
|
||||
nodes = cmds.listRelatives(nodes,
|
||||
fullPath=True)
|
||||
if nodes:
|
||||
result.extend(nodes)
|
||||
else:
|
||||
return result
|
||||
|
||||
|
||||
def get_selected_nodes():
|
||||
"""Get information from current selection"""
|
||||
|
||||
selection = cmds.ls(selection=True, long=True)
|
||||
hierarchy = list_descendents(selection)
|
||||
nodes = list(set(selection + hierarchy))
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
def get_all_asset_nodes():
|
||||
"""Get all assets from the scene, container based
|
||||
|
||||
Returns:
|
||||
list: list of dictionaries
|
||||
"""
|
||||
|
||||
host = api.registered_host()
|
||||
|
||||
nodes = []
|
||||
for container in host.ls():
|
||||
# We are not interested in looks but assets!
|
||||
if container["loader"] == "LookLoader":
|
||||
continue
|
||||
|
||||
# Gather all information
|
||||
container_name = container["objectName"]
|
||||
nodes += cmds.sets(container_name, query=True, nodesOnly=True) or []
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
def create_asset_id_hash(nodes):
|
||||
"""Create a hash based on cbId attribute value
|
||||
Args:
|
||||
nodes (list): a list of nodes
|
||||
|
||||
Returns:
|
||||
dict
|
||||
"""
|
||||
node_id_hash = defaultdict(list)
|
||||
for node in nodes:
|
||||
value = lib.get_id(node)
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
asset_id = value.split(":")[0]
|
||||
node_id_hash[asset_id].append(node)
|
||||
|
||||
return dict(node_id_hash)
|
||||
|
||||
|
||||
def create_items_from_nodes(nodes):
|
||||
"""Create an item for the view based the container and content of it
|
||||
|
||||
It fetches the look document based on the asset ID found in the content.
|
||||
The item will contain all important information for the tool to work.
|
||||
|
||||
If there is an asset ID which is not registered in the project's collection
|
||||
it will log a warning message.
|
||||
|
||||
Args:
|
||||
nodes (list): list of maya nodes
|
||||
|
||||
Returns:
|
||||
list of dicts
|
||||
|
||||
"""
|
||||
|
||||
asset_view_items = []
|
||||
|
||||
id_hashes = create_asset_id_hash(nodes)
|
||||
if not id_hashes:
|
||||
return asset_view_items
|
||||
|
||||
for _id, id_nodes in id_hashes.items():
|
||||
asset = io.find_one({"_id": io.ObjectId(_id)},
|
||||
projection={"name": True})
|
||||
|
||||
# Skip if asset id is not found
|
||||
if not asset:
|
||||
log.warning("Id not found in the database, skipping '%s'." % _id)
|
||||
log.warning("Nodes: %s" % id_nodes)
|
||||
continue
|
||||
|
||||
# Collect available look subsets for this asset
|
||||
looks = lib.list_looks(asset["_id"])
|
||||
|
||||
# Collect namespaces the asset is found in
|
||||
namespaces = set()
|
||||
for node in id_nodes:
|
||||
namespace = get_namespace_from_node(node)
|
||||
namespaces.add(namespace)
|
||||
|
||||
asset_view_items.append({"label": asset["name"],
|
||||
"asset": asset,
|
||||
"looks": looks,
|
||||
"namespaces": namespaces})
|
||||
|
||||
return asset_view_items
|
||||
|
||||
|
||||
def remove_unused_looks():
|
||||
"""Removes all loaded looks for which none of the shaders are used.
|
||||
|
||||
This will cleanup all loaded "LookLoader" containers that are unused in
|
||||
the current scene.
|
||||
|
||||
"""
|
||||
|
||||
host = api.registered_host()
|
||||
|
||||
unused = list()
|
||||
for container in host.ls():
|
||||
if container['loader'] == "LookLoader":
|
||||
members = cmds.sets(container['objectName'], query=True)
|
||||
look_sets = cmds.ls(members, type="objectSet")
|
||||
for look_set in look_sets:
|
||||
# If the set is used than we consider this look *in use*
|
||||
if cmds.sets(look_set, query=True):
|
||||
break
|
||||
else:
|
||||
unused.append(container)
|
||||
|
||||
for container in unused:
|
||||
log.info("Removing unused look container: %s", container['objectName'])
|
||||
api.remove(container)
|
||||
|
||||
log.info("Finished removing unused looks. (see log for details)")
|
||||
120
pype/tools/mayalookassigner/models.py
Normal file
120
pype/tools/mayalookassigner/models.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
from collections import defaultdict
|
||||
from avalon.tools import models
|
||||
|
||||
from avalon.vendor.Qt import QtCore
|
||||
from avalon.vendor import qtawesome
|
||||
from avalon.style import colors
|
||||
|
||||
|
||||
class AssetModel(models.TreeModel):
|
||||
|
||||
Columns = ["label"]
|
||||
|
||||
def add_items(self, items):
|
||||
"""
|
||||
Add items to model with needed data
|
||||
Args:
|
||||
items(list): collection of item data
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
self.beginResetModel()
|
||||
|
||||
# Add the items sorted by label
|
||||
sorter = lambda x: x["label"]
|
||||
|
||||
for item in sorted(items, key=sorter):
|
||||
|
||||
asset_item = models.Item()
|
||||
asset_item.update(item)
|
||||
asset_item["icon"] = "folder"
|
||||
|
||||
# Add namespace children
|
||||
namespaces = item["namespaces"]
|
||||
for namespace in sorted(namespaces):
|
||||
child = models.Item()
|
||||
child.update(item)
|
||||
child.update({
|
||||
"label": (namespace if namespace != ":"
|
||||
else "(no namespace)"),
|
||||
"namespace": namespace,
|
||||
"looks": item["looks"],
|
||||
"icon": "folder-o"
|
||||
})
|
||||
asset_item.add_child(child)
|
||||
|
||||
self.add_child(asset_item)
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
def data(self, index, role):
|
||||
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
if role == models.TreeModel.ItemRole:
|
||||
node = index.internalPointer()
|
||||
return node
|
||||
|
||||
# Add icon
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
if index.column() == 0:
|
||||
node = index.internalPointer()
|
||||
icon = node.get("icon")
|
||||
if icon:
|
||||
return qtawesome.icon("fa.{0}".format(icon),
|
||||
color=colors.default)
|
||||
|
||||
return super(AssetModel, self).data(index, role)
|
||||
|
||||
|
||||
class LookModel(models.TreeModel):
|
||||
"""Model displaying a list of looks and matches for assets"""
|
||||
|
||||
Columns = ["label", "match"]
|
||||
|
||||
def add_items(self, items):
|
||||
"""Add items to model with needed data
|
||||
|
||||
An item exists of:
|
||||
{
|
||||
"subset": 'name of subset',
|
||||
"asset": asset_document
|
||||
}
|
||||
|
||||
Args:
|
||||
items(list): collection of item data
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
self.beginResetModel()
|
||||
|
||||
# Collect the assets per look name (from the items of the AssetModel)
|
||||
look_subsets = defaultdict(list)
|
||||
for asset_item in items:
|
||||
asset = asset_item["asset"]
|
||||
for look in asset_item["looks"]:
|
||||
look_subsets[look["name"]].append(asset)
|
||||
|
||||
for subset, assets in sorted(look_subsets.iteritems()):
|
||||
|
||||
# Define nice label without "look" prefix for readability
|
||||
label = subset if not subset.startswith("look") else subset[4:]
|
||||
|
||||
item_node = models.Item()
|
||||
item_node["label"] = label
|
||||
item_node["subset"] = subset
|
||||
|
||||
# Amount of matching assets for this look
|
||||
item_node["match"] = len(assets)
|
||||
|
||||
# Store the assets that have this subset available
|
||||
item_node["assets"] = assets
|
||||
|
||||
self.add_child(item_node)
|
||||
|
||||
self.endResetModel()
|
||||
50
pype/tools/mayalookassigner/views.py
Normal file
50
pype/tools/mayalookassigner/views.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from avalon.vendor.Qt import QtWidgets, QtCore
|
||||
|
||||
|
||||
DEFAULT_COLOR = "#fb9c15"
|
||||
|
||||
|
||||
class View(QtWidgets.QTreeView):
|
||||
data_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(View, self).__init__(parent=parent)
|
||||
|
||||
# view settings
|
||||
self.setAlternatingRowColors(False)
|
||||
self.setSortingEnabled(True)
|
||||
self.setSelectionMode(self.ExtendedSelection)
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
|
||||
def get_indices(self):
|
||||
"""Get the selected rows"""
|
||||
selection_model = self.selectionModel()
|
||||
return selection_model.selectedRows()
|
||||
|
||||
def extend_to_children(self, indices):
|
||||
"""Extend the indices to the children indices.
|
||||
|
||||
Top-level indices are extended to its children indices. Sub-items
|
||||
are kept as is.
|
||||
|
||||
:param indices: The indices to extend.
|
||||
:type indices: list
|
||||
|
||||
:return: The children indices
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
subitems = set()
|
||||
for i in indices:
|
||||
valid_parent = i.parent().isValid()
|
||||
if valid_parent and i not in subitems:
|
||||
subitems.add(i)
|
||||
else:
|
||||
# is top level node
|
||||
model = i.model()
|
||||
rows = model.rowCount(parent=i)
|
||||
for row in range(rows):
|
||||
child = model.index(row, 0, parent=i)
|
||||
subitems.add(child)
|
||||
|
||||
return list(subitems)
|
||||
261
pype/tools/mayalookassigner/widgets.py
Normal file
261
pype/tools/mayalookassigner/widgets.py
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
from avalon.vendor.Qt import QtWidgets, QtCore
|
||||
|
||||
# TODO: expose this better in avalon core
|
||||
from avalon.tools import lib
|
||||
from avalon.tools.models import TreeModel
|
||||
|
||||
from . import models
|
||||
from . import commands
|
||||
from . import views
|
||||
|
||||
from maya import cmds
|
||||
|
||||
MODELINDEX = QtCore.QModelIndex()
|
||||
|
||||
|
||||
class AssetOutliner(QtWidgets.QWidget):
|
||||
|
||||
refreshed = QtCore.Signal()
|
||||
selection_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QtWidgets.QWidget.__init__(self, parent)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
title = QtWidgets.QLabel("Assets")
|
||||
title.setAlignment(QtCore.Qt.AlignCenter)
|
||||
title.setStyleSheet("font-weight: bold; font-size: 12px")
|
||||
|
||||
model = models.AssetModel()
|
||||
view = views.View()
|
||||
view.setModel(model)
|
||||
view.customContextMenuRequested.connect(self.right_mouse_menu)
|
||||
view.setSortingEnabled(False)
|
||||
view.setHeaderHidden(True)
|
||||
view.setIndentation(10)
|
||||
|
||||
from_all_asset_btn = QtWidgets.QPushButton("Get All Assets")
|
||||
from_selection_btn = QtWidgets.QPushButton("Get Assets From Selection")
|
||||
|
||||
layout.addWidget(title)
|
||||
layout.addWidget(from_all_asset_btn)
|
||||
layout.addWidget(from_selection_btn)
|
||||
layout.addWidget(view)
|
||||
|
||||
# Build connections
|
||||
from_selection_btn.clicked.connect(self.get_selected_assets)
|
||||
from_all_asset_btn.clicked.connect(self.get_all_assets)
|
||||
|
||||
selection_model = view.selectionModel()
|
||||
selection_model.selectionChanged.connect(self.selection_changed)
|
||||
|
||||
self.view = view
|
||||
self.model = model
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
self.log = logging.getLogger(__name__)
|
||||
|
||||
def clear(self):
|
||||
self.model.clear()
|
||||
|
||||
# fix looks remaining visible when no items present after "refresh"
|
||||
# todo: figure out why this workaround is needed.
|
||||
self.selection_changed.emit()
|
||||
|
||||
def add_items(self, items):
|
||||
"""Add new items to the outliner"""
|
||||
|
||||
self.model.add_items(items)
|
||||
self.refreshed.emit()
|
||||
|
||||
def get_selected_items(self):
|
||||
"""Get current selected items from view
|
||||
|
||||
Returns:
|
||||
list: list of dictionaries
|
||||
"""
|
||||
|
||||
selection_model = self.view.selectionModel()
|
||||
items = [row.data(TreeModel.ItemRole) for row in
|
||||
selection_model.selectedRows(0)]
|
||||
|
||||
return items
|
||||
|
||||
def get_all_assets(self):
|
||||
"""Add all items from the current scene"""
|
||||
|
||||
with lib.preserve_expanded_rows(self.view):
|
||||
with lib.preserve_selection(self.view):
|
||||
self.clear()
|
||||
nodes = commands.get_all_asset_nodes()
|
||||
items = commands.create_items_from_nodes(nodes)
|
||||
self.add_items(items)
|
||||
|
||||
return len(items) > 0
|
||||
|
||||
def get_selected_assets(self):
|
||||
"""Add all selected items from the current scene"""
|
||||
|
||||
with lib.preserve_expanded_rows(self.view):
|
||||
with lib.preserve_selection(self.view):
|
||||
self.clear()
|
||||
nodes = commands.get_selected_nodes()
|
||||
items = commands.create_items_from_nodes(nodes)
|
||||
self.add_items(items)
|
||||
|
||||
def get_nodes(self, selection=False):
|
||||
"""Find the nodes in the current scene per asset."""
|
||||
|
||||
items = self.get_selected_items()
|
||||
|
||||
# Collect all nodes by hash (optimization)
|
||||
if not selection:
|
||||
nodes = cmds.ls(dag=True, long=True)
|
||||
else:
|
||||
nodes = commands.get_selected_nodes()
|
||||
id_nodes = commands.create_asset_id_hash(nodes)
|
||||
|
||||
# Collect the asset item entries per asset
|
||||
# and collect the namespaces we'd like to apply
|
||||
assets = dict()
|
||||
asset_namespaces = defaultdict(set)
|
||||
for item in items:
|
||||
asset_id = str(item["asset"]["_id"])
|
||||
asset_name = item["asset"]["name"]
|
||||
asset_namespaces[asset_name].add(item.get("namespace"))
|
||||
|
||||
if asset_name in assets:
|
||||
continue
|
||||
|
||||
assets[asset_name] = item
|
||||
assets[asset_name]["nodes"] = id_nodes.get(asset_id, [])
|
||||
|
||||
# Filter nodes to namespace (if only namespaces were selected)
|
||||
for asset_name in assets:
|
||||
namespaces = asset_namespaces[asset_name]
|
||||
|
||||
# When None is present there should be no filtering
|
||||
if None in namespaces:
|
||||
continue
|
||||
|
||||
# Else only namespaces are selected and *not* the top entry so
|
||||
# we should filter to only those namespaces.
|
||||
nodes = assets[asset_name]["nodes"]
|
||||
nodes = [node for node in nodes if
|
||||
commands.get_namespace_from_node(node) in namespaces]
|
||||
assets[asset_name]["nodes"] = nodes
|
||||
|
||||
return assets
|
||||
|
||||
def select_asset_from_items(self):
|
||||
"""Select nodes from listed asset"""
|
||||
|
||||
items = self.get_nodes(selection=False)
|
||||
nodes = []
|
||||
for item in items.values():
|
||||
nodes.extend(item["nodes"])
|
||||
|
||||
commands.select(nodes)
|
||||
|
||||
def right_mouse_menu(self, pos):
|
||||
"""Build RMB menu for asset outliner"""
|
||||
|
||||
active = self.view.currentIndex() # index under mouse
|
||||
active = active.sibling(active.row(), 0) # get first column
|
||||
globalpos = self.view.viewport().mapToGlobal(pos)
|
||||
|
||||
menu = QtWidgets.QMenu(self.view)
|
||||
|
||||
# Direct assignment
|
||||
apply_action = QtWidgets.QAction(menu, text="Select nodes")
|
||||
apply_action.triggered.connect(self.select_asset_from_items)
|
||||
|
||||
if not active.isValid():
|
||||
apply_action.setEnabled(False)
|
||||
|
||||
menu.addAction(apply_action)
|
||||
|
||||
menu.exec_(globalpos)
|
||||
|
||||
|
||||
class LookOutliner(QtWidgets.QWidget):
|
||||
|
||||
menu_apply_action = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QtWidgets.QWidget.__init__(self, parent)
|
||||
|
||||
# look manager layout
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# Looks from database
|
||||
title = QtWidgets.QLabel("Looks")
|
||||
title.setAlignment(QtCore.Qt.AlignCenter)
|
||||
title.setStyleSheet("font-weight: bold; font-size: 12px")
|
||||
title.setAlignment(QtCore.Qt.AlignCenter)
|
||||
|
||||
model = models.LookModel()
|
||||
|
||||
# Proxy for dynamic sorting
|
||||
proxy = QtCore.QSortFilterProxyModel()
|
||||
proxy.setSourceModel(model)
|
||||
|
||||
view = views.View()
|
||||
view.setModel(proxy)
|
||||
view.setMinimumHeight(180)
|
||||
view.setToolTip("Use right mouse button menu for direct actions")
|
||||
view.customContextMenuRequested.connect(self.right_mouse_menu)
|
||||
view.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
layout.addWidget(title)
|
||||
layout.addWidget(view)
|
||||
|
||||
self.view = view
|
||||
self.model = model
|
||||
|
||||
def clear(self):
|
||||
self.model.clear()
|
||||
|
||||
def add_items(self, items):
|
||||
self.model.add_items(items)
|
||||
|
||||
def get_selected_items(self):
|
||||
"""Get current selected items from view
|
||||
|
||||
Returns:
|
||||
list: list of dictionaries
|
||||
"""
|
||||
|
||||
datas = [i.data(TreeModel.ItemRole) for i in self.view.get_indices()]
|
||||
items = [d for d in datas if d is not None] # filter Nones
|
||||
|
||||
return items
|
||||
|
||||
def right_mouse_menu(self, pos):
|
||||
"""Build RMB menu for look view"""
|
||||
|
||||
active = self.view.currentIndex() # index under mouse
|
||||
active = active.sibling(active.row(), 0) # get first column
|
||||
globalpos = self.view.viewport().mapToGlobal(pos)
|
||||
|
||||
if not active.isValid():
|
||||
return
|
||||
|
||||
menu = QtWidgets.QMenu(self.view)
|
||||
|
||||
# Direct assignment
|
||||
apply_action = QtWidgets.QAction(menu, text="Assign looks..")
|
||||
apply_action.triggered.connect(self.menu_apply_action)
|
||||
|
||||
menu.addAction(apply_action)
|
||||
|
||||
menu.exec_(globalpos)
|
||||
|
||||
|
||||
|
|
@ -340,11 +340,8 @@ class FamilyWidget(QtWidgets.QWidget):
|
|||
).distinct("name")
|
||||
|
||||
if versions:
|
||||
versions = sorted(
|
||||
[v for v in versions],
|
||||
key=lambda ver: ver['name']
|
||||
)
|
||||
version = int(versions[-1]['name']) + 1
|
||||
versions = sorted(versions)
|
||||
version = int(versions[-1]) + 1
|
||||
|
||||
self.version_spinbox.setValue(version)
|
||||
|
||||
|
|
|
|||
402
pype/tools/tray/pype_info_widget.py
Normal file
402
pype/tools/tray/pype_info_widget.py
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
import os
|
||||
import json
|
||||
import collections
|
||||
|
||||
from avalon import style
|
||||
from Qt import QtCore, QtGui, QtWidgets
|
||||
from pype.api import resources
|
||||
from pype.settings.lib import get_local_settings
|
||||
from pype.lib.pype_info import (
|
||||
get_all_current_info,
|
||||
get_pype_info,
|
||||
get_workstation_info,
|
||||
extract_pype_info_to_file
|
||||
)
|
||||
|
||||
IS_MAIN_ROLE = QtCore.Qt.UserRole
|
||||
|
||||
|
||||
class EnvironmentValueDelegate(QtWidgets.QStyledItemDelegate):
|
||||
def createEditor(self, parent, option, index):
|
||||
edit_widget = QtWidgets.QLineEdit(parent)
|
||||
edit_widget.setReadOnly(True)
|
||||
return edit_widget
|
||||
|
||||
|
||||
class EnvironmentsView(QtWidgets.QTreeView):
|
||||
def __init__(self, parent=None):
|
||||
super(EnvironmentsView, self).__init__(parent)
|
||||
|
||||
model = QtGui.QStandardItemModel()
|
||||
|
||||
env = os.environ.copy()
|
||||
keys = []
|
||||
values = []
|
||||
for key in sorted(env.keys()):
|
||||
key_item = QtGui.QStandardItem(key)
|
||||
key_item.setFlags(
|
||||
QtCore.Qt.ItemIsSelectable
|
||||
| QtCore.Qt.ItemIsEnabled
|
||||
)
|
||||
key_item.setData(True, IS_MAIN_ROLE)
|
||||
keys.append(key_item)
|
||||
|
||||
value = env[key]
|
||||
value_item = QtGui.QStandardItem(value)
|
||||
value_item.setData(True, IS_MAIN_ROLE)
|
||||
values.append(value_item)
|
||||
|
||||
value_parts = [
|
||||
part
|
||||
for part in value.split(os.pathsep) if part
|
||||
]
|
||||
if len(value_parts) < 2:
|
||||
continue
|
||||
|
||||
sub_parts = []
|
||||
for part_value in value_parts:
|
||||
part_item = QtGui.QStandardItem(part_value)
|
||||
part_item.setData(False, IS_MAIN_ROLE)
|
||||
sub_parts.append(part_item)
|
||||
key_item.appendRows(sub_parts)
|
||||
|
||||
model.appendColumn(keys)
|
||||
model.appendColumn(values)
|
||||
model.setHorizontalHeaderLabels(["Key", "Value"])
|
||||
|
||||
self.setModel(model)
|
||||
# self.setIndentation(0)
|
||||
delegate = EnvironmentValueDelegate(self)
|
||||
self.setItemDelegate(delegate)
|
||||
self.header().setSectionResizeMode(
|
||||
0, QtWidgets.QHeaderView.ResizeToContents
|
||||
)
|
||||
self.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection)
|
||||
|
||||
def get_selection_as_dict(self):
|
||||
indexes = self.selectionModel().selectedIndexes()
|
||||
|
||||
main_mapping = collections.defaultdict(dict)
|
||||
for index in indexes:
|
||||
is_main = index.data(IS_MAIN_ROLE)
|
||||
if not is_main:
|
||||
continue
|
||||
row = index.row()
|
||||
value = index.data(QtCore.Qt.DisplayRole)
|
||||
if index.column() == 0:
|
||||
key = "key"
|
||||
else:
|
||||
key = "value"
|
||||
main_mapping[row][key] = value
|
||||
|
||||
result = {}
|
||||
for item in main_mapping.values():
|
||||
result[item["key"]] = item["value"]
|
||||
return result
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if (
|
||||
event.type() == QtGui.QKeyEvent.KeyPress
|
||||
and event.matches(QtGui.QKeySequence.Copy)
|
||||
):
|
||||
selected_data = self.get_selection_as_dict()
|
||||
selected_str = json.dumps(selected_data, indent=4)
|
||||
|
||||
mime_data = QtCore.QMimeData()
|
||||
mime_data.setText(selected_str)
|
||||
QtWidgets.QApplication.instance().clipboard().setMimeData(
|
||||
mime_data
|
||||
)
|
||||
event.accept()
|
||||
else:
|
||||
return super(EnvironmentsView, self).keyPressEvent(event)
|
||||
|
||||
|
||||
class ClickableWidget(QtWidgets.QWidget):
|
||||
clicked = QtCore.Signal()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.clicked.emit()
|
||||
super(ClickableWidget, self).mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class CollapsibleWidget(QtWidgets.QWidget):
|
||||
def __init__(self, label, parent):
|
||||
super(CollapsibleWidget, self).__init__(parent)
|
||||
|
||||
self.content_widget = None
|
||||
|
||||
top_part = ClickableWidget(parent=self)
|
||||
|
||||
button_size = QtCore.QSize(5, 5)
|
||||
button_toggle = QtWidgets.QToolButton(parent=top_part)
|
||||
button_toggle.setIconSize(button_size)
|
||||
button_toggle.setArrowType(QtCore.Qt.RightArrow)
|
||||
button_toggle.setCheckable(True)
|
||||
button_toggle.setChecked(False)
|
||||
|
||||
label_widget = QtWidgets.QLabel(label, parent=top_part)
|
||||
spacer_widget = QtWidgets.QWidget(top_part)
|
||||
|
||||
top_part_layout = QtWidgets.QHBoxLayout(top_part)
|
||||
top_part_layout.setContentsMargins(0, 0, 0, 5)
|
||||
top_part_layout.addWidget(button_toggle)
|
||||
top_part_layout.addWidget(label_widget)
|
||||
top_part_layout.addWidget(spacer_widget, 1)
|
||||
|
||||
label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
self.button_toggle = button_toggle
|
||||
self.label_widget = label_widget
|
||||
|
||||
top_part.clicked.connect(self._top_part_clicked)
|
||||
self.button_toggle.clicked.connect(self._btn_clicked)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(0)
|
||||
main_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
main_layout.addWidget(top_part)
|
||||
|
||||
self.main_layout = main_layout
|
||||
|
||||
def set_content_widget(self, content_widget):
|
||||
content_widget.setVisible(self.button_toggle.isChecked())
|
||||
self.main_layout.addWidget(content_widget)
|
||||
self.content_widget = content_widget
|
||||
|
||||
def _btn_clicked(self):
|
||||
self.toggle_content(self.button_toggle.isChecked())
|
||||
|
||||
def _top_part_clicked(self):
|
||||
self.toggle_content()
|
||||
|
||||
def toggle_content(self, *args):
|
||||
if len(args) > 0:
|
||||
checked = args[0]
|
||||
else:
|
||||
checked = not self.button_toggle.isChecked()
|
||||
arrow_type = QtCore.Qt.RightArrow
|
||||
if checked:
|
||||
arrow_type = QtCore.Qt.DownArrow
|
||||
self.button_toggle.setChecked(checked)
|
||||
self.button_toggle.setArrowType(arrow_type)
|
||||
if self.content_widget:
|
||||
self.content_widget.setVisible(checked)
|
||||
self.parent().updateGeometry()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(CollapsibleWidget, self).resizeEvent(event)
|
||||
if self.content_widget:
|
||||
self.content_widget.updateGeometry()
|
||||
|
||||
|
||||
class PypeInfoWidget(QtWidgets.QWidget):
|
||||
not_applicable = "N/A"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(PypeInfoWidget, self).__init__(parent)
|
||||
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
self.setWindowTitle("Pype info")
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
main_layout.setAlignment(QtCore.Qt.AlignTop)
|
||||
main_layout.addWidget(self._create_pype_info_widget(), 0)
|
||||
main_layout.addWidget(self._create_separator(), 0)
|
||||
main_layout.addWidget(self._create_workstation_widget(), 0)
|
||||
main_layout.addWidget(self._create_separator(), 0)
|
||||
main_layout.addWidget(self._create_local_settings_widget(), 0)
|
||||
main_layout.addWidget(self._create_separator(), 0)
|
||||
main_layout.addWidget(self._create_environ_widget(), 1)
|
||||
main_layout.addWidget(self._create_btns_section(), 0)
|
||||
|
||||
def _create_btns_section(self):
|
||||
btns_widget = QtWidgets.QWidget(self)
|
||||
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
|
||||
btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
copy_to_clipboard_btn = QtWidgets.QPushButton(
|
||||
"Copy to clipboard", btns_widget
|
||||
)
|
||||
export_to_file_btn = QtWidgets.QPushButton(
|
||||
"Export", btns_widget
|
||||
)
|
||||
btns_layout.addWidget(QtWidgets.QWidget(btns_widget), 1)
|
||||
btns_layout.addWidget(copy_to_clipboard_btn)
|
||||
btns_layout.addWidget(export_to_file_btn)
|
||||
|
||||
copy_to_clipboard_btn.clicked.connect(self._on_copy_to_clipboard)
|
||||
export_to_file_btn.clicked.connect(self._on_export_to_file)
|
||||
|
||||
return btns_widget
|
||||
|
||||
def _on_export_to_file(self):
|
||||
dst_dir_path = QtWidgets.QFileDialog.getExistingDirectory(
|
||||
self,
|
||||
"Choose directory",
|
||||
os.path.expanduser("~"),
|
||||
QtWidgets.QFileDialog.ShowDirsOnly
|
||||
)
|
||||
if not dst_dir_path or not os.path.exists(dst_dir_path):
|
||||
return
|
||||
|
||||
filepath = extract_pype_info_to_file(dst_dir_path)
|
||||
title = "Extraction done"
|
||||
message = "Extraction is done. Destination filepath is \"{}\"".format(
|
||||
filepath.replace("\\", "/")
|
||||
)
|
||||
dialog = QtWidgets.QMessageBox(self)
|
||||
dialog.setIcon(QtWidgets.QMessageBox.NoIcon)
|
||||
dialog.setWindowTitle(title)
|
||||
dialog.setText(message)
|
||||
dialog.exec_()
|
||||
|
||||
def _on_copy_to_clipboard(self):
|
||||
all_data = get_all_current_info()
|
||||
all_data_str = json.dumps(all_data, indent=4)
|
||||
|
||||
mime_data = QtCore.QMimeData()
|
||||
mime_data.setText(all_data_str)
|
||||
QtWidgets.QApplication.instance().clipboard().setMimeData(
|
||||
mime_data
|
||||
)
|
||||
|
||||
def _create_separator(self):
|
||||
separator_widget = QtWidgets.QWidget(self)
|
||||
separator_widget.setStyleSheet("background: #222222;")
|
||||
separator_widget.setMinimumHeight(2)
|
||||
separator_widget.setMaximumHeight(2)
|
||||
return separator_widget
|
||||
|
||||
def _create_workstation_widget(self):
|
||||
key_label_mapping = {
|
||||
"system_name": "System:",
|
||||
"local_id": "Local ID:",
|
||||
"username": "Username:",
|
||||
"hostname": "Hostname:",
|
||||
"hostip": "Host IP:"
|
||||
}
|
||||
keys_order = [
|
||||
"system_name",
|
||||
"local_id",
|
||||
"username",
|
||||
"hostname",
|
||||
"hostip"
|
||||
]
|
||||
workstation_info = get_workstation_info()
|
||||
for key in workstation_info.keys():
|
||||
if key not in keys_order:
|
||||
keys_order.append(key)
|
||||
|
||||
wokstation_info_widget = CollapsibleWidget("Workstation info", self)
|
||||
|
||||
info_widget = QtWidgets.QWidget(self)
|
||||
info_layout = QtWidgets.QGridLayout(info_widget)
|
||||
# Add spacer to 3rd column
|
||||
info_layout.addWidget(QtWidgets.QWidget(info_widget), 0, 2)
|
||||
info_layout.setColumnStretch(2, 1)
|
||||
|
||||
for key in keys_order:
|
||||
if key not in workstation_info:
|
||||
continue
|
||||
|
||||
label = key_label_mapping.get(key, key)
|
||||
value = workstation_info[key]
|
||||
row = info_layout.rowCount()
|
||||
info_layout.addWidget(
|
||||
QtWidgets.QLabel(label), row, 0, 1, 1
|
||||
)
|
||||
value_label = QtWidgets.QLabel(value)
|
||||
value_label.setTextInteractionFlags(
|
||||
QtCore.Qt.TextSelectableByMouse
|
||||
)
|
||||
info_layout.addWidget(
|
||||
value_label, row, 1, 1, 1
|
||||
)
|
||||
|
||||
wokstation_info_widget.set_content_widget(info_widget)
|
||||
|
||||
return wokstation_info_widget
|
||||
|
||||
def _create_local_settings_widget(self):
|
||||
local_settings = get_local_settings()
|
||||
|
||||
local_settings_widget = CollapsibleWidget("Local settings", self)
|
||||
|
||||
settings_input = QtWidgets.QPlainTextEdit(local_settings_widget)
|
||||
settings_input.setReadOnly(True)
|
||||
settings_input.setPlainText(json.dumps(local_settings, indent=4))
|
||||
|
||||
local_settings_widget.set_content_widget(settings_input)
|
||||
|
||||
return local_settings_widget
|
||||
|
||||
def _create_environ_widget(self):
|
||||
env_widget = CollapsibleWidget("Environments", self)
|
||||
|
||||
env_view = EnvironmentsView(env_widget)
|
||||
|
||||
env_widget.set_content_widget(env_view)
|
||||
|
||||
return env_widget
|
||||
|
||||
def _create_pype_info_widget(self):
|
||||
"""Create widget with information about pype application."""
|
||||
|
||||
# Get pype info data
|
||||
pype_info = get_pype_info()
|
||||
# Modify version key/values
|
||||
version_value = "{} ({})".format(
|
||||
pype_info.pop("version", self.not_applicable),
|
||||
pype_info.pop("version_type", self.not_applicable)
|
||||
)
|
||||
pype_info["version_value"] = version_value
|
||||
# Prepare lable mapping
|
||||
key_label_mapping = {
|
||||
"version_value": "Pype version:",
|
||||
"executable": "Pype executable:",
|
||||
"pype_root": "Pype location:",
|
||||
"mongo_url": "Pype Mongo URL:"
|
||||
}
|
||||
# Prepare keys order
|
||||
keys_order = ["version_value", "executable", "pype_root", "mongo_url"]
|
||||
for key in pype_info.keys():
|
||||
if key not in keys_order:
|
||||
keys_order.append(key)
|
||||
|
||||
# Create widgets
|
||||
info_widget = QtWidgets.QWidget(self)
|
||||
info_layout = QtWidgets.QGridLayout(info_widget)
|
||||
# Add spacer to 3rd column
|
||||
info_layout.addWidget(QtWidgets.QWidget(info_widget), 0, 2)
|
||||
info_layout.setColumnStretch(2, 1)
|
||||
|
||||
title_label = QtWidgets.QLabel(info_widget)
|
||||
title_label.setText("Application information")
|
||||
title_label.setStyleSheet("font-weight: bold;")
|
||||
info_layout.addWidget(title_label, 0, 0, 1, 2)
|
||||
|
||||
for key in keys_order:
|
||||
if key not in pype_info:
|
||||
continue
|
||||
value = pype_info[key]
|
||||
label = key_label_mapping.get(key, key)
|
||||
row = info_layout.rowCount()
|
||||
info_layout.addWidget(
|
||||
QtWidgets.QLabel(label), row, 0, 1, 1
|
||||
)
|
||||
value_label = QtWidgets.QLabel(value)
|
||||
value_label.setTextInteractionFlags(
|
||||
QtCore.Qt.TextSelectableByMouse
|
||||
)
|
||||
info_layout.addWidget(
|
||||
value_label, row, 1, 1, 1
|
||||
)
|
||||
return info_widget
|
||||
|
|
@ -3,15 +3,12 @@ import sys
|
|||
|
||||
import platform
|
||||
from avalon import style
|
||||
from Qt import QtCore, QtGui, QtWidgets, QtSvg
|
||||
from Qt import QtCore, QtGui, QtWidgets
|
||||
from pype.api import Logger, resources
|
||||
from pype.modules import TrayModulesManager, ITrayService
|
||||
from pype.settings.lib import get_system_settings
|
||||
import pype.version
|
||||
try:
|
||||
import configparser
|
||||
except Exception:
|
||||
import ConfigParser as configparser
|
||||
from .pype_info_widget import PypeInfoWidget
|
||||
|
||||
|
||||
class TrayManager:
|
||||
|
|
@ -19,13 +16,14 @@ class TrayManager:
|
|||
|
||||
Load submenus, actions, separators and modules into tray's context.
|
||||
"""
|
||||
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.pype_info_widget = None
|
||||
|
||||
self.log = Logger.get_logger(self.__class__.__name__)
|
||||
|
||||
self.module_settings = get_system_settings()["modules"]
|
||||
|
||||
|
|
@ -36,7 +34,7 @@ class TrayManager:
|
|||
def initialize_modules(self):
|
||||
"""Add modules to tray."""
|
||||
|
||||
self.modules_manager.initialize(self.tray_widget.menu)
|
||||
self.modules_manager.initialize(self, self.tray_widget.menu)
|
||||
|
||||
# Add services if they are
|
||||
services_submenu = ITrayService.services_submenu(self.tray_widget.menu)
|
||||
|
|
@ -58,6 +56,26 @@ class TrayManager:
|
|||
# Print time report
|
||||
self.modules_manager.print_report()
|
||||
|
||||
def show_tray_message(self, title, message, icon=None, msecs=None):
|
||||
"""Show tray message.
|
||||
|
||||
Args:
|
||||
title (str): Title of message.
|
||||
message (str): Content of message.
|
||||
icon (QSystemTrayIcon.MessageIcon): Message's icon. Default is
|
||||
Information icon, may differ by Qt version.
|
||||
msecs (int): Duration of message visibility in miliseconds.
|
||||
Default is 10000 msecs, may differ by Qt version.
|
||||
"""
|
||||
args = [title, message]
|
||||
kwargs = {}
|
||||
if icon:
|
||||
kwargs["icon"] = icon
|
||||
if msecs:
|
||||
kwargs["msecs"] = msecs
|
||||
|
||||
self.tray_widget.showMessage(*args, **kwargs)
|
||||
|
||||
def _add_version_item(self):
|
||||
subversion = os.environ.get("PYPE_SUBVERSION")
|
||||
client_name = os.environ.get("PYPE_CLIENT")
|
||||
|
|
@ -70,12 +88,21 @@ class TrayManager:
|
|||
version_string += ", {}".format(client_name)
|
||||
|
||||
version_action = QtWidgets.QAction(version_string, self.tray_widget)
|
||||
version_action.triggered.connect(self._on_version_action)
|
||||
self.tray_widget.menu.addAction(version_action)
|
||||
self.tray_widget.menu.addSeparator()
|
||||
|
||||
def on_exit(self):
|
||||
self.modules_manager.on_exit()
|
||||
|
||||
def _on_version_action(self):
|
||||
if self.pype_info_widget is None:
|
||||
self.pype_info_widget = PypeInfoWidget()
|
||||
|
||||
self.pype_info_widget.show()
|
||||
self.pype_info_widget.raise_()
|
||||
self.pype_info_widget.activateWindow()
|
||||
|
||||
|
||||
class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
|
||||
"""Tray widget.
|
||||
|
|
@ -85,9 +112,9 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
|
|||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
self.icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
icon = QtGui.QIcon(resources.pype_icon_filepath())
|
||||
|
||||
QtWidgets.QSystemTrayIcon.__init__(self, self.icon, parent)
|
||||
super(SystemTrayIcon, self).__init__(icon, parent)
|
||||
|
||||
# Store parent - QtWidgets.QMainWindow()
|
||||
self.parent = parent
|
||||
|
|
@ -100,15 +127,15 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
|
|||
self.tray_man = TrayManager(self, self.parent)
|
||||
self.tray_man.initialize_modules()
|
||||
|
||||
# Catch activate event
|
||||
self.activated.connect(self.on_systray_activated)
|
||||
# Catch activate event for left click if not on MacOS
|
||||
# - MacOS has this ability by design so menu would be doubled
|
||||
if platform.system().lower() != "darwin":
|
||||
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)
|
||||
|
|
@ -128,119 +155,24 @@ class TrayMainWindow(QtWidgets.QMainWindow):
|
|||
|
||||
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__()
|
||||
super(TrayMainWindow, self).__init__()
|
||||
self.app = app
|
||||
|
||||
self.set_working_widget()
|
||||
|
||||
self.trayIcon = SystemTrayIcon(self)
|
||||
self.trayIcon.show()
|
||||
|
||||
def set_working_widget(self):
|
||||
image_file = resources.get_resource("icons", "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
|
||||
self.tray_widget = SystemTrayIcon(self)
|
||||
self.tray_widget.show()
|
||||
|
||||
|
||||
class PypeTrayApplication(QtWidgets.QApplication):
|
||||
"""Qt application manages application's control flow."""
|
||||
|
||||
def __init__(self):
|
||||
super(self.__class__, self).__init__(sys.argv)
|
||||
super(PypeTrayApplication, self).__init__(sys.argv)
|
||||
# Allows to close widgets without exiting app
|
||||
self.setQuitOnLastWindowClosed(False)
|
||||
|
||||
# Allow show icon istead of python icon in task bar (Windows)
|
||||
if os.name == "nt":
|
||||
import ctypes
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
|
||||
u"pype_tray"
|
||||
)
|
||||
|
||||
# Sets up splash
|
||||
splash_widget = self.set_splash()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue