diff --git a/pype/api.py b/pype/api.py index 5775bb3ce4..44a31f2626 100644 --- a/pype/api.py +++ b/pype/api.py @@ -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 diff --git a/pype/hosts/blender/__init__.py b/pype/hosts/blender/__init__.py index a6d3cd82ef..dafeca5107 100644 --- a/pype/hosts/blender/__init__.py +++ b/pype/hosts/blender/__init__.py @@ -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() diff --git a/pype/hosts/blender/plugin.py b/pype/hosts/blender/plugin.py index 77fce90d65..ab53d49041 100644 --- a/pype/hosts/blender/plugin.py +++ b/pype/hosts/blender/plugin.py @@ -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 diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index 3d49c60563..3cae695852 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -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): diff --git a/pype/hosts/nukestudio/workio.py b/pype/hosts/nukestudio/workio.py index eee6654a4c..2cf898aa33 100644 --- a/pype/hosts/nukestudio/workio.py +++ b/pype/hosts/nukestudio/workio.py @@ -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(): diff --git a/pype/hosts/photoshop/__init__.py b/pype/hosts/photoshop/__init__.py index 01ed757a8d..564e9c8a05 100644 --- a/pype/hosts/photoshop/__init__.py +++ b/pype/hosts/photoshop/__init__.py @@ -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.""" diff --git a/pype/modules/clockify/widget_message.py b/pype/modules/clockify/widget_message.py index f919c3f819..9e4fad1df1 100644 --- a/pype/modules/clockify/widget_message.py +++ b/pype/modules/clockify/widget_message.py @@ -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 | diff --git a/pype/modules/clockify/widget_settings.py b/pype/modules/clockify/widget_settings.py index 956bdb1916..7e5ee300bb 100644 --- a/pype/modules/clockify/widget_settings.py +++ b/pype/modules/clockify/widget_settings.py @@ -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( diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py index 8377187ebe..acf31ab437 100644 --- a/pype/modules/ftrack/ftrack_server/lib.py +++ b/pype/modules/ftrack/ftrack_server/lib.py @@ -26,7 +26,7 @@ from pype.api import ( compose_url ) -from pype.modules.ftrack.lib.custom_db_connector import DbConnector +from pype.modules.ftrack.lib.custom_db_connector import CustomDbConnector TOPIC_STATUS_SERVER = "pype.event.server.status" @@ -44,15 +44,8 @@ def get_ftrack_event_mongo_info(): mongo_url = os.environ.get("FTRACK_EVENTS_MONGO_URL") if mongo_url is not None: components = decompose_url(mongo_url) - _used_ftrack_url = True else: components = get_default_components() - _used_ftrack_url = False - - if not _used_ftrack_url or components["database"] is None: - components["database"] = database_name - - components.pop("collection", None) uri = compose_url(**components) @@ -166,10 +159,10 @@ class ProcessEventHub(SocketBaseEventHub): pypelog = Logger().get_logger("Session Processor") def __init__(self, *args, **kwargs): - self.dbcon = DbConnector( + self.dbcon = CustomDbConnector( self.uri, - self.port, self.database, + self.port, self.table_name ) super(ProcessEventHub, self).__init__(*args, **kwargs) diff --git a/pype/modules/ftrack/ftrack_server/sub_event_storer.py b/pype/modules/ftrack/ftrack_server/sub_event_storer.py index 61b9aaf2c8..1635f6cea3 100644 --- a/pype/modules/ftrack/ftrack_server/sub_event_storer.py +++ b/pype/modules/ftrack/ftrack_server/sub_event_storer.py @@ -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 = [] diff --git a/pype/modules/ftrack/lib/custom_db_connector.py b/pype/modules/ftrack/lib/custom_db_connector.py index a734b3f80a..d498d041dc 100644 --- a/pype/modules/ftrack/lib/custom_db_connector.py +++ b/pype/modules/ftrack/lib/custom_db_connector.py @@ -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() diff --git a/pype/modules/ftrack/lib/ftrack_app_handler.py b/pype/modules/ftrack/lib/ftrack_app_handler.py index 00bd13fd73..34ab8c5ee4 100644 --- a/pype/modules/ftrack/lib/ftrack_app_handler.py +++ b/pype/modules/ftrack/lib/ftrack_app_handler.py @@ -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"] diff --git a/pype/modules/ftrack/tray/login_dialog.py b/pype/modules/ftrack/tray/login_dialog.py index 3b8a366209..e0614513a3 100644 --- a/pype/modules/ftrack/tray/login_dialog.py +++ b/pype/modules/ftrack/tray/login_dialog.py @@ -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( diff --git a/pype/modules/idle_manager/idle_manager.py b/pype/modules/idle_manager/idle_manager.py index cfcdfef78f..3a9f9154a9 100644 --- a/pype/modules/idle_manager/idle_manager.py +++ b/pype/modules/idle_manager/idle_manager.py @@ -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() diff --git a/pype/modules/muster/widget_login.py b/pype/modules/muster/widget_login.py index 8de0d3136a..f446c13325 100644 --- a/pype/modules/muster/widget_login.py +++ b/pype/modules/muster/widget_login.py @@ -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( diff --git a/pype/modules/standalonepublish/widgets/widget_drop_frame.py b/pype/modules/standalonepublish/widgets/widget_drop_frame.py index 80e67aa69a..c91e906f45 100644 --- a/pype/modules/standalonepublish/widgets/widget_drop_frame.py +++ b/pype/modules/standalonepublish/widgets/widget_drop_frame.py @@ -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']: diff --git a/pype/modules/timers_manager/timers_manager.py b/pype/modules/timers_manager/timers_manager.py index cec730d007..8df7952baf 100644 --- a/pype/modules/timers_manager/timers_manager.py +++ b/pype/modules/timers_manager/timers_manager.py @@ -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) diff --git a/pype/modules/timers_manager/widget_user_idle.py b/pype/modules/timers_manager/widget_user_idle.py index 697c0a04d9..22455846fd 100644 --- a/pype/modules/timers_manager/widget_user_idle.py +++ b/pype/modules/timers_manager/widget_user_idle.py @@ -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) diff --git a/pype/modules/user/widget_user.py b/pype/modules/user/widget_user.py index 1d43941345..ba211c4737 100644 --- a/pype/modules/user/widget_user.py +++ b/pype/modules/user/widget_user.py @@ -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) diff --git a/pype/plugins/blender/load/load_layout.py b/pype/plugins/blender/load/load_layout.py index 0c1032c4fb..cfab5a207b 100644 --- a/pype/plugins/blender/load/load_layout.py +++ b/pype/plugins/blender/load/load_layout.py @@ -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) diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 4a8f43cd48..ad9137a15d 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -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 diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 3e53ff0363..e09a9cb92f 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -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) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 040ed9cd67..9f20999f55 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -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: diff --git a/pype/plugins/global/publish/validate_containers.py b/pype/plugins/global/publish/validate_containers.py index 44cb5def3c..1bf4967ec2 100644 --- a/pype/plugins/global/publish/validate_containers.py +++ b/pype/plugins/global/publish/validate_containers.py @@ -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] diff --git a/pype/plugins/harmony/load/load_imagesequence.py b/pype/plugins/harmony/load/load_imagesequence.py index 7862e027af..f81018d0fb 100644 --- a/pype/plugins/harmony/load/load_imagesequence.py +++ b/pype/plugins/harmony/load/load_imagesequence.py @@ -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); diff --git a/pype/plugins/harmony/load/load_palette.py b/pype/plugins/harmony/load/load_palette.py new file mode 100644 index 0000000000..001758d5a8 --- /dev/null +++ b/pype/plugins/harmony/load/load_palette.py @@ -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) diff --git a/pype/plugins/harmony/load/load_template_workfile.py b/pype/plugins/harmony/load/load_template_workfile.py index a9dcd0c776..b727cf865c 100644 --- a/pype/plugins/harmony/load/load_template_workfile.py +++ b/pype/plugins/harmony/load/load_template_workfile.py @@ -40,5 +40,5 @@ class ImportWorkfileLoader(ImportTemplateLoader): """Import workfiles.""" families = ["workfile"] - representations = ["*"] + representations = ["zip"] label = "Import Workfile" diff --git a/pype/plugins/harmony/publish/collect_palettes.py b/pype/plugins/harmony/publish/collect_palettes.py new file mode 100644 index 0000000000..2a2c1066c0 --- /dev/null +++ b/pype/plugins/harmony/publish/collect_palettes.py @@ -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 + ) + ) diff --git a/pype/plugins/harmony/publish/extract_palette.py b/pype/plugins/harmony/publish/extract_palette.py new file mode 100644 index 0000000000..9bca005278 --- /dev/null +++ b/pype/plugins/harmony/publish/extract_palette.py @@ -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] diff --git a/pype/plugins/maya/load/load_image_plane.py b/pype/plugins/maya/load/load_image_plane.py index e95ea6cd8f..653a8d4128 100644 --- a/pype/plugins/maya/load/load_image_plane.py +++ b/pype/plugins/maya/load/load_image_plane.py @@ -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( diff --git a/pype/plugins/nuke/publish/validate_write_knobs.py b/pype/plugins/nuke/publish/validate_knobs.py similarity index 61% rename from pype/plugins/nuke/publish/validate_write_knobs.py rename to pype/plugins/nuke/publish/validate_knobs.py index 24572bedb3..22f0d344c9 100644 --- a/pype/plugins/nuke/publish/validate_write_knobs.py +++ b/pype/plugins/nuke/publish/validate_knobs.py @@ -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 diff --git a/pype/plugins/photoshop/create/create_image.py b/pype/plugins/photoshop/create/create_image.py index ff0a5dcb6c..5b2f9f7981 100644 --- a/pype/plugins/photoshop/create/create_image.py +++ b/pype/plugins/photoshop/create/create_image.py @@ -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) diff --git a/pype/plugins/photoshop/publish/collect_review.py b/pype/plugins/photoshop/publish/collect_review.py new file mode 100644 index 0000000000..30042d188b --- /dev/null +++ b/pype/plugins/photoshop/publish/collect_review.py @@ -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"] + }) diff --git a/pype/plugins/photoshop/publish/extract_review.py b/pype/plugins/photoshop/publish/extract_review.py new file mode 100644 index 0000000000..49e932eb67 --- /dev/null +++ b/pype/plugins/photoshop/publish/extract_review.py @@ -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}") diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py index 1d85ea99a0..51e00da352 100644 --- a/pype/plugins/photoshop/publish/validate_naming.py +++ b/pype/plugins/photoshop/publish/validate_naming.py @@ -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 diff --git a/pype/plugins/standalonepublisher/publish/collect_shots.py b/pype/plugins/standalonepublisher/publish/collect_shots.py index 853ba4e8de..4f682bd808 100644 --- a/pype/plugins/standalonepublisher/publish/collect_shots.py +++ b/pype/plugins/standalonepublisher/publish/collect_shots.py @@ -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( diff --git a/pype/resources/__init__.py b/pype/resources/__init__.py index 248614ae9d..ba882a84fb 100644 --- a/pype/resources/__init__.py +++ b/pype/resources/__init__.py @@ -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) diff --git a/pype/resources/circle_green.png b/pype/resources/icons/circle_green.png similarity index 100% rename from pype/resources/circle_green.png rename to pype/resources/icons/circle_green.png diff --git a/pype/resources/circle_orange.png b/pype/resources/icons/circle_orange.png similarity index 100% rename from pype/resources/circle_orange.png rename to pype/resources/icons/circle_orange.png diff --git a/pype/resources/circle_red.png b/pype/resources/icons/circle_red.png similarity index 100% rename from pype/resources/circle_red.png rename to pype/resources/icons/circle_red.png diff --git a/pype/resources/icon.png b/pype/resources/icons/pype_icon.png similarity index 100% rename from pype/resources/icon.png rename to pype/resources/icons/pype_icon.png diff --git a/pype/resources/icon_dev.png b/pype/resources/icons/pype_icon_dev.png similarity index 100% rename from pype/resources/icon_dev.png rename to pype/resources/icons/pype_icon_dev.png diff --git a/pype/resources/splash.png b/pype/resources/icons/pype_splash.png similarity index 100% rename from pype/resources/splash.png rename to pype/resources/icons/pype_splash.png diff --git a/pype/resources/splash_dev.png b/pype/resources/icons/pype_splash_dev.png similarity index 100% rename from pype/resources/splash_dev.png rename to pype/resources/icons/pype_splash_dev.png diff --git a/pype/resources/working.svg b/pype/resources/icons/working.svg similarity index 100% rename from pype/resources/working.svg rename to pype/resources/icons/working.svg diff --git a/res/workspace.mel b/pype/resources/maya/workspace.mel similarity index 100% rename from res/workspace.mel rename to pype/resources/maya/workspace.mel diff --git a/pype/tools/pyblish_pype/constants.py b/pype/tools/pyblish_pype/constants.py index 5395d1fd0a..03536fb829 100644 --- a/pype/tools/pyblish_pype/constants.py +++ b/pype/tools/pyblish_pype/constants.py @@ -1,5 +1,7 @@ from Qt import QtCore +EXPANDER_WIDTH = 20 + def flags(*args, **kwargs): type_name = kwargs.pop("type_name", "Flags") diff --git a/pype/tools/pyblish_pype/delegate.py b/pype/tools/pyblish_pype/delegate.py index e88835b81a..cb9123bf3a 100644 --- a/pype/tools/pyblish_pype/delegate.py +++ b/pype/tools/pyblish_pype/delegate.py @@ -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""" diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index 203b512d12..9086003258 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -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) diff --git a/pype/tools/pyblish_pype/view.py b/pype/tools/pyblish_pype/view.py index 450f56421c..477303eae8 100644 --- a/pype/tools/pyblish_pype/view.py +++ b/pype/tools/pyblish_pype/view.py @@ -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): diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 3c7808496c..7d79e0e26c 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -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 ) diff --git a/pype/tools/tray/modules_imports.json b/pype/tools/tray/modules_imports.json new file mode 100644 index 0000000000..e9526dcddb --- /dev/null +++ b/pype/tools/tray/modules_imports.json @@ -0,0 +1,58 @@ +[ + { + "title": "User settings", + "type": "module", + "import_path": "pype.modules.user", + "fromlist": ["pype", "modules"] + }, { + "title": "Ftrack", + "type": "module", + "import_path": "pype.modules.ftrack.tray", + "fromlist": ["pype", "modules", "ftrack"] + }, { + "title": "Muster", + "type": "module", + "import_path": "pype.modules.muster", + "fromlist": ["pype", "modules"] + }, { + "title": "Avalon", + "type": "module", + "import_path": "pype.modules.avalon_apps", + "fromlist": ["pype", "modules"] + }, { + "title": "Clockify", + "type": "module", + "import_path": "pype.modules.clockify", + "fromlist": ["pype", "modules"] + }, { + "title": "Standalone Publish", + "type": "module", + "import_path": "pype.modules.standalonepublish", + "fromlist": ["pype", "modules"] + }, { + "title": "Logging", + "type": "module", + "import_path": "pype.modules.logging.tray", + "fromlist": ["pype", "modules", "logging"] + }, { + "title": "Idle Manager", + "type": "module", + "import_path": "pype.modules.idle_manager", + "fromlist": ["pype","modules"] + }, { + "title": "Timers Manager", + "type": "module", + "import_path": "pype.modules.timers_manager", + "fromlist": ["pype","modules"] + }, { + "title": "Rest Api", + "type": "module", + "import_path": "pype.modules.rest_api", + "fromlist": ["pype","modules"] + }, { + "title": "Adobe Communicator", + "type": "module", + "import_path": "pype.modules.adobe_communicator", + "fromlist": ["pype", "modules"] + } +] diff --git a/pype/tools/tray/pype_tray.py b/pype/tools/tray/pype_tray.py index eec8f61cc4..677c329ad2 100644 --- a/pype/tools/tray/pype_tray.py +++ b/pype/tools/tray/pype_tray.py @@ -3,8 +3,7 @@ import sys import platform from avalon import style from Qt import QtCore, QtGui, QtWidgets, QtSvg -from pype.resources import get_resource -from pype.api import config, Logger +from pype.api import config, Logger, resources class TrayManager: @@ -12,28 +11,40 @@ class TrayManager: Load submenus, actions, separators and modules into tray's context. """ - modules = {} - services = {} - services_submenu = None - - errors = [] - items = ( - config.get_presets(first_run=True) - .get('tray', {}) - .get('menu_items', []) - ) - available_sourcetypes = ['python', 'file'] + available_sourcetypes = ["python", "file"] def __init__(self, tray_widget, main_window): self.tray_widget = tray_widget self.main_window = main_window + self.log = Logger().get_logger(self.__class__.__name__) - self.icon_run = QtGui.QIcon(get_resource('circle_green.png')) - self.icon_stay = QtGui.QIcon(get_resource('circle_orange.png')) - self.icon_failed = QtGui.QIcon(get_resource('circle_red.png')) + self.modules = {} + self.services = {} + self.services_submenu = None - self.services_thread = None + self.errors = [] + + CURRENT_DIR = os.path.dirname(__file__) + self.modules_imports = config.load_json( + os.path.join(CURRENT_DIR, "modules_imports.json") + ) + presets = config.get_presets(first_run=True) + try: + self.modules_usage = presets["tray"]["menu_items"]["item_usage"] + except Exception: + self.modules_usage = {} + self.log.critical("Couldn't find modules usage data.") + + self.icon_run = QtGui.QIcon( + resources.get_resource("icons", "circle_green.png") + ) + self.icon_stay = QtGui.QIcon( + resources.get_resource("icons", "circle_orange.png") + ) + self.icon_failed = QtGui.QIcon( + resources.get_resource("icons", "circle_red.png") + ) def process_presets(self): """Add modules to tray by presets. @@ -46,42 +57,26 @@ class TrayManager: "item_usage": { "Statics Server": false } - }, { - "item_import": [{ - "title": "Ftrack", - "type": "module", - "import_path": "pype.ftrack.tray", - "fromlist": ["pype", "ftrack"] - }, { - "title": "Statics Server", - "type": "module", - "import_path": "pype.services.statics_server", - "fromlist": ["pype","services"] - }] } In this case `Statics Server` won't be used. """ - # Backwards compatible presets loading - if isinstance(self.items, list): - items = self.items - else: - items = [] - # Get booleans is module should be used - usages = self.items.get("item_usage") or {} - for item in self.items.get("item_import", []): - import_path = item.get("import_path") - title = item.get("title") - item_usage = usages.get(title) - if item_usage is None: - item_usage = usages.get(import_path, True) + items = [] + # Get booleans is module should be used + for item in self.modules_imports: + import_path = item.get("import_path") + title = item.get("title") - if item_usage: - items.append(item) - else: - if not title: - title = import_path - self.log.debug("{} - Module ignored".format(title)) + item_usage = self.modules_usage.get(title) + if item_usage is None: + item_usage = self.modules_usage.get(import_path, True) + + if item_usage: + items.append(item) + else: + if not title: + title = import_path + self.log.info("{} - Module ignored".format(title)) if items: self.process_items(items, self.tray_widget.menu) @@ -333,12 +328,7 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): :type parent: QtWidgets.QMainWindow """ def __init__(self, parent): - if os.getenv("PYPE_DEV"): - icon_file_name = "icon_dev.png" - else: - icon_file_name = "icon.png" - - self.icon = QtGui.QIcon(get_resource(icon_file_name)) + self.icon = QtGui.QIcon(resources.pype_icon_filepath()) QtWidgets.QSystemTrayIcon.__init__(self, self.icon, parent) @@ -402,7 +392,7 @@ class TrayMainWindow(QtWidgets.QMainWindow): self.trayIcon.show() def set_working_widget(self): - image_file = get_resource('working.svg') + image_file = resources.get_resource("icons", "working.svg") img_pix = QtGui.QPixmap(image_file) if image_file.endswith('.svg'): widget = QtSvg.QSvgWidget(image_file) @@ -492,11 +482,7 @@ class PypeTrayApplication(QtWidgets.QApplication): splash_widget.hide() def set_splash(self): - if os.getenv("PYPE_DEV"): - splash_file_name = "splash_dev.png" - else: - splash_file_name = "splash.png" - splash_pix = QtGui.QPixmap(get_resource(splash_file_name)) + splash_pix = QtGui.QPixmap(resources.pype_splash_filepath()) splash = QtWidgets.QSplashScreen(splash_pix) splash.setMask(splash_pix.mask()) splash.setEnabled(False) diff --git a/pype/widgets/message_window.py b/pype/widgets/message_window.py index 3532d2df44..41c709b933 100644 --- a/pype/widgets/message_window.py +++ b/pype/widgets/message_window.py @@ -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_())