Merge branch '2.x/develop' into feature/move_module_imports_to_pype

This commit is contained in:
Milan Kolar 2020-07-08 12:38:59 +02:00 committed by GitHub
commit 18c857b31d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1648 additions and 453 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,16 +44,10 @@ 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["collection"] = collection_name
uri = compose_url(components)
uri = compose_url(**components)
return uri, components["port"], database_name, collection_name
@ -165,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

@ -0,0 +1,32 @@
"""Create a camera asset."""
import bpy
from avalon import api
from avalon.blender import Creator, lib
import pype.hosts.blender.plugin
class CreateCamera(Creator):
"""Polygonal static geometry"""
name = "cameraMain"
label = "Camera"
family = "camera"
icon = "video-camera"
def process(self):
asset = self.data["asset"]
subset = self.data["subset"]
name = pype.hosts.blender.plugin.asset_name(asset, subset)
collection = bpy.data.collections.new(name=name)
bpy.context.scene.collection.children.link(collection)
self.data['task'] = api.Session.get('AVALON_TASK')
lib.imprint(collection, self.data)
if (self.options or {}).get("useSelection"):
for obj in lib.get_selection():
collection.objects.link(obj)
return collection

View file

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

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

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

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

@ -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:
@ -37,9 +36,15 @@ class TrayManager:
self.modules_usage = {}
self.log.critical("Couldn't find modules usage data.")
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.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.
@ -323,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)
@ -392,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)
@ -482,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_())