Merge branch 'develop' into feature/igniter-improvements

This commit is contained in:
Milan Kolar 2021-03-10 17:33:27 +01:00
commit 5c441aaf98
45 changed files with 3052 additions and 1525 deletions

3
.gitmodules vendored
View file

@ -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

View file

@ -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()

View file

@ -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']

View file

@ -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")

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -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"]

View file

@ -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)

View file

@ -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")

View file

@ -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,
})

View file

@ -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))

View file

@ -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

View file

@ -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

View 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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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
View 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

View file

@ -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:

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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": {

View file

@ -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": []
}
}
}
}

View file

@ -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",

View file

@ -45,6 +45,11 @@
"type": "schema",
"name": "schema_nuke_publish",
"template_data": []
},
{
"type": "schema",
"name": "schema_nuke_load",
"template_data": []
},
{
"type": "schema",

View file

@ -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
}
]
}
]
}

View file

@ -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,

View file

@ -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"
}
]
}
]
}

View file

@ -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
}
]
}
]
}
]
}
]

View 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.

View file

@ -0,0 +1,9 @@
from .app import (
App,
show
)
__all__ = [
"App",
"show"]

View 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

View 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)")

View 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()

View 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)

View 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)

View file

@ -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)

View 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

View file

@ -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()