Merge branch '2.x/develop' into feature/239-resolve_tagging_for_publish

This commit is contained in:
Jakub Jezek 2020-07-10 10:59:16 +02:00
commit b1afb626a6
No known key found for this signature in database
GPG key ID: C4B96E101D2A47F3
54 changed files with 1466 additions and 495 deletions

View file

@ -12,6 +12,8 @@ from pypeapp.lib.mongo import (
get_default_components
)
from . import resources
from .plugin import (
Extractor,
@ -54,6 +56,8 @@ __all__ = [
"compose_url",
"get_default_components",
# Resources
"resources",
# plugin classes
"Extractor",
# ordering

View file

@ -5,6 +5,8 @@ import traceback
from avalon import api as avalon
from pyblish import api as pyblish
import bpy
from pype import PLUGINS_DIR
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "blender", "publish")
@ -25,6 +27,9 @@ def install():
avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH))
avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH))
avalon.on("new", on_new)
avalon.on("open", on_open)
def uninstall():
"""Uninstall Blender configuration for Avalon."""
@ -32,3 +37,24 @@ def uninstall():
pyblish.deregister_plugin_path(str(PUBLISH_PATH))
avalon.deregister_plugin_path(avalon.Loader, str(LOAD_PATH))
avalon.deregister_plugin_path(avalon.Creator, str(CREATE_PATH))
def set_start_end_frames():
from avalon import io
asset_name = io.Session["AVALON_ASSET"]
asset_doc = io.find_one({
"type": "asset",
"name": asset_name
})
bpy.context.scene.frame_start = asset_doc["data"]["frameStart"]
bpy.context.scene.frame_end = asset_doc["data"]["frameEnd"]
def on_new(arg1, arg2):
set_start_end_frames()
def on_open(arg1, arg2):
set_start_end_frames()

View file

@ -14,12 +14,41 @@ def asset_name(
asset: str, subset: str, namespace: Optional[str] = None
) -> str:
"""Return a consistent name for an asset."""
name = f"{asset}_{subset}"
name = f"{asset}"
if namespace:
name = f"{namespace}:{name}"
name = f"{name}_{namespace}"
name = f"{name}_{subset}"
return name
def get_unique_number(
asset: str, subset: str
) -> str:
"""Return a unique number based on the asset name."""
avalon_containers = [
c for c in bpy.data.collections
if c.name == 'AVALON_CONTAINERS'
]
loaded_assets = []
for c in avalon_containers:
loaded_assets.extend(c.children)
collections_names = [
c.name for c in loaded_assets
]
count = 1
name = f"{asset}_{count:0>2}_{subset}_CON"
while name in collections_names:
count += 1
name = f"{asset}_{count:0>2}_{subset}_CON"
return f"{count:0>2}"
def prepare_data(data, container_name):
name = data.name
data = data.make_local()
data.name = f"{name}:{container_name}"
def create_blender_context(active: Optional[bpy.types.Object] = None,
selected: Optional[bpy.types.Object] = None,):
"""Create a new Blender context. If an object is passed as
@ -47,6 +76,25 @@ def create_blender_context(active: Optional[bpy.types.Object] = None,
raise Exception("Could not create a custom Blender context.")
def get_parent_collection(collection):
"""Get the parent of the input collection"""
check_list = [bpy.context.scene.collection]
for c in check_list:
if collection.name in c.children.keys():
return c
check_list.extend(c.children)
return None
def get_local_collection_with_name(name):
for collection in bpy.data.collections:
if collection.name == name and collection.library is None:
return collection
return None
class AssetLoader(api.Loader):
"""A basic AssetLoader for Blender

View file

@ -1,8 +1,9 @@
import os
import sys
from avalon import api, harmony
from avalon import api, io, harmony
from avalon.vendor import Qt
import avalon.tools.sceneinventory
import pyblish.api
from pype import lib
@ -92,6 +93,61 @@ def ensure_scene_settings():
set_scene_settings(valid_settings)
def check_inventory():
if not lib.any_outdated():
return
host = avalon.api.registered_host()
outdated_containers = []
for container in host.ls():
representation = container['representation']
representation_doc = io.find_one(
{
"_id": io.ObjectId(representation),
"type": "representation"
},
projection={"parent": True}
)
if representation_doc and not lib.is_latest(representation_doc):
outdated_containers.append(container)
# Colour nodes.
func = """function func(args){
for( var i =0; i <= args[0].length - 1; ++i)
{
var red_color = new ColorRGBA(255, 0, 0, 255);
node.setColor(args[0][i], red_color);
}
}
func
"""
outdated_nodes = []
for container in outdated_containers:
if container["loader"] == "ImageSequenceLoader":
outdated_nodes.append(
harmony.find_node_by_name(container["name"], "READ")
)
harmony.send({"function": func, "args": [outdated_nodes]})
# Warn about outdated containers.
print("Starting new QApplication..")
app = Qt.QtWidgets.QApplication(sys.argv)
message_box = Qt.QtWidgets.QMessageBox()
message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning)
msg = "There are outdated containers in the scene."
message_box.setText(msg)
message_box.exec_()
# Garbage collect QApplication.
del app
def application_launch():
ensure_scene_settings()
check_inventory()
def export_template(backdrops, nodes, filepath):
func = """function func(args)
{
@ -161,7 +217,7 @@ def install():
"instanceToggled", on_pyblish_instance_toggled
)
api.on("application.launched", ensure_scene_settings)
api.on("application.launched", application_launch)
def on_pyblish_instance_toggled(instance, old_value, new_value):

View file

@ -6,8 +6,9 @@ from pype.api import Logger
log = Logger().get_logger(__name__, "nukestudio")
def file_extensions():
return [".hrox"]
return api.HOST_WORKFILE_EXTENSIONS["nukestudio"]
def has_unsaved_changes():

View file

@ -1,9 +1,48 @@
import os
import sys
from avalon import api
from avalon import api, io
from avalon.vendor import Qt
from pype import lib
import pyblish.api
def check_inventory():
if not lib.any_outdated():
return
host = api.registered_host()
outdated_containers = []
for container in host.ls():
representation = container['representation']
representation_doc = io.find_one(
{
"_id": io.ObjectId(representation),
"type": "representation"
},
projection={"parent": True}
)
if representation_doc and not lib.is_latest(representation_doc):
outdated_containers.append(container)
# Warn about outdated containers.
print("Starting new QApplication..")
app = Qt.QtWidgets.QApplication(sys.argv)
message_box = Qt.QtWidgets.QMessageBox()
message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning)
msg = "There are outdated containers in the scene."
message_box.setText(msg)
message_box.exec_()
# Garbage collect QApplication.
del app
def application_launch():
check_inventory()
def install():
print("Installing Pype config...")
@ -27,6 +66,8 @@ def install():
"instanceToggled", on_pyblish_instance_toggled
)
api.on("application.launched", application_launch)
def on_pyblish_instance_toggled(instance, old_value, new_value):
"""Toggle layer visibility on instance toggles."""

View file

@ -1,5 +1,6 @@
from Qt import QtCore, QtGui, QtWidgets
from avalon import style
from pype.api import resources
class MessageWidget(QtWidgets.QWidget):
@ -19,8 +20,8 @@ class MessageWidget(QtWidgets.QWidget):
if parent and hasattr(parent, 'icon'):
self.setWindowIcon(parent.icon)
else:
from pypeapp.resources import get_resource
self.setWindowIcon(QtGui.QIcon(get_resource('icon.png')))
icon = QtGui.QIcon(resources.pype_icon_filepath())
self.setWindowIcon(icon)
self.setWindowFlags(
QtCore.Qt.WindowCloseButtonHint |

View file

@ -1,6 +1,7 @@
import os
from Qt import QtCore, QtGui, QtWidgets
from avalon import style
from pype.api import resources
class ClockifySettings(QtWidgets.QWidget):
@ -26,10 +27,7 @@ class ClockifySettings(QtWidgets.QWidget):
elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'):
self.setWindowIcon(self.parent.parent.icon)
else:
pype_setup = os.getenv('PYPE_SETUP_PATH')
items = [pype_setup, "app", "resources", "icon.png"]
fname = os.path.sep.join(items)
icon = QtGui.QIcon(fname)
icon = QtGui.QIcon(resources.pype_icon_filepath())
self.setWindowIcon(icon)
self.setWindowFlags(

View file

@ -26,7 +26,7 @@ from pype.api import (
compose_url
)
from pype.modules.ftrack.lib.custom_db_connector import DbConnector
from pype.modules.ftrack.lib.custom_db_connector import CustomDbConnector
TOPIC_STATUS_SERVER = "pype.event.server.status"
@ -44,15 +44,8 @@ def get_ftrack_event_mongo_info():
mongo_url = os.environ.get("FTRACK_EVENTS_MONGO_URL")
if mongo_url is not None:
components = decompose_url(mongo_url)
_used_ftrack_url = True
else:
components = get_default_components()
_used_ftrack_url = False
if not _used_ftrack_url or components["database"] is None:
components["database"] = database_name
components.pop("collection", None)
uri = compose_url(**components)
@ -166,10 +159,10 @@ class ProcessEventHub(SocketBaseEventHub):
pypelog = Logger().get_logger("Session Processor")
def __init__(self, *args, **kwargs):
self.dbcon = DbConnector(
self.dbcon = CustomDbConnector(
self.uri,
self.port,
self.database,
self.port,
self.table_name
)
super(ProcessEventHub, self).__init__(*args, **kwargs)

View file

@ -12,7 +12,7 @@ from pype.modules.ftrack.ftrack_server.lib import (
get_ftrack_event_mongo_info,
TOPIC_STATUS_SERVER, TOPIC_STATUS_SERVER_RESULT
)
from pype.modules.ftrack.lib.custom_db_connector import DbConnector
from pype.modules.ftrack.lib.custom_db_connector import CustomDbConnector
from pype.api import Logger
log = Logger().get_logger("Event storer")
@ -24,7 +24,7 @@ class SessionFactory:
uri, port, database, table_name = get_ftrack_event_mongo_info()
dbcon = DbConnector(uri, port, database, table_name)
dbcon = CustomDbConnector(uri, database, port, table_name)
# ignore_topics = ["ftrack.meta.connected"]
ignore_topics = []

View file

@ -9,6 +9,7 @@ import time
import logging
import functools
import atexit
import os
# Third-party dependencies
import pymongo
@ -40,7 +41,7 @@ def auto_reconnect(func):
def check_active_table(func):
"""Check if DbConnector has active table before db method is called"""
"""Check if CustomDbConnector has active collection."""
@functools.wraps(func)
def decorated(obj, *args, **kwargs):
if not obj.active_table:
@ -49,23 +50,12 @@ def check_active_table(func):
return decorated
def check_active_table(func):
"""Handling auto reconnect in 3 retry times"""
@functools.wraps(func)
def decorated(obj, *args, **kwargs):
if not obj.active_table:
raise NotActiveTable("Active table is not set. (This is bug)")
return func(obj, *args, **kwargs)
return decorated
class DbConnector:
class CustomDbConnector:
log = logging.getLogger(__name__)
timeout = 1000
timeout = int(os.environ["AVALON_TIMEOUT"])
def __init__(
self, uri, port=None, database_name=None, table_name=None
self, uri, database_name, port=None, table_name=None
):
self._mongo_client = None
self._sentry_client = None
@ -78,9 +68,6 @@ class DbConnector:
if port is None:
port = components.get("port")
if database_name is None:
database_name = components.get("database")
if database_name is None:
raise ValueError(
"Database is not defined for connection. {}".format(uri)
@ -99,7 +86,7 @@ class DbConnector:
# not all methods of PyMongo database are implemented with this it is
# possible to use them too
try:
return super(DbConnector, self).__getattribute__(attr)
return super(CustomDbConnector, self).__getattribute__(attr)
except AttributeError:
if self.active_table is None:
raise NotActiveTable()

View file

@ -4,9 +4,11 @@ import copy
import platform
import avalon.lib
import acre
import getpass
from pype import lib as pypelib
from pype.api import config, Anatomy
from .ftrack_action_handler import BaseAction
from avalon.api import last_workfile, HOST_WORKFILE_EXTENSIONS
class AppAction(BaseAction):
@ -152,10 +154,11 @@ class AppAction(BaseAction):
hierarchy = ""
asset_doc_parents = asset_document["data"].get("parents")
if len(asset_doc_parents) > 0:
if asset_doc_parents:
hierarchy = os.path.join(*asset_doc_parents)
application = avalon.lib.get_application(self.identifier)
host_name = application["application_dir"]
data = {
"project": {
"name": entity["project"]["full_name"],
@ -163,7 +166,7 @@ class AppAction(BaseAction):
},
"task": entity["name"],
"asset": asset_name,
"app": application["application_dir"],
"app": host_name,
"hierarchy": hierarchy
}
@ -187,6 +190,21 @@ class AppAction(BaseAction):
except FileExistsError:
pass
last_workfile_path = None
extensions = HOST_WORKFILE_EXTENSIONS.get(host_name)
if extensions:
# Find last workfile
file_template = anatomy.templates["work"]["file"]
data.update({
"version": 1,
"user": getpass.getuser(),
"ext": extensions[0]
})
last_workfile_path = last_workfile(
workdir, file_template, data, extensions, True
)
# set environments for Avalon
prep_env = copy.deepcopy(os.environ)
prep_env.update({
@ -198,6 +216,8 @@ class AppAction(BaseAction):
"AVALON_HIERARCHY": hierarchy,
"AVALON_WORKDIR": workdir
})
if last_workfile_path:
prep_env["AVALON_LAST_WORKFILE"] = last_workfile_path
prep_env.update(anatomy.roots_obj.root_environments())
# collect all parents from the task
@ -213,7 +233,6 @@ class AppAction(BaseAction):
tools_env = acre.get_tools(tools_attr)
env = acre.compute(tools_env)
env = acre.merge(env, current_env=dict(prep_env))
env = acre.append(dict(prep_env), env)
# Get path to execute
st_temp_path = os.environ["PYPE_CONFIG"]

View file

@ -3,6 +3,7 @@ import requests
from avalon import style
from pype.modules.ftrack import credentials
from . import login_tools
from pype.api import resources
from Qt import QtCore, QtGui, QtWidgets
@ -29,10 +30,7 @@ class Login_Dialog_ui(QtWidgets.QWidget):
elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'):
self.setWindowIcon(self.parent.parent.icon)
else:
pype_setup = os.getenv('PYPE_SETUP_PATH')
items = [pype_setup, "app", "resources", "icon.png"]
fname = os.path.sep.join(items)
icon = QtGui.QIcon(fname)
icon = QtGui.QIcon(resources.pype_icon_filepath())
self.setWindowIcon(icon)
self.setWindowFlags(

View file

@ -1,26 +1,25 @@
import time
import collections
from Qt import QtCore
import threading
from pynput import mouse, keyboard
from pype.api import Logger
class IdleManager(QtCore.QThread):
class IdleManager(threading.Thread):
""" Measure user's idle time in seconds.
Idle time resets on keyboard/mouse input.
Is able to emit signals at specific time idle.
"""
time_signals = collections.defaultdict(list)
time_callbacks = collections.defaultdict(list)
idle_time = 0
signal_reset_timer = QtCore.Signal()
def __init__(self):
super(IdleManager, self).__init__()
self.log = Logger().get_logger(self.__class__.__name__)
self.signal_reset_timer.connect(self._reset_time)
self.qaction = None
self.failed_icon = None
self._is_running = False
self.threads = []
def set_qaction(self, qaction, failed_icon):
self.qaction = qaction
@ -32,18 +31,18 @@ class IdleManager(QtCore.QThread):
def tray_exit(self):
self.stop()
try:
self.time_signals = {}
self.time_callbacks = {}
except Exception:
pass
def add_time_signal(self, emit_time, signal):
""" If any module want to use IdleManager, need to use add_time_signal
:param emit_time: time when signal will be emitted
:type emit_time: int
:param signal: signal that will be emitted (without objects)
:type signal: QtCore.Signal
def add_time_callback(self, emit_time, callback):
"""If any module want to use IdleManager, need to use this method.
Args:
emit_time(int): Time when callback will be triggered.
callback(func): Callback that will be triggered.
"""
self.time_signals[emit_time].append(signal)
self.time_callbacks[emit_time].append(callback)
@property
def is_running(self):
@ -58,17 +57,26 @@ class IdleManager(QtCore.QThread):
def run(self):
self.log.info('IdleManager has started')
self._is_running = True
thread_mouse = MouseThread(self.signal_reset_timer)
thread_mouse = MouseThread(self._reset_time)
thread_mouse.start()
thread_keyboard = KeyboardThread(self.signal_reset_timer)
thread_keyboard = KeyboardThread(self._reset_time)
thread_keyboard.start()
try:
while self.is_running:
if self.idle_time in self.time_callbacks:
for callback in self.time_callbacks[self.idle_time]:
thread = threading.Thread(target=callback)
thread.start()
self.threads.append(thread)
for thread in tuple(self.threads):
if not thread.isAlive():
thread.join()
self.threads.remove(thread)
self.idle_time += 1
if self.idle_time in self.time_signals:
for signal in self.time_signals[self.idle_time]:
signal.emit()
time.sleep(1)
except Exception:
self.log.warning(
'Idle Manager service has failed', exc_info=True
@ -79,16 +87,14 @@ class IdleManager(QtCore.QThread):
# Threads don't have their attrs when Qt application already finished
try:
thread_mouse.signal_stop.emit()
thread_mouse.terminate()
thread_mouse.wait()
thread_mouse.stop()
thread_mouse.join()
except AttributeError:
pass
try:
thread_keyboard.signal_stop.emit()
thread_keyboard.terminate()
thread_keyboard.wait()
thread_keyboard.stop()
thread_keyboard.join()
except AttributeError:
pass
@ -96,49 +102,24 @@ class IdleManager(QtCore.QThread):
self.log.info('IdleManager has stopped')
class MouseThread(QtCore.QThread):
"""Listens user's mouse movement
"""
signal_stop = QtCore.Signal()
class MouseThread(mouse.Listener):
"""Listens user's mouse movement."""
def __init__(self, signal):
super(MouseThread, self).__init__()
self.signal_stop.connect(self.stop)
self.m_listener = None
self.signal_reset_timer = signal
def stop(self):
if self.m_listener is not None:
self.m_listener.stop()
def __init__(self, callback):
super(MouseThread, self).__init__(on_move=self.on_move)
self.callback = callback
def on_move(self, posx, posy):
self.signal_reset_timer.emit()
def run(self):
self.m_listener = mouse.Listener(on_move=self.on_move)
self.m_listener.start()
self.callback()
class KeyboardThread(QtCore.QThread):
"""Listens user's keyboard input
"""
signal_stop = QtCore.Signal()
class KeyboardThread(keyboard.Listener):
"""Listens user's keyboard input."""
def __init__(self, signal):
super(KeyboardThread, self).__init__()
self.signal_stop.connect(self.stop)
self.k_listener = None
def __init__(self, callback):
super(KeyboardThread, self).__init__(on_press=self.on_press)
self.signal_reset_timer = signal
def stop(self):
if self.k_listener is not None:
self.k_listener.stop()
self.callback = callback
def on_press(self, key):
self.signal_reset_timer.emit()
def run(self):
self.k_listener = keyboard.Listener(on_press=self.on_press)
self.k_listener.start()
self.callback()

View file

@ -1,6 +1,7 @@
import os
from Qt import QtCore, QtGui, QtWidgets
from avalon import style
from pype.api import resources
class MusterLogin(QtWidgets.QWidget):
@ -23,10 +24,7 @@ class MusterLogin(QtWidgets.QWidget):
elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'):
self.setWindowIcon(parent.parent.icon)
else:
pype_setup = os.getenv('PYPE_SETUP_PATH')
items = [pype_setup, "app", "resources", "icon.png"]
fname = os.path.sep.join(items)
icon = QtGui.QIcon(fname)
icon = QtGui.QIcon(resources.pype_icon_filepath())
self.setWindowIcon(icon)
self.setWindowFlags(

View file

@ -10,10 +10,37 @@ from . import DropEmpty, ComponentsList, ComponentItem
class DropDataFrame(QtWidgets.QFrame):
image_extensions = [
".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", ".cal",
".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", ".fits",
".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", ".icer",
".icns", ".ico", ".cur", ".ics", ".ilbm", ".jbig", ".jbig2",
".jng", ".jpeg", ".jpeg-ls", ".jpeg", ".2000", ".jpg", ".xr",
".jpeg", ".xt", ".jpeg-hdr", ".kra", ".mng", ".miff", ".nrrd",
".ora", ".pam", ".pbm", ".pgm", ".ppm", ".pnm", ".pcx", ".pgf",
".pictor", ".png", ".psd", ".psb", ".psp", ".qtvr", ".ras",
".rgbe", ".logluv", ".tiff", ".sgi", ".tga", ".tiff", ".tiff/ep",
".tiff/it", ".ufo", ".ufp", ".wbmp", ".webp", ".xbm", ".xcf",
".xpm", ".xwd"
]
video_extensions = [
".3g2", ".3gp", ".amv", ".asf", ".avi", ".drc", ".f4a", ".f4b",
".f4p", ".f4v", ".flv", ".gif", ".gifv", ".m2v", ".m4p", ".m4v",
".mkv", ".mng", ".mov", ".mp2", ".mp4", ".mpe", ".mpeg", ".mpg",
".mpv", ".mxf", ".nsv", ".ogg", ".ogv", ".qt", ".rm", ".rmvb",
".roq", ".svi", ".vob", ".webm", ".wmv", ".yuv"
]
extensions = {
"nuke": [".nk"],
"maya": [".ma", ".mb"],
"houdini": [".hip"],
"image_file": image_extensions,
"video_file": video_extensions
}
def __init__(self, parent):
super().__init__()
self.parent_widget = parent
self.presets = config.get_presets()['standalone_publish']
self.setAcceptDrops(True)
layout = QtWidgets.QVBoxLayout(self)
@ -26,7 +53,9 @@ class DropDataFrame(QtWidgets.QFrame):
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.drop_widget.sizePolicy().hasHeightForWidth())
sizePolicy.setHeightForWidth(
self.drop_widget.sizePolicy().hasHeightForWidth()
)
self.drop_widget.setSizePolicy(sizePolicy)
layout.addWidget(self.drop_widget)
@ -255,8 +284,8 @@ class DropDataFrame(QtWidgets.QFrame):
file_info = data['file_info']
if (
ext in self.presets['extensions']['image_file'] or
ext in self.presets['extensions']['video_file']
ext in self.image_extensions
or ext in self.video_extensions
):
probe_data = self.load_data_with_probe(filepath)
if 'fps' not in data:
@ -293,7 +322,7 @@ class DropDataFrame(QtWidgets.QFrame):
data[key] = value
icon = 'default'
for ico, exts in self.presets['extensions'].items():
for ico, exts in self.extensions.items():
if ext in exts:
icon = ico
break
@ -304,17 +333,16 @@ class DropDataFrame(QtWidgets.QFrame):
icon += 's'
data['icon'] = icon
data['thumb'] = (
ext in self.presets['extensions']['image_file'] or
ext in self.presets['extensions']['video_file']
ext in self.image_extensions
or ext in self.video_extensions
)
data['prev'] = (
ext in self.presets['extensions']['video_file'] or
(new_is_seq and ext in self.presets['extensions']['image_file'])
ext in self.video_extensions
or (new_is_seq and ext in self.image_extensions)
)
actions = []
found = False
for item in self.components_list.widgets():
if data['ext'] != item.in_data['ext']:

View file

@ -1,5 +1,4 @@
from Qt import QtCore
from .widget_user_idle import WidgetUserIdle
from .widget_user_idle import WidgetUserIdle, SignalHandler
from pype.api import Logger, config
@ -31,7 +30,10 @@ class TimersManager(metaclass=Singleton):
self.log = Logger().get_logger(self.__class__.__name__)
self.tray_widget = tray_widget
self.main_widget = main_widget
self.widget_user_idle = WidgetUserIdle(self)
self.idle_man = None
self.signal_handler = None
self.widget_user_idle = WidgetUserIdle(self, tray_widget)
def set_signal_times(self):
try:
@ -114,49 +116,59 @@ class TimersManager(metaclass=Singleton):
:param modules: All imported modules from TrayManager
:type modules: dict
"""
self.s_handler = SignalHandler(self)
if 'IdleManager' in modules:
self.signal_handler = SignalHandler(self)
if self.set_signal_times() is True:
self.register_to_idle_manager(modules['IdleManager'])
def time_callback(self, int_def):
if not self.signal_handler:
return
if int_def == 0:
self.signal_handler.signal_show_message.emit()
elif int_def == 1:
self.signal_handler.signal_change_label.emit()
elif int_def == 2:
self.signal_handler.signal_stop_timers.emit()
def register_to_idle_manager(self, man_obj):
self.idle_man = man_obj
# Time when message is shown
self.idle_man.add_time_callback(
self.time_show_message,
lambda: self.time_callback(0)
)
# Times when idle is between show widget and stop timers
show_to_stop_range = range(
self.time_show_message-1, self.time_stop_timer
self.time_show_message - 1, self.time_stop_timer
)
for num in show_to_stop_range:
self.idle_man.add_time_signal(
num,
self.s_handler.signal_change_label
self.idle_man.add_time_callback(
num, lambda: self.time_callback(1)
)
# Times when widget is already shown and user restart idle
shown_and_moved_range = range(
self.time_stop_timer - self.time_show_message
)
for num in shown_and_moved_range:
self.idle_man.add_time_signal(
num,
self.s_handler.signal_change_label
self.idle_man.add_time_callback(
num, lambda: self.time_callback(1)
)
# Time when message is shown
self.idle_man.add_time_signal(
self.time_show_message,
self.s_handler.signal_show_message
)
# Time when timers are stopped
self.idle_man.add_time_signal(
self.idle_man.add_time_callback(
self.time_stop_timer,
self.s_handler.signal_stop_timers
lambda: self.time_callback(2)
)
def change_label(self):
if self.is_running is False:
return
if self.widget_user_idle.bool_is_showed is False:
return
if not hasattr(self, 'idle_man'):
if not self.idle_man or self.widget_user_idle.bool_is_showed is False:
return
if self.idle_man.idle_time > self.time_show_message:
@ -174,14 +186,3 @@ class TimersManager(metaclass=Singleton):
return
if self.widget_user_idle.bool_is_showed is False:
self.widget_user_idle.show()
class SignalHandler(QtCore.QObject):
signal_show_message = QtCore.Signal()
signal_change_label = QtCore.Signal()
signal_stop_timers = QtCore.Signal()
def __init__(self, cls):
super().__init__()
self.signal_show_message.connect(cls.show_message)
self.signal_change_label.connect(cls.change_label)
self.signal_stop_timers.connect(cls.stop_timers)

View file

@ -1,4 +1,3 @@
from pype.api import Logger
from avalon import style
from Qt import QtCore, QtGui, QtWidgets
@ -8,18 +7,18 @@ class WidgetUserIdle(QtWidgets.QWidget):
SIZE_W = 300
SIZE_H = 160
def __init__(self, parent):
def __init__(self, module, tray_widget):
super(WidgetUserIdle, self).__init__()
self.bool_is_showed = False
self.bool_not_stopped = True
self.parent_widget = parent
self.setWindowIcon(parent.tray_widget.icon)
self.module = module
self.setWindowIcon(tray_widget.icon)
self.setWindowFlags(
QtCore.Qt.WindowCloseButtonHint |
QtCore.Qt.WindowMinimizeButtonHint
QtCore.Qt.WindowCloseButtonHint
| QtCore.Qt.WindowMinimizeButtonHint
)
self._translate = QtCore.QCoreApplication.translate
@ -129,11 +128,11 @@ class WidgetUserIdle(QtWidgets.QWidget):
self.lbl_rest_time.setText(str_time)
def stop_timer(self):
self.parent_widget.stop_timers()
self.module.stop_timers()
self.close_widget()
def restart_timer(self):
self.parent_widget.restart_timers()
self.module.restart_timers()
self.close_widget()
def continue_timer(self):
@ -154,3 +153,15 @@ class WidgetUserIdle(QtWidgets.QWidget):
def showEvent(self, event):
self.bool_is_showed = True
class SignalHandler(QtCore.QObject):
signal_show_message = QtCore.Signal()
signal_change_label = QtCore.Signal()
signal_stop_timers = QtCore.Signal()
def __init__(self, cls):
super().__init__()
self.signal_show_message.connect(cls.show_message)
self.signal_change_label.connect(cls.change_label)
self.signal_stop_timers.connect(cls.stop_timers)

View file

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

View file

@ -7,20 +7,11 @@ from typing import Dict, List, Optional
from avalon import api, blender
import bpy
import pype.hosts.blender.plugin
import pype.hosts.blender.plugin as plugin
logger = logging.getLogger("pype").getChild(
"blender").getChild("load_layout")
class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
"""Load animations from a .blend file.
Warning:
Loading the same asset more then once is not properly supported at the
moment.
"""
class BlendLayoutLoader(plugin.AssetLoader):
"""Load layout from a .blend file."""
families = ["layout"]
representations = ["blend"]
@ -29,24 +20,25 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
icon = "code-fork"
color = "orange"
def _remove(self, objects, lib_container):
def _remove(self, objects, obj_container):
for obj in objects:
if obj.type == 'ARMATURE':
bpy.data.armatures.remove(obj.data)
elif obj.type == 'MESH':
bpy.data.meshes.remove(obj.data)
elif obj.type == 'CAMERA':
bpy.data.cameras.remove(obj.data)
elif obj.type == 'CURVE':
bpy.data.curves.remove(obj.data)
for element_container in bpy.data.collections[lib_container].children:
for element_container in obj_container.children:
for child in element_container.children:
bpy.data.collections.remove(child)
bpy.data.collections.remove(element_container)
bpy.data.collections.remove(bpy.data.collections[lib_container])
bpy.data.collections.remove(obj_container)
def _process(self, libpath, lib_container, container_name, actions):
relative = bpy.context.preferences.filepaths.use_relative_paths
with bpy.data.libraries.load(
libpath, link=True, relative=relative
@ -58,26 +50,38 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
scene.collection.children.link(bpy.data.collections[lib_container])
layout_container = scene.collection.children[lib_container].make_local()
layout_container.name = container_name
meshes = []
objects_local_types = ['MESH', 'CAMERA', 'CURVE']
objects = []
armatures = []
objects_list = []
containers = list(layout_container.children)
for element_container in layout_container.children:
element_container.make_local()
meshes.extend([obj for obj in element_container.objects if obj.type == 'MESH'])
armatures.extend([obj for obj in element_container.objects if obj.type == 'ARMATURE'])
for child in element_container.children:
child.make_local()
meshes.extend(child.objects)
for container in layout_container.children:
if container.name == blender.pipeline.AVALON_CONTAINERS:
containers.remove(container)
for container in containers:
container.make_local()
objects.extend([
obj for obj in container.objects
if obj.type in objects_local_types
])
armatures.extend([
obj for obj in container.objects
if obj.type == 'ARMATURE'
])
containers.extend(list(container.children))
# Link meshes first, then armatures.
# The armature is unparented for all the non-local meshes,
# when it is made local.
for obj in meshes + armatures:
obj = obj.make_local()
obj.data.make_local()
for obj in objects + armatures:
obj.make_local()
if obj.data:
obj.data.make_local()
if not obj.get(blender.pipeline.AVALON_PROPERTY):
obj[blender.pipeline.AVALON_PROPERTY] = dict()
@ -85,18 +89,16 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
avalon_info = obj[blender.pipeline.AVALON_PROPERTY]
avalon_info.update({"container_name": container_name})
action = actions.get( obj.name, None )
action = actions.get(obj.name, None)
if obj.type == 'ARMATURE' and action is not None:
obj.animation_data.action = action
objects_list.append(obj)
layout_container.pop(blender.pipeline.AVALON_PROPERTY)
bpy.ops.object.select_all(action='DESELECT')
return objects_list
return layout_container
def process_asset(
self, context: dict, name: str, namespace: Optional[str] = None,
@ -113,9 +115,15 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
libpath = self.fname
asset = context["asset"]["name"]
subset = context["subset"]["name"]
lib_container = pype.hosts.blender.plugin.asset_name(asset, subset)
container_name = pype.hosts.blender.plugin.asset_name(
asset, subset, namespace
lib_container = plugin.asset_name(
asset, subset
)
unique_number = plugin.get_unique_number(
asset, subset
)
namespace = namespace or f"{asset}_{unique_number}"
container_name = plugin.asset_name(
asset, subset, unique_number
)
container = bpy.data.collections.new(lib_container)
@ -134,11 +142,13 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
container_metadata["libpath"] = libpath
container_metadata["lib_container"] = lib_container
objects_list = self._process(
obj_container = self._process(
libpath, lib_container, container_name, {})
container_metadata["obj_container"] = obj_container
# Save the list of objects in the metadata container
container_metadata["objects"] = objects_list
container_metadata["objects"] = obj_container.all_objects
nodes = list(container.objects)
nodes.append(container)
@ -157,7 +167,6 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
Warning:
No nested collections are supported at the moment!
"""
collection = bpy.data.collections.get(
container["objectName"]
)
@ -165,7 +174,7 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
libpath = Path(api.get_representation_path(representation))
extension = libpath.suffix.lower()
logger.info(
self.log.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
@ -189,41 +198,40 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
collection_metadata = collection.get(
blender.pipeline.AVALON_PROPERTY)
collection_libpath = collection_metadata["libpath"]
objects = collection_metadata["objects"]
lib_container = collection_metadata["lib_container"]
obj_container = collection_metadata["obj_container"]
normalized_collection_libpath = (
str(Path(bpy.path.abspath(collection_libpath)).resolve())
)
normalized_libpath = (
str(Path(bpy.path.abspath(str(libpath))).resolve())
)
logger.debug(
self.log.debug(
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
normalized_collection_libpath,
normalized_libpath,
)
if normalized_collection_libpath == normalized_libpath:
logger.info("Library already loaded, not updating...")
self.log.info("Library already loaded, not updating...")
return
objects = collection_metadata["objects"]
lib_container = collection_metadata["lib_container"]
actions = {}
for obj in objects:
if obj.type == 'ARMATURE':
actions[obj.name] = obj.animation_data.action
self._remove(objects, lib_container)
self._remove(objects, obj_container)
objects_list = self._process(
obj_container = self._process(
str(libpath), lib_container, collection.name, actions)
# Save the list of objects in the metadata container
collection_metadata["objects"] = objects_list
collection_metadata["obj_container"] = obj_container
collection_metadata["objects"] = obj_container.all_objects
collection_metadata["libpath"] = str(libpath)
collection_metadata["representation"] = str(representation["_id"])
@ -255,9 +263,9 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader):
collection_metadata = collection.get(
blender.pipeline.AVALON_PROPERTY)
objects = collection_metadata["objects"]
lib_container = collection_metadata["lib_container"]
obj_container = collection_metadata["obj_container"]
self._remove(objects, lib_container)
self._remove(objects, obj_container)
bpy.data.collections.remove(collection)

View file

@ -7,20 +7,14 @@ from typing import Dict, List, Optional
from avalon import api, blender
import bpy
import pype.hosts.blender.plugin
logger = logging.getLogger("pype").getChild("blender").getChild("load_model")
import pype.hosts.blender.plugin as plugin
class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader):
class BlendModelLoader(plugin.AssetLoader):
"""Load models from a .blend file.
Because they come from a .blend file we can simply link the collection that
contains the model. There is no further need to 'containerise' it.
Warning:
Loading the same asset more then once is not properly supported at the
moment.
"""
families = ["model"]
@ -30,54 +24,52 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader):
icon = "code-fork"
color = "orange"
def _remove(self, objects, lib_container):
def _remove(self, objects, container):
for obj in objects:
for material_slot in obj.material_slots:
bpy.data.materials.remove(material_slot.material)
bpy.data.meshes.remove(obj.data)
bpy.data.collections.remove(bpy.data.collections[lib_container])
def _process(self, libpath, lib_container, container_name):
bpy.data.collections.remove(container)
def _process(
self, libpath, lib_container, container_name,
parent_collection
):
relative = bpy.context.preferences.filepaths.use_relative_paths
with bpy.data.libraries.load(
libpath, link=True, relative=relative
) as (_, data_to):
data_to.collections = [lib_container]
scene = bpy.context.scene
parent = parent_collection
scene.collection.children.link(bpy.data.collections[lib_container])
if parent is None:
parent = bpy.context.scene.collection
model_container = scene.collection.children[lib_container].make_local()
parent.children.link(bpy.data.collections[lib_container])
objects_list = []
model_container = parent.children[lib_container].make_local()
model_container.name = container_name
for obj in model_container.objects:
obj = obj.make_local()
obj.data.make_local()
plugin.prepare_data(obj, container_name)
plugin.prepare_data(obj.data, container_name)
for material_slot in obj.material_slots:
material_slot.material.make_local()
plugin.prepare_data(material_slot.material, container_name)
if not obj.get(blender.pipeline.AVALON_PROPERTY):
obj[blender.pipeline.AVALON_PROPERTY] = dict()
avalon_info = obj[blender.pipeline.AVALON_PROPERTY]
avalon_info.update({"container_name": container_name})
objects_list.append(obj)
model_container.pop(blender.pipeline.AVALON_PROPERTY)
bpy.ops.object.select_all(action='DESELECT')
return objects_list
return model_container
def process_asset(
self, context: dict, name: str, namespace: Optional[str] = None,
@ -94,35 +86,44 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader):
libpath = self.fname
asset = context["asset"]["name"]
subset = context["subset"]["name"]
lib_container = pype.hosts.blender.plugin.asset_name(asset, subset)
container_name = pype.hosts.blender.plugin.asset_name(
asset, subset, namespace
lib_container = plugin.asset_name(
asset, subset
)
unique_number = plugin.get_unique_number(
asset, subset
)
namespace = namespace or f"{asset}_{unique_number}"
container_name = plugin.asset_name(
asset, subset, unique_number
)
collection = bpy.data.collections.new(lib_container)
collection.name = container_name
container = bpy.data.collections.new(lib_container)
container.name = container_name
blender.pipeline.containerise_existing(
collection,
container,
name,
namespace,
context,
self.__class__.__name__,
)
container_metadata = collection.get(
container_metadata = container.get(
blender.pipeline.AVALON_PROPERTY)
container_metadata["libpath"] = libpath
container_metadata["lib_container"] = lib_container
objects_list = self._process(
libpath, lib_container, container_name)
obj_container = self._process(
libpath, lib_container, container_name, None)
container_metadata["obj_container"] = obj_container
# Save the list of objects in the metadata container
container_metadata["objects"] = objects_list
container_metadata["objects"] = obj_container.all_objects
nodes = list(collection.objects)
nodes.append(collection)
nodes = list(container.objects)
nodes.append(container)
self[:] = nodes
return nodes
@ -144,7 +145,7 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader):
libpath = Path(api.get_representation_path(representation))
extension = libpath.suffix.lower()
logger.debug(
self.log.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
@ -162,38 +163,47 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader):
assert libpath.is_file(), (
f"The file doesn't exist: {libpath}"
)
assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, (
assert extension in plugin.VALID_EXTENSIONS, (
f"Unsupported file: {libpath}"
)
collection_metadata = collection.get(
blender.pipeline.AVALON_PROPERTY)
collection_libpath = collection_metadata["libpath"]
objects = collection_metadata["objects"]
lib_container = collection_metadata["lib_container"]
obj_container = plugin.get_local_collection_with_name(
collection_metadata["obj_container"].name
)
objects = obj_container.all_objects
container_name = obj_container.name
normalized_collection_libpath = (
str(Path(bpy.path.abspath(collection_libpath)).resolve())
)
normalized_libpath = (
str(Path(bpy.path.abspath(str(libpath))).resolve())
)
logger.debug(
self.log.debug(
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
normalized_collection_libpath,
normalized_libpath,
)
if normalized_collection_libpath == normalized_libpath:
logger.info("Library already loaded, not updating...")
self.log.info("Library already loaded, not updating...")
return
self._remove(objects, lib_container)
parent = plugin.get_parent_collection(obj_container)
objects_list = self._process(
str(libpath), lib_container, collection.name)
self._remove(objects, obj_container)
obj_container = self._process(
str(libpath), lib_container, container_name, parent)
# Save the list of objects in the metadata container
collection_metadata["objects"] = objects_list
collection_metadata["obj_container"] = obj_container
collection_metadata["objects"] = obj_container.all_objects
collection_metadata["libpath"] = str(libpath)
collection_metadata["representation"] = str(representation["_id"])
@ -221,17 +231,20 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader):
collection_metadata = collection.get(
blender.pipeline.AVALON_PROPERTY)
objects = collection_metadata["objects"]
lib_container = collection_metadata["lib_container"]
self._remove(objects, lib_container)
obj_container = plugin.get_local_collection_with_name(
collection_metadata["obj_container"].name
)
objects = obj_container.all_objects
self._remove(objects, obj_container)
bpy.data.collections.remove(collection)
return True
class CacheModelLoader(pype.hosts.blender.plugin.AssetLoader):
class CacheModelLoader(plugin.AssetLoader):
"""Load cache models.
Stores the imported asset in a collection named after the asset.
@ -267,7 +280,7 @@ class CacheModelLoader(pype.hosts.blender.plugin.AssetLoader):
subset = context["subset"]["name"]
# TODO (jasper): evaluate use of namespace which is 'alien' to Blender.
lib_container = container_name = (
pype.hosts.blender.plugin.asset_name(asset, subset, namespace)
plugin.asset_name(asset, subset, namespace)
)
relative = bpy.context.preferences.filepaths.use_relative_paths

View file

@ -7,20 +7,14 @@ from typing import Dict, List, Optional
from avalon import api, blender
import bpy
import pype.hosts.blender.plugin
logger = logging.getLogger("pype").getChild("blender").getChild("load_model")
import pype.hosts.blender.plugin as plugin
class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
class BlendRigLoader(plugin.AssetLoader):
"""Load rigs from a .blend file.
Because they come from a .blend file we can simply link the collection that
contains the model. There is no further need to 'containerise' it.
Warning:
Loading the same asset more then once is not properly supported at the
moment.
"""
families = ["rig"]
@ -30,50 +24,54 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
icon = "code-fork"
color = "orange"
def _remove(self, objects, lib_container):
def _remove(self, objects, obj_container):
for obj in objects:
if obj.type == 'ARMATURE':
bpy.data.armatures.remove(obj.data)
elif obj.type == 'MESH':
bpy.data.meshes.remove(obj.data)
for child in bpy.data.collections[lib_container].children:
for child in obj_container.children:
bpy.data.collections.remove(child)
bpy.data.collections.remove(bpy.data.collections[lib_container])
def _process(self, libpath, lib_container, container_name, action):
bpy.data.collections.remove(obj_container)
def _process(
self, libpath, lib_container, container_name,
action, parent_collection
):
relative = bpy.context.preferences.filepaths.use_relative_paths
with bpy.data.libraries.load(
libpath, link=True, relative=relative
) as (_, data_to):
data_to.collections = [lib_container]
scene = bpy.context.scene
parent = parent_collection
scene.collection.children.link(bpy.data.collections[lib_container])
if parent is None:
parent = bpy.context.scene.collection
rig_container = scene.collection.children[lib_container].make_local()
parent.children.link(bpy.data.collections[lib_container])
rig_container = parent.children[lib_container].make_local()
rig_container.name = container_name
meshes = []
armatures = [
obj for obj in rig_container.objects if obj.type == 'ARMATURE']
objects_list = []
obj for obj in rig_container.objects
if obj.type == 'ARMATURE'
]
for child in rig_container.children:
child.make_local()
meshes.extend( child.objects )
plugin.prepare_data(child, container_name)
meshes.extend(child.objects)
# Link meshes first, then armatures.
# The armature is unparented for all the non-local meshes,
# when it is made local.
for obj in meshes + armatures:
obj = obj.make_local()
obj.data.make_local()
plugin.prepare_data(obj, container_name)
plugin.prepare_data(obj.data, container_name)
if not obj.get(blender.pipeline.AVALON_PROPERTY):
obj[blender.pipeline.AVALON_PROPERTY] = dict()
@ -84,13 +82,11 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
if obj.type == 'ARMATURE' and action is not None:
obj.animation_data.action = action
objects_list.append(obj)
rig_container.pop(blender.pipeline.AVALON_PROPERTY)
bpy.ops.object.select_all(action='DESELECT')
return objects_list
return rig_container
def process_asset(
self, context: dict, name: str, namespace: Optional[str] = None,
@ -107,9 +103,15 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
libpath = self.fname
asset = context["asset"]["name"]
subset = context["subset"]["name"]
lib_container = pype.hosts.blender.plugin.asset_name(asset, subset)
container_name = pype.hosts.blender.plugin.asset_name(
asset, subset, namespace
lib_container = plugin.asset_name(
asset, subset
)
unique_number = plugin.get_unique_number(
asset, subset
)
namespace = namespace or f"{asset}_{unique_number}"
container_name = plugin.asset_name(
asset, subset, unique_number
)
container = bpy.data.collections.new(lib_container)
@ -128,11 +130,13 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
container_metadata["libpath"] = libpath
container_metadata["lib_container"] = lib_container
objects_list = self._process(
libpath, lib_container, container_name, None)
obj_container = self._process(
libpath, lib_container, container_name, None, None)
container_metadata["obj_container"] = obj_container
# Save the list of objects in the metadata container
container_metadata["objects"] = objects_list
container_metadata["objects"] = obj_container.all_objects
nodes = list(container.objects)
nodes.append(container)
@ -151,15 +155,13 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
Warning:
No nested collections are supported at the moment!
"""
collection = bpy.data.collections.get(
container["objectName"]
)
libpath = Path(api.get_representation_path(representation))
extension = libpath.suffix.lower()
logger.info(
self.log.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
@ -177,29 +179,35 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
assert libpath.is_file(), (
f"The file doesn't exist: {libpath}"
)
assert extension in pype.hosts.blender.plugin.VALID_EXTENSIONS, (
assert extension in plugin.VALID_EXTENSIONS, (
f"Unsupported file: {libpath}"
)
collection_metadata = collection.get(
blender.pipeline.AVALON_PROPERTY)
collection_libpath = collection_metadata["libpath"]
objects = collection_metadata["objects"]
lib_container = collection_metadata["lib_container"]
obj_container = plugin.get_local_collection_with_name(
collection_metadata["obj_container"].name
)
objects = obj_container.all_objects
container_name = obj_container.name
normalized_collection_libpath = (
str(Path(bpy.path.abspath(collection_libpath)).resolve())
)
normalized_libpath = (
str(Path(bpy.path.abspath(str(libpath))).resolve())
)
logger.debug(
self.log.debug(
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
normalized_collection_libpath,
normalized_libpath,
)
if normalized_collection_libpath == normalized_libpath:
logger.info("Library already loaded, not updating...")
self.log.info("Library already loaded, not updating...")
return
# Get the armature of the rig
@ -208,13 +216,16 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
action = armatures[0].animation_data.action
self._remove(objects, lib_container)
parent = plugin.get_parent_collection(obj_container)
objects_list = self._process(
str(libpath), lib_container, collection.name, action)
self._remove(objects, obj_container)
obj_container = self._process(
str(libpath), lib_container, container_name, action, parent)
# Save the list of objects in the metadata container
collection_metadata["objects"] = objects_list
collection_metadata["obj_container"] = obj_container
collection_metadata["objects"] = obj_container.all_objects
collection_metadata["libpath"] = str(libpath)
collection_metadata["representation"] = str(representation["_id"])
@ -245,10 +256,13 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader):
collection_metadata = collection.get(
blender.pipeline.AVALON_PROPERTY)
objects = collection_metadata["objects"]
lib_container = collection_metadata["lib_container"]
self._remove(objects, lib_container)
obj_container = plugin.get_local_collection_with_name(
collection_metadata["obj_container"].name
)
objects = obj_container.all_objects
self._remove(objects, obj_container)
bpy.data.collections.remove(collection)

View file

@ -83,6 +83,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
"textures",
"action",
"harmony.template",
"harmony.palette",
"editorial"
]
exclude_families = ["clip"]
@ -605,7 +606,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
"type": "subset",
"name": subset_name,
"data": {
"families": instance.data.get('families')
"families": instance.data.get("families", [])
},
"parent": asset["_id"]
}).inserted_id
@ -727,7 +728,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
task_name = io.Session.get("AVALON_TASK")
family = self.main_family_from_instance(instance)
matching_profiles = None
matching_profiles = {}
highest_value = -1
self.log.info(self.template_name_profiles)
for name, filters in self.template_name_profiles.items():
@ -745,7 +746,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
value += 1
if value > highest_value:
matching_profiles = {}
highest_value = value
if value == highest_value:

View file

@ -19,7 +19,7 @@ class ValidateContainers(pyblish.api.ContextPlugin):
label = "Validate Containers"
order = pyblish.api.ValidatorOrder
hosts = ["maya", "houdini", "nuke"]
hosts = ["maya", "houdini", "nuke", "harmony", "photoshop"]
optional = True
actions = [ShowInventory]

View file

@ -1,8 +1,10 @@
import os
import uuid
import clique
from avalon import api, harmony
import pype.lib
copy_files = """function copyFile(srcFilename, dstFilename)
{
@ -98,33 +100,63 @@ function import_files(args)
transparencyModeAttr.setValue(SGITransparencyMode);
if (extension == "psd")
transparencyModeAttr.setValue(FlatPSDTransparencyMode);
if (extension == "jpg")
transparencyModeAttr.setValue(LayeredPSDTransparencyMode);
node.linkAttr(read, "DRAWING.ELEMENT", uniqueColumnName);
// Create a drawing for each file.
for( var i =0; i <= files.length - 1; ++i)
if (files.length == 1)
{
timing = start_frame + i
// Create a drawing drawing, 'true' indicate that the file exists.
Drawing.create(elemId, timing, true);
Drawing.create(elemId, 1, true);
// Get the actual path, in tmp folder.
var drawingFilePath = Drawing.filename(elemId, timing.toString());
copyFile( files[i], drawingFilePath );
var drawingFilePath = Drawing.filename(elemId, "1");
copyFile(files[0], drawingFilePath);
// Expose the image for the entire frame range.
for( var i =0; i <= frame.numberOf() - 1; ++i)
{
timing = start_frame + i
column.setEntry(uniqueColumnName, 1, timing, "1");
}
} else {
// Create a drawing for each file.
for( var i =0; i <= files.length - 1; ++i)
{
timing = start_frame + i
// Create a drawing drawing, 'true' indicate that the file exists.
Drawing.create(elemId, timing, true);
// Get the actual path, in tmp folder.
var drawingFilePath = Drawing.filename(elemId, timing.toString());
copyFile( files[i], drawingFilePath );
column.setEntry(uniqueColumnName, 1, timing, timing.toString());
column.setEntry(uniqueColumnName, 1, timing, timing.toString());
}
}
var green_color = new ColorRGBA(0, 255, 0, 255);
node.setColor(read, green_color);
return read;
}
import_files
"""
replace_files = """function replace_files(args)
replace_files = """var PNGTransparencyMode = 0; //Premultiplied wih Black
var TGATransparencyMode = 0; //Premultiplied wih Black
var SGITransparencyMode = 0; //Premultiplied wih Black
var LayeredPSDTransparencyMode = 1; //Straight
var FlatPSDTransparencyMode = 2; //Premultiplied wih White
function replace_files(args)
{
var files = args[0];
MessageLog.trace(files);
MessageLog.trace(files.length);
var _node = args[1];
var start_frame = args[2];
var _column = node.linkedColumn(_node, "DRAWING.ELEMENT");
var elemId = column.getElementIdOfDrawing(_column);
// Delete existing drawings.
var timings = column.getDrawingTimings(_column);
@ -133,20 +165,62 @@ replace_files = """function replace_files(args)
column.deleteDrawingAt(_column, parseInt(timings[i]));
}
// Create new drawings.
for( var i =0; i <= files.length - 1; ++i)
{
timing = start_frame + i
// Create a drawing drawing, 'true' indicate that the file exists.
Drawing.create(node.getElementId(_node), timing, true);
// Get the actual path, in tmp folder.
var drawingFilePath = Drawing.filename(
node.getElementId(_node), timing.toString()
);
copyFile( files[i], drawingFilePath );
column.setEntry(_column, 1, timing, timing.toString());
var filename = files[0];
var pos = filename.lastIndexOf(".");
if( pos < 0 )
return null;
var extension = filename.substr(pos+1).toLowerCase();
if(extension == "jpeg")
extension = "jpg";
var transparencyModeAttr = node.getAttr(
_node, frame.current(), "applyMatteToColor"
);
if (extension == "png")
transparencyModeAttr.setValue(PNGTransparencyMode);
if (extension == "tga")
transparencyModeAttr.setValue(TGATransparencyMode);
if (extension == "sgi")
transparencyModeAttr.setValue(SGITransparencyMode);
if (extension == "psd")
transparencyModeAttr.setValue(FlatPSDTransparencyMode);
if (extension == "jpg")
transparencyModeAttr.setValue(LayeredPSDTransparencyMode);
if (files.length == 1)
{
// Create a drawing drawing, 'true' indicate that the file exists.
Drawing.create(elemId, 1, true);
// Get the actual path, in tmp folder.
var drawingFilePath = Drawing.filename(elemId, "1");
copyFile(files[0], drawingFilePath);
MessageLog.trace(files[0]);
MessageLog.trace(drawingFilePath);
// Expose the image for the entire frame range.
for( var i =0; i <= frame.numberOf() - 1; ++i)
{
timing = start_frame + i
column.setEntry(_column, 1, timing, "1");
}
} else {
// Create a drawing for each file.
for( var i =0; i <= files.length - 1; ++i)
{
timing = start_frame + i
// Create a drawing drawing, 'true' indicate that the file exists.
Drawing.create(elemId, timing, true);
// Get the actual path, in tmp folder.
var drawingFilePath = Drawing.filename(elemId, timing.toString());
copyFile( files[i], drawingFilePath );
column.setEntry(_column, 1, timing, timing.toString());
}
}
var green_color = new ColorRGBA(0, 255, 0, 255);
node.setColor(_node, green_color);
}
replace_files
"""
@ -156,8 +230,8 @@ class ImageSequenceLoader(api.Loader):
"""Load images
Stores the imported asset in a container named after the asset.
"""
families = ["shot", "render"]
representations = ["jpeg", "png"]
families = ["shot", "render", "image"]
representations = ["jpeg", "png", "jpg"]
def load(self, context, name=None, namespace=None, data=None):
@ -165,20 +239,29 @@ class ImageSequenceLoader(api.Loader):
os.listdir(os.path.dirname(self.fname))
)
files = []
for f in list(collections[0]):
if collections:
for f in list(collections[0]):
files.append(
os.path.join(
os.path.dirname(self.fname), f
).replace("\\", "/")
)
else:
files.append(
os.path.join(os.path.dirname(self.fname), f).replace("\\", "/")
os.path.join(
os.path.dirname(self.fname), remainder[0]
).replace("\\", "/")
)
name = context["subset"]["name"]
name += "_{}".format(uuid.uuid4())
read_node = harmony.send(
{
"function": copy_files + import_files,
"args": ["Top", files, context["subset"]["name"], 1]
"args": ["Top", files, name, 1]
}
)["result"]
self[:] = [read_node]
return harmony.containerise(
name,
namespace,
@ -188,17 +271,25 @@ class ImageSequenceLoader(api.Loader):
)
def update(self, container, representation):
node = container.pop("node")
node = harmony.find_node_by_name(container["name"], "READ")
path = api.get_representation_path(representation)
collections, remainder = clique.assemble(
os.listdir(
os.path.dirname(api.get_representation_path(representation))
)
os.listdir(os.path.dirname(path))
)
files = []
for f in list(collections[0]):
if collections:
for f in list(collections[0]):
files.append(
os.path.join(
os.path.dirname(path), f
).replace("\\", "/")
)
else:
files.append(
os.path.join(os.path.dirname(self.fname), f).replace("\\", "/")
os.path.join(
os.path.dirname(path), remainder[0]
).replace("\\", "/")
)
harmony.send(
@ -208,12 +299,34 @@ class ImageSequenceLoader(api.Loader):
}
)
# Colour node.
func = """function func(args){
for( var i =0; i <= args[0].length - 1; ++i)
{
var red_color = new ColorRGBA(255, 0, 0, 255);
var green_color = new ColorRGBA(0, 255, 0, 255);
if (args[1] == "red"){
node.setColor(args[0], red_color);
}
if (args[1] == "green"){
node.setColor(args[0], green_color);
}
}
}
func
"""
if pype.lib.is_latest(representation):
harmony.send({"function": func, "args": [node, "green"]})
else:
harmony.send({"function": func, "args": [node, "red"]})
harmony.imprint(
node, {"representation": str(representation["_id"])}
)
def remove(self, container):
node = container.pop("node")
node = harmony.find_node_by_name(container["name"], "READ")
func = """function deleteNode(_node)
{
node.deleteNode(_node, true, true);

View file

@ -0,0 +1,66 @@
import os
import shutil
from avalon import api, harmony
from avalon.vendor import Qt
class ImportPaletteLoader(api.Loader):
"""Import palettes."""
families = ["harmony.palette"]
representations = ["plt"]
label = "Import Palette"
def load(self, context, name=None, namespace=None, data=None):
name = self.load_palette(context["representation"])
return harmony.containerise(
name,
namespace,
name,
context,
self.__class__.__name__
)
def load_palette(self, representation):
subset_name = representation["context"]["subset"]
name = subset_name.replace("palette", "")
# Overwrite palette on disk.
scene_path = harmony.send(
{"function": "scene.currentProjectPath"}
)["result"]
src = api.get_representation_path(representation)
dst = os.path.join(
scene_path,
"palette-library",
"{}.plt".format(name)
)
shutil.copy(src, dst)
harmony.save_scene()
# Dont allow instances with the same name.
message_box = Qt.QtWidgets.QMessageBox()
message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning)
msg = "Updated {}.".format(subset_name)
msg += " You need to reload the scene to see the changes."
message_box.setText(msg)
message_box.exec_()
return name
def remove(self, container):
harmony.remove(container["name"])
def switch(self, container, representation):
self.update(container, representation)
def update(self, container, representation):
self.remove(container)
name = self.load_palette(representation)
container["representation"] = str(representation["_id"])
container["name"] = name
harmony.imprint(name, container)

View file

@ -40,5 +40,5 @@ class ImportWorkfileLoader(ImportTemplateLoader):
"""Import workfiles."""
families = ["workfile"]
representations = ["*"]
representations = ["zip"]
label = "Import Workfile"

View file

@ -0,0 +1,45 @@
import os
import json
import pyblish.api
from avalon import harmony
class CollectPalettes(pyblish.api.ContextPlugin):
"""Gather palettes from scene when publishing templates."""
label = "Palettes"
order = pyblish.api.CollectorOrder
hosts = ["harmony"]
def process(self, context):
func = """function func()
{
var palette_list = PaletteObjectManager.getScenePaletteList();
var palettes = {};
for(var i=0; i < palette_list.numPalettes; ++i)
{
var palette = palette_list.getPaletteByIndex(i);
palettes[palette.getName()] = palette.id;
}
return palettes;
}
func
"""
palettes = harmony.send({"function": func})["result"]
for name, id in palettes.items():
instance = context.create_instance(name)
instance.data.update({
"id": id,
"family": "harmony.palette",
"asset": os.environ["AVALON_ASSET"],
"subset": "palette" + name
})
self.log.info(
"Created instance:\n" + json.dumps(
instance.data, sort_keys=True, indent=4
)
)

View file

@ -0,0 +1,34 @@
import os
from avalon import harmony
import pype.api
import pype.hosts.harmony
class ExtractPalette(pype.api.Extractor):
"""Extract palette."""
label = "Extract Palette"
hosts = ["harmony"]
families = ["harmony.palette"]
def process(self, instance):
func = """function func(args)
{
var palette_list = PaletteObjectManager.getScenePaletteList();
var palette = palette_list.getPaletteById(args[0]);
return (palette.getPath() + "/" + palette.getName() + ".plt");
}
func
"""
palette_file = harmony.send(
{"function": func, "args": [instance.data["id"]]}
)["result"]
representation = {
"name": "plt",
"ext": "plt",
"files": os.path.basename(palette_file),
"stagingDir": os.path.dirname(palette_file)
}
instance.data["representations"] = [representation]

View file

@ -50,8 +50,11 @@ class ImagePlaneLoader(api.Loader):
camera = selection[0]
camera.displayResolution.set(1)
camera.farClipPlane.set(image_plane_depth * 10)
try:
camera.displayResolution.set(1)
camera.farClipPlane.set(image_plane_depth * 10)
except RuntimeError:
pass
# Create image plane
image_plane_transform, image_plane_shape = pc.imagePlane(

View file

@ -4,14 +4,14 @@ import pyblish.api
import pype.api
class ValidateNukeWriteKnobs(pyblish.api.ContextPlugin):
class ValidateKnobs(pyblish.api.ContextPlugin):
"""Ensure knobs are consistent.
Knobs to validate and their values comes from the
Example for presets in config:
"presets/plugins/nuke/publish.json" preset, which needs this structure:
"ValidateNukeWriteKnobs": {
"ValidateKnobs": {
"enabled": true,
"knobs": {
"family": {
@ -22,22 +22,31 @@ class ValidateNukeWriteKnobs(pyblish.api.ContextPlugin):
"""
order = pyblish.api.ValidatorOrder
label = "Validate Write Knobs"
label = "Validate Knobs"
hosts = ["nuke"]
actions = [pype.api.RepairContextAction]
optional = True
def process(self, context):
# Check for preset existence.
if not getattr(self, "knobs"):
nuke_presets = context.data["presets"].get("nuke")
if not nuke_presets:
return
publish_presets = nuke_presets.get("publish")
if not publish_presets:
return
plugin_preset = publish_presets.get("ValidateKnobs")
if not plugin_preset:
return
self.log.debug("__ self.knobs: {}".format(self.knobs))
invalid = self.get_invalid(context, compute=True)
if invalid:
raise RuntimeError(
"Found knobs with invalid values: {}".format(invalid)
"Found knobs with invalid values:\n{}".format(invalid)
)
@classmethod
@ -51,6 +60,8 @@ class ValidateNukeWriteKnobs(pyblish.api.ContextPlugin):
@classmethod
def get_invalid_knobs(cls, context):
invalid_knobs = []
publish_presets = context.data["presets"]["nuke"]["publish"]
knobs_preset = publish_presets["ValidateKnobs"]["knobs"]
for instance in context:
# Filter publisable instances.
if not instance.data["publish"]:
@ -59,15 +70,15 @@ class ValidateNukeWriteKnobs(pyblish.api.ContextPlugin):
# Filter families.
families = [instance.data["family"]]
families += instance.data.get("families", [])
families = list(set(families) & set(cls.knobs.keys()))
families = list(set(families) & set(knobs_preset.keys()))
if not families:
continue
# Get all knobs to validate.
knobs = {}
for family in families:
for preset in cls.knobs[family]:
knobs.update({preset: cls.knobs[family][preset]})
for preset in knobs_preset[family]:
knobs.update({preset: knobs_preset[family][preset]})
# Get invalid knobs.
nodes = []
@ -82,16 +93,20 @@ class ValidateNukeWriteKnobs(pyblish.api.ContextPlugin):
for node in nodes:
for knob in node.knobs():
if knob in knobs.keys():
expected = knobs[knob]
if node[knob].value() != expected:
invalid_knobs.append(
{
"knob": node[knob],
"expected": expected,
"current": node[knob].value()
}
)
if knob not in knobs.keys():
continue
expected = knobs[knob]
if node[knob].value() != expected:
invalid_knobs.append(
{
"knob": node[knob],
"name": node[knob].name(),
"label": node[knob].label(),
"expected": expected,
"current": node[knob].value()
}
)
context.data["invalid_knobs"] = invalid_knobs
return invalid_knobs

View file

@ -74,4 +74,5 @@ class CreateImage(api.Creator):
groups.append(group)
for group in groups:
self.data.update({"subset": "image" + group.Name})
photoshop.imprint(group, self.data)

View file

@ -0,0 +1,36 @@
import os
import pythoncom
import pyblish.api
class CollectReview(pyblish.api.ContextPlugin):
"""Gather the active document as review instance."""
label = "Review"
order = pyblish.api.CollectorOrder
hosts = ["photoshop"]
def process(self, context):
# Necessary call when running in a different thread which pyblish-qml
# can be.
pythoncom.CoInitialize()
family = "review"
task = os.getenv("AVALON_TASK", None)
subset = family + task.capitalize()
file_path = context.data["currentFile"]
base_name = os.path.basename(file_path)
instance = context.create_instance(subset)
instance.data.update({
"subset": subset,
"label": base_name,
"name": base_name,
"family": family,
"families": ["ftrack"],
"representations": [],
"asset": os.environ["AVALON_ASSET"]
})

View file

@ -0,0 +1,103 @@
import os
import pype.api
import pype.lib
from avalon import photoshop
class ExtractReview(pype.api.Extractor):
"""Produce a flattened image file from all instances."""
label = "Extract Review"
hosts = ["photoshop"]
families = ["review"]
def process(self, instance):
staging_dir = self.staging_dir(instance)
self.log.info("Outputting image to {}".format(staging_dir))
layers = []
for image_instance in instance.context:
if image_instance.data["family"] != "image":
continue
layers.append(image_instance[0])
# Perform extraction
output_image = "{} copy.jpg".format(
os.path.splitext(photoshop.app().ActiveDocument.Name)[0]
)
with photoshop.maintained_visibility():
# Hide all other layers.
extract_ids = [
x.id for x in photoshop.get_layers_in_layers(layers)
]
for layer in photoshop.get_layers_in_document():
if layer.id in extract_ids:
layer.Visible = True
else:
layer.Visible = False
photoshop.app().ActiveDocument.SaveAs(
staging_dir, photoshop.com_objects.JPEGSaveOptions(), True
)
instance.data["representations"].append({
"name": "jpg",
"ext": "jpg",
"files": output_image,
"stagingDir": staging_dir
})
instance.data["stagingDir"] = staging_dir
# Generate thumbnail.
thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg")
args = [
"ffmpeg", "-y",
"-i", os.path.join(staging_dir, output_image),
"-vf", "scale=300:-1",
"-vframes", "1",
thumbnail_path
]
output = pype.lib._subprocess(args, cwd=os.environ["FFMPEG_PATH"])
self.log.debug(output)
instance.data["representations"].append({
"name": "thumbnail",
"ext": "jpg",
"files": os.path.basename(thumbnail_path),
"stagingDir": staging_dir,
"tags": ["thumbnail"]
})
# Generate mov.
mov_path = os.path.join(staging_dir, "review.mov")
args = [
"ffmpeg", "-y",
"-i", os.path.join(staging_dir, output_image),
"-vframes", "1",
mov_path
]
output = pype.lib._subprocess(args, cwd=os.environ["FFMPEG_PATH"])
self.log.debug(output)
instance.data["representations"].append({
"name": "mov",
"ext": "mov",
"files": os.path.basename(mov_path),
"stagingDir": staging_dir,
"frameStart": 1,
"frameEnd": 1,
"fps": 25,
"preview": True,
"tags": ["review", "ftrackreview"]
})
# Required for extract_review plugin (L222 onwards).
instance.data["frameStart"] = 1
instance.data["frameEnd"] = 1
instance.data["fps"] = 25
self.log.info(f"Extracted {instance} to {staging_dir}")

View file

@ -1,5 +1,6 @@
import pyblish.api
import pype.api
from avalon import photoshop
class ValidateNamingRepair(pyblish.api.Action):
@ -22,7 +23,11 @@ class ValidateNamingRepair(pyblish.api.Action):
instances = pyblish.api.instances_by_plugin(failed, plugin)
for instance in instances:
instance[0].Name = instance.data["name"].replace(" ", "_")
name = instance.data["name"].replace(" ", "_")
instance[0].Name = name
data = photoshop.read(instance[0])
data["subset"] = "image" + name
photoshop.imprint(instance[0], data)
return True
@ -42,3 +47,6 @@ class ValidateNaming(pyblish.api.InstancePlugin):
def process(self, instance):
msg = "Name \"{}\" is not allowed.".format(instance.data["name"])
assert " " not in instance.data["name"], msg
msg = "Subset \"{}\" is not allowed.".format(instance.data["subset"])
assert " " not in instance.data["subset"], msg

View file

@ -56,12 +56,18 @@ class CollectShots(pyblish.api.InstancePlugin):
asset_entity = instance.context.data["assetEntity"]
asset_name = asset_entity["name"]
# Ask user for sequence start. Usually 10:00:00:00.
sequence_start_frame = 900000
# Project specific prefix naming. This needs to be replaced with some
# options to be more flexible.
asset_name = asset_name.split("_")[0]
instances = []
for track in tracks:
track_start_frame = (
abs(track.source_range.start_time.value) - sequence_start_frame
)
for child in track.each_child():
# Transitions are ignored, because Clips have the full frame
@ -69,12 +75,17 @@ class CollectShots(pyblish.api.InstancePlugin):
if isinstance(child, otio.schema.transition.Transition):
continue
if child.name is None:
continue
# Hardcoded to expect a shot name of "[name].[extension]"
child_name = os.path.splitext(child.name)[0].lower()
name = f"{asset_name}_{child_name}"
frame_start = child.range_in_parent().start_time.value
frame_end = child.range_in_parent().end_time_inclusive().value
frame_start = track_start_frame
frame_start += child.range_in_parent().start_time.value
frame_end = track_start_frame
frame_end += child.range_in_parent().end_time_inclusive().value
label = f"{name} (framerange: {frame_start}-{frame_end})"
instances.append(

View file

@ -14,3 +14,25 @@ def get_resource(*args):
*args
)
)
def pype_icon_filepath(debug=None):
if debug is None:
debug = bool(os.getenv("PYPE_DEV"))
if debug:
icon_file_name = "pype_icon_dev.png"
else:
icon_file_name = "pype_icon.png"
return get_resource("icons", icon_file_name)
def pype_splash_filepath(debug=None):
if debug is None:
debug = bool(os.getenv("PYPE_DEV"))
if debug:
splash_file_name = "pype_splash_dev.png"
else:
splash_file_name = "pype_splash.png"
return get_resource("icons", splash_file_name)

View file

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

@ -1,5 +1,7 @@
from Qt import QtCore
EXPANDER_WIDTH = 20
def flags(*args, **kwargs):
type_name = kwargs.pop("type_name", "Flags")

View file

@ -5,7 +5,7 @@ from Qt import QtWidgets, QtGui, QtCore
from . import model
from .awesome import tags as awesome
from .constants import (
PluginStates, InstanceStates, PluginActionStates, Roles
PluginStates, InstanceStates, PluginActionStates, Roles, EXPANDER_WIDTH
)
colors = {
@ -14,12 +14,16 @@ colors = {
"ok": QtGui.QColor("#77AE24"),
"active": QtGui.QColor("#99CEEE"),
"idle": QtCore.Qt.white,
"font": QtGui.QColor("#DDD"),
"inactive": QtGui.QColor("#888"),
"hover": QtGui.QColor(255, 255, 255, 10),
"selected": QtGui.QColor(255, 255, 255, 20),
"outline": QtGui.QColor("#333"),
"group": QtGui.QColor("#333")
"group": QtGui.QColor("#333"),
"group-hover": QtGui.QColor("#3c3c3c"),
"group-selected-hover": QtGui.QColor("#555555"),
"expander-bg": QtGui.QColor("#222"),
"expander-hover": QtGui.QColor("#2d6c9f"),
"expander-selected-hover": QtGui.QColor("#3784c5")
}
scale_factors = {"darwin": 1.5}
@ -279,14 +283,169 @@ class InstanceItemDelegate(QtWidgets.QStyledItemDelegate):
return QtCore.QSize(option.rect.width(), 20)
class OverviewGroupSection(QtWidgets.QStyledItemDelegate):
"""Generic delegate for section header"""
class InstanceDelegate(QtWidgets.QStyledItemDelegate):
"""Generic delegate for instance header"""
item_class = None
radius = 8.0
def __init__(self, parent):
super(OverviewGroupSection, self).__init__(parent)
self.item_delegate = self.item_class(parent)
super(InstanceDelegate, self).__init__(parent)
self.item_delegate = InstanceItemDelegate(parent)
def paint(self, painter, option, index):
if index.data(Roles.TypeRole) in (
model.InstanceType, model.PluginType
):
self.item_delegate.paint(painter, option, index)
return
self.group_item_paint(painter, option, index)
def group_item_paint(self, painter, option, index):
"""Paint text
_
My label
"""
body_rect = QtCore.QRectF(option.rect)
bg_rect = QtCore.QRectF(
body_rect.left(), body_rect.top() + 1,
body_rect.width() - 5, body_rect.height() - 2
)
expander_rect = QtCore.QRectF(bg_rect)
expander_rect.setWidth(EXPANDER_WIDTH)
remainder_rect = QtCore.QRectF(
expander_rect.x() + expander_rect.width(),
expander_rect.y(),
bg_rect.width() - expander_rect.width(),
expander_rect.height()
)
width = float(expander_rect.width())
height = float(expander_rect.height())
x_pos = expander_rect.x()
y_pos = expander_rect.y()
x_radius = min(self.radius, width / 2)
y_radius = min(self.radius, height / 2)
x_radius2 = x_radius * 2
y_radius2 = y_radius * 2
expander_path = QtGui.QPainterPath()
expander_path.moveTo(x_pos, y_pos + y_radius)
expander_path.arcTo(
x_pos, y_pos,
x_radius2, y_radius2,
180.0, -90.0
)
expander_path.lineTo(x_pos + width, y_pos)
expander_path.lineTo(x_pos + width, y_pos + height)
expander_path.lineTo(x_pos + x_radius, y_pos + height)
expander_path.arcTo(
x_pos, y_pos + height - y_radius2,
x_radius2, y_radius2,
270.0, -90.0
)
expander_path.closeSubpath()
width = float(remainder_rect.width())
height = float(remainder_rect.height())
x_pos = remainder_rect.x()
y_pos = remainder_rect.y()
x_radius = min(self.radius, width / 2)
y_radius = min(self.radius, height / 2)
x_radius2 = x_radius * 2
y_radius2 = y_radius * 2
remainder_path = QtGui.QPainterPath()
remainder_path.moveTo(x_pos + width, y_pos + height - y_radius)
remainder_path.arcTo(
x_pos + width - x_radius2, y_pos + height - y_radius2,
x_radius2, y_radius2,
0.0, -90.0
)
remainder_path.lineTo(x_pos, y_pos + height)
remainder_path.lineTo(x_pos, y_pos)
remainder_path.lineTo(x_pos + width - x_radius, y_pos)
remainder_path.arcTo(
x_pos + width - x_radius2, y_pos,
x_radius2, y_radius2,
90.0, -90.0
)
remainder_path.closeSubpath()
painter.fillPath(expander_path, colors["expander-bg"])
painter.fillPath(remainder_path, colors["group"])
mouse_pos = option.widget.mapFromGlobal(QtGui.QCursor.pos())
selected = option.state & QtWidgets.QStyle.State_Selected
hovered = option.state & QtWidgets.QStyle.State_MouseOver
if selected and hovered:
if expander_rect.contains(mouse_pos):
painter.fillPath(
expander_path, colors["expander-selected-hover"]
)
else:
painter.fillPath(
remainder_path, colors["group-selected-hover"]
)
elif hovered:
if expander_rect.contains(mouse_pos):
painter.fillPath(expander_path, colors["expander-hover"])
else:
painter.fillPath(remainder_path, colors["group-hover"])
text_height = font_metrics["awesome6"].height()
adjust_value = (expander_rect.height() - text_height) / 2
expander_rect.adjust(
adjust_value + 1.5, adjust_value - 0.5,
-adjust_value + 1.5, -adjust_value - 0.5
)
offset = (remainder_rect.height() - font_metrics["h5"].height()) / 2
label_rect = QtCore.QRectF(remainder_rect.adjusted(
5, offset - 1, 0, 0
))
expander_icon = icons["plus-sign"]
expanded = self.parent().isExpanded(index)
if expanded:
expander_icon = icons["minus-sign"]
label = index.data(QtCore.Qt.DisplayRole)
label = font_metrics["h5"].elidedText(
label, QtCore.Qt.ElideRight, label_rect.width()
)
# Maintain reference to state, so we can restore it once we're done
painter.save()
painter.setFont(fonts["awesome6"])
painter.setPen(QtGui.QPen(colors["idle"]))
painter.drawText(expander_rect, QtCore.Qt.AlignCenter, expander_icon)
# Draw label
painter.setFont(fonts["h5"])
painter.drawText(label_rect, label)
# Ok, we're done, tidy up.
painter.restore()
def sizeHint(self, option, index):
return QtCore.QSize(option.rect.width(), 20)
class PluginDelegate(QtWidgets.QStyledItemDelegate):
"""Generic delegate for plugin header"""
def __init__(self, parent):
super(PluginDelegate, self).__init__(parent)
self.item_delegate = PluginItemDelegate(parent)
def paint(self, painter, option, index):
if index.data(Roles.TypeRole) in (
@ -310,7 +469,14 @@ class OverviewGroupSection(QtWidgets.QStyledItemDelegate):
radius = 8.0
bg_path = QtGui.QPainterPath()
bg_path.addRoundedRect(bg_rect, radius, radius)
painter.fillPath(bg_path, colors["group"])
hovered = option.state & QtWidgets.QStyle.State_MouseOver
selected = option.state & QtWidgets.QStyle.State_Selected
if hovered and selected:
painter.fillPath(bg_path, colors["group-selected-hover"])
elif hovered:
painter.fillPath(bg_path, colors["group-hover"])
else:
painter.fillPath(bg_path, colors["group"])
expander_rect = QtCore.QRectF(bg_rect)
expander_rect.setWidth(expander_rect.height())
@ -343,18 +509,12 @@ class OverviewGroupSection(QtWidgets.QStyledItemDelegate):
painter.setFont(fonts["awesome6"])
painter.setPen(QtGui.QPen(colors["idle"]))
painter.drawText(expander_rect, expander_icon)
painter.drawText(expander_rect, QtCore.Qt.AlignCenter, expander_icon)
# Draw label
painter.setFont(fonts["h5"])
painter.drawText(label_rect, label)
if option.state & QtWidgets.QStyle.State_MouseOver:
painter.fillPath(bg_path, colors["hover"])
if option.state & QtWidgets.QStyle.State_Selected:
painter.fillPath(bg_path, colors["selected"])
# Ok, we're done, tidy up.
painter.restore()
@ -362,16 +522,6 @@ class OverviewGroupSection(QtWidgets.QStyledItemDelegate):
return QtCore.QSize(option.rect.width(), 20)
class PluginDelegate(OverviewGroupSection):
"""Generic delegate for model items in proxy tree view"""
item_class = PluginItemDelegate
class InstanceDelegate(OverviewGroupSection):
"""Generic delegate for model items in proxy tree view"""
item_class = InstanceItemDelegate
class ArtistDelegate(QtWidgets.QStyledItemDelegate):
"""Delegate used on Artist page"""

View file

@ -319,7 +319,7 @@ class PluginItem(QtGui.QStandardItem):
return False
self.plugin.active = value
self.emitDataChanged()
return True
return
elif role == Roles.PluginActionProgressRole:
if isinstance(value, list):
@ -652,14 +652,14 @@ class InstanceItem(QtGui.QStandardItem):
def setData(self, value, role=(QtCore.Qt.UserRole + 1)):
if role == QtCore.Qt.CheckStateRole:
if not self.data(Roles.IsEnabledRole):
return False
return
self.instance.data["publish"] = value
self.emitDataChanged()
return True
return
if role == Roles.IsEnabledRole:
if not self.instance.optional:
return False
return
if role == Roles.PublishFlagsRole:
if isinstance(value, list):
@ -692,12 +692,12 @@ class InstanceItem(QtGui.QStandardItem):
self.instance._publish_states = value
self.emitDataChanged()
return True
return
if role == Roles.LogRecordsRole:
self.instance._logs = value
self.emitDataChanged()
return True
return
return super(InstanceItem, self).setData(value, role)

View file

@ -1,6 +1,6 @@
from Qt import QtCore, QtWidgets
from . import model
from .constants import Roles
from .constants import Roles, EXPANDER_WIDTH
# Imported when used
widgets = None
@ -84,8 +84,6 @@ class OverviewView(QtWidgets.QTreeView):
self.setRootIsDecorated(False)
self.setIndentation(0)
self.clicked.connect(self.item_expand)
def event(self, event):
if not event.type() == QtCore.QEvent.KeyPress:
return super(OverviewView, self).event(event)
@ -113,6 +111,24 @@ class OverviewView(QtWidgets.QTreeView):
def focusOutEvent(self, event):
self.selectionModel().clear()
def mouseReleaseEvent(self, event):
if event.button() in (QtCore.Qt.LeftButton, QtCore.Qt.RightButton):
# Deselect all group labels
indexes = self.selectionModel().selectedIndexes()
for index in indexes:
if index.data(Roles.TypeRole) == model.GroupType:
self.selectionModel().select(
index, QtCore.QItemSelectionModel.Deselect
)
return super(OverviewView, self).mouseReleaseEvent(event)
class PluginView(OverviewView):
def __init__(self, *args, **kwargs):
super(PluginView, self).__init__(*args, **kwargs)
self.clicked.connect(self.item_expand)
def item_expand(self, index):
if index.data(Roles.TypeRole) == model.GroupType:
if self.isExpanded(index):
@ -125,23 +141,86 @@ class OverviewView(QtWidgets.QTreeView):
indexes = self.selectionModel().selectedIndexes()
if len(indexes) == 1:
index = indexes[0]
# If instance or Plugin
if index.data(Roles.TypeRole) in (
model.InstanceType, model.PluginType
pos_index = self.indexAt(event.pos())
# If instance or Plugin and is selected
if (
index == pos_index
and index.data(Roles.TypeRole) == model.PluginType
):
if event.pos().x() < 20:
self.toggled.emit(index, None)
elif event.pos().x() > self.width() - 20:
self.show_perspective.emit(index)
# Deselect all group labels
for index in indexes:
if index.data(Roles.TypeRole) == model.GroupType:
self.selectionModel().select(
index, QtCore.QItemSelectionModel.Deselect
)
return super(PluginView, self).mouseReleaseEvent(event)
return super(OverviewView, self).mouseReleaseEvent(event)
class InstanceView(OverviewView):
def __init__(self, parent=None):
super(InstanceView, self).__init__(parent)
self.viewport().setMouseTracking(True)
def mouseMoveEvent(self, event):
index = self.indexAt(event.pos())
if index.data(Roles.TypeRole) == model.GroupType:
self.update(index)
super(InstanceView, self).mouseMoveEvent(event)
def item_expand(self, index, expand=None):
if expand is None:
expand = not self.isExpanded(index)
if expand:
self.expand(index)
else:
self.collapse(index)
def group_toggle(self, index):
model = index.model()
chilren_indexes_checked = []
chilren_indexes_unchecked = []
for idx in range(model.rowCount(index)):
child_index = model.index(idx, 0, index)
if not child_index.data(Roles.IsEnabledRole):
continue
if child_index.data(QtCore.Qt.CheckStateRole):
chilren_indexes_checked.append(child_index)
else:
chilren_indexes_unchecked.append(child_index)
if chilren_indexes_checked:
to_change_indexes = chilren_indexes_checked
new_state = False
else:
to_change_indexes = chilren_indexes_unchecked
new_state = True
for index in to_change_indexes:
model.setData(index, new_state, QtCore.Qt.CheckStateRole)
self.toggled.emit(index, new_state)
def mouseReleaseEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
indexes = self.selectionModel().selectedIndexes()
if len(indexes) == 1:
index = indexes[0]
pos_index = self.indexAt(event.pos())
if index == pos_index:
# If instance or Plugin
if index.data(Roles.TypeRole) == model.InstanceType:
if event.pos().x() < 20:
self.toggled.emit(index, None)
elif event.pos().x() > self.width() - 20:
self.show_perspective.emit(index)
else:
if event.pos().x() < EXPANDER_WIDTH:
self.item_expand(index)
else:
self.group_toggle(index)
self.item_expand(index, True)
return super(InstanceView, self).mouseReleaseEvent(event)
class TerminalView(QtWidgets.QTreeView):

View file

@ -160,14 +160,14 @@ class Window(QtWidgets.QDialog):
# TODO add parent
overview_page = QtWidgets.QWidget()
overview_instance_view = view.OverviewView(parent=overview_page)
overview_instance_view = view.InstanceView(parent=overview_page)
overview_instance_delegate = delegate.InstanceDelegate(
parent=overview_instance_view
)
overview_instance_view.setItemDelegate(overview_instance_delegate)
overview_instance_view.setModel(instance_model)
overview_plugin_view = view.OverviewView(parent=overview_page)
overview_plugin_view = view.PluginView(parent=overview_page)
overview_plugin_delegate = delegate.PluginDelegate(
parent=overview_plugin_view
)

View file

@ -0,0 +1,58 @@
[
{
"title": "User settings",
"type": "module",
"import_path": "pype.modules.user",
"fromlist": ["pype", "modules"]
}, {
"title": "Ftrack",
"type": "module",
"import_path": "pype.modules.ftrack.tray",
"fromlist": ["pype", "modules", "ftrack"]
}, {
"title": "Muster",
"type": "module",
"import_path": "pype.modules.muster",
"fromlist": ["pype", "modules"]
}, {
"title": "Avalon",
"type": "module",
"import_path": "pype.modules.avalon_apps",
"fromlist": ["pype", "modules"]
}, {
"title": "Clockify",
"type": "module",
"import_path": "pype.modules.clockify",
"fromlist": ["pype", "modules"]
}, {
"title": "Standalone Publish",
"type": "module",
"import_path": "pype.modules.standalonepublish",
"fromlist": ["pype", "modules"]
}, {
"title": "Logging",
"type": "module",
"import_path": "pype.modules.logging.tray",
"fromlist": ["pype", "modules", "logging"]
}, {
"title": "Idle Manager",
"type": "module",
"import_path": "pype.modules.idle_manager",
"fromlist": ["pype","modules"]
}, {
"title": "Timers Manager",
"type": "module",
"import_path": "pype.modules.timers_manager",
"fromlist": ["pype","modules"]
}, {
"title": "Rest Api",
"type": "module",
"import_path": "pype.modules.rest_api",
"fromlist": ["pype","modules"]
}, {
"title": "Adobe Communicator",
"type": "module",
"import_path": "pype.modules.adobe_communicator",
"fromlist": ["pype", "modules"]
}
]

View file

@ -3,8 +3,7 @@ import sys
import platform
from avalon import style
from Qt import QtCore, QtGui, QtWidgets, QtSvg
from pype.resources import get_resource
from pype.api import config, Logger
from pype.api import config, Logger, resources
class TrayManager:
@ -12,28 +11,40 @@ class TrayManager:
Load submenus, actions, separators and modules into tray's context.
"""
modules = {}
services = {}
services_submenu = None
errors = []
items = (
config.get_presets(first_run=True)
.get('tray', {})
.get('menu_items', [])
)
available_sourcetypes = ['python', 'file']
available_sourcetypes = ["python", "file"]
def __init__(self, tray_widget, main_window):
self.tray_widget = tray_widget
self.main_window = main_window
self.log = Logger().get_logger(self.__class__.__name__)
self.icon_run = QtGui.QIcon(get_resource('circle_green.png'))
self.icon_stay = QtGui.QIcon(get_resource('circle_orange.png'))
self.icon_failed = QtGui.QIcon(get_resource('circle_red.png'))
self.modules = {}
self.services = {}
self.services_submenu = None
self.services_thread = None
self.errors = []
CURRENT_DIR = os.path.dirname(__file__)
self.modules_imports = config.load_json(
os.path.join(CURRENT_DIR, "modules_imports.json")
)
presets = config.get_presets(first_run=True)
try:
self.modules_usage = presets["tray"]["menu_items"]["item_usage"]
except Exception:
self.modules_usage = {}
self.log.critical("Couldn't find modules usage data.")
self.icon_run = QtGui.QIcon(
resources.get_resource("icons", "circle_green.png")
)
self.icon_stay = QtGui.QIcon(
resources.get_resource("icons", "circle_orange.png")
)
self.icon_failed = QtGui.QIcon(
resources.get_resource("icons", "circle_red.png")
)
def process_presets(self):
"""Add modules to tray by presets.
@ -46,42 +57,26 @@ class TrayManager:
"item_usage": {
"Statics Server": false
}
}, {
"item_import": [{
"title": "Ftrack",
"type": "module",
"import_path": "pype.ftrack.tray",
"fromlist": ["pype", "ftrack"]
}, {
"title": "Statics Server",
"type": "module",
"import_path": "pype.services.statics_server",
"fromlist": ["pype","services"]
}]
}
In this case `Statics Server` won't be used.
"""
# Backwards compatible presets loading
if isinstance(self.items, list):
items = self.items
else:
items = []
# Get booleans is module should be used
usages = self.items.get("item_usage") or {}
for item in self.items.get("item_import", []):
import_path = item.get("import_path")
title = item.get("title")
item_usage = usages.get(title)
if item_usage is None:
item_usage = usages.get(import_path, True)
items = []
# Get booleans is module should be used
for item in self.modules_imports:
import_path = item.get("import_path")
title = item.get("title")
if item_usage:
items.append(item)
else:
if not title:
title = import_path
self.log.debug("{} - Module ignored".format(title))
item_usage = self.modules_usage.get(title)
if item_usage is None:
item_usage = self.modules_usage.get(import_path, True)
if item_usage:
items.append(item)
else:
if not title:
title = import_path
self.log.info("{} - Module ignored".format(title))
if items:
self.process_items(items, self.tray_widget.menu)
@ -333,12 +328,7 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
:type parent: QtWidgets.QMainWindow
"""
def __init__(self, parent):
if os.getenv("PYPE_DEV"):
icon_file_name = "icon_dev.png"
else:
icon_file_name = "icon.png"
self.icon = QtGui.QIcon(get_resource(icon_file_name))
self.icon = QtGui.QIcon(resources.pype_icon_filepath())
QtWidgets.QSystemTrayIcon.__init__(self, self.icon, parent)
@ -402,7 +392,7 @@ class TrayMainWindow(QtWidgets.QMainWindow):
self.trayIcon.show()
def set_working_widget(self):
image_file = get_resource('working.svg')
image_file = resources.get_resource("icons", "working.svg")
img_pix = QtGui.QPixmap(image_file)
if image_file.endswith('.svg'):
widget = QtSvg.QSvgWidget(image_file)
@ -492,11 +482,7 @@ class PypeTrayApplication(QtWidgets.QApplication):
splash_widget.hide()
def set_splash(self):
if os.getenv("PYPE_DEV"):
splash_file_name = "splash_dev.png"
else:
splash_file_name = "splash.png"
splash_pix = QtGui.QPixmap(get_resource(splash_file_name))
splash_pix = QtGui.QPixmap(resources.pype_splash_filepath())
splash = QtWidgets.QSplashScreen(splash_pix)
splash.setMask(splash_pix.mask())
splash.setEnabled(False)

View file

@ -52,6 +52,19 @@ def message(title=None, message=None, level="info", parent=None):
app = parent
if not app:
app = QtWidgets.QApplication(sys.argv)
ex = Window(app, title, message, level)
ex.show()
# Move widget to center of screen
try:
desktop_rect = QtWidgets.QApplication.desktop().availableGeometry(ex)
center = desktop_rect.center()
ex.move(
center.x() - (ex.width() * 0.5),
center.y() - (ex.height() * 0.5)
)
except Exception:
# skip all possible issues that may happen feature is not crutial
log.warning("Couldn't center message.", exc_info=True)
# sys.exit(app.exec_())