diff --git a/pype/api.py b/pype/api.py
index 021080b4d5..c1bf84b4ef 100644
--- a/pype/api.py
+++ b/pype/api.py
@@ -1,6 +1,7 @@
from .settings import (
system_settings,
- project_settings
+ project_settings,
+ environments
)
from pypeapp import (
Logger,
@@ -55,6 +56,7 @@ from .lib import _subprocess as subprocess
__all__ = [
"system_settings",
"project_settings",
+ "environments",
"Logger",
"Anatomy",
diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py
index 7310e91e9b..f920e38765 100644
--- a/pype/hosts/harmony/__init__.py
+++ b/pype/hosts/harmony/__init__.py
@@ -1,5 +1,6 @@
import os
import sys
+from uuid import uuid4
from avalon import api, io, harmony
from avalon.vendor import Qt
@@ -8,8 +9,11 @@ import pyblish.api
from pype import lib
+signature = str(uuid4())
+
+
def set_scene_settings(settings):
- func = """function func(args)
+ func = """function %s_func(args)
{
if (args[0]["fps"])
{
@@ -36,8 +40,8 @@ def set_scene_settings(settings):
)
}
}
- func
- """
+ %s_func
+ """ % (signature, signature)
harmony.send({"function": func, "args": [settings]})
@@ -107,15 +111,15 @@ def check_inventory():
outdated_containers.append(container)
# Colour nodes.
- func = """function func(args){
+ func = """function %s_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
- """
+ %s_func
+ """ % (signature, signature)
outdated_nodes = []
for container in outdated_containers:
if container["loader"] == "ImageSequenceLoader":
@@ -144,7 +148,7 @@ def application_launch():
def export_template(backdrops, nodes, filepath):
- func = """function func(args)
+ func = """function %s_func(args)
{
var temp_node = node.add("Top", "temp_note", "NOTE", 0, 0, 0);
@@ -179,8 +183,8 @@ def export_template(backdrops, nodes, filepath):
Action.perform("onActionUpToParent()", "Node View");
node.deleteNode(template_group, true, true);
}
- func
- """
+ %s_func
+ """ % (signature, signature)
harmony.send({
"function": func,
"args": [
@@ -221,12 +225,15 @@ def install():
def on_pyblish_instance_toggled(instance, old_value, new_value):
"""Toggle node enabling on instance toggles."""
- func = """function func(args)
+ func = """function %s_func(args)
{
node.setEnable(args[0], args[1])
}
- func
- """
- harmony.send(
- {"function": func, "args": [instance[0], new_value]}
- )
+ %s_func
+ """ % (signature, signature)
+ try:
+ harmony.send(
+ {"function": func, "args": [instance[0], new_value]}
+ )
+ except IndexError:
+ print(f"Instance '{instance}' is missing node")
diff --git a/pype/modules/__init__.py b/pype/modules/__init__.py
index e69de29bb2..aacd541e18 100644
--- a/pype/modules/__init__.py
+++ b/pype/modules/__init__.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+from .base import PypeModule
+
+__all__ = (
+ "PypeModule",
+)
diff --git a/pype/modules/avalon_apps/avalon_app.py b/pype/modules/avalon_apps/avalon_app.py
index 7ed651f82b..de10268304 100644
--- a/pype/modules/avalon_apps/avalon_app.py
+++ b/pype/modules/avalon_apps/avalon_app.py
@@ -1,16 +1,27 @@
-from Qt import QtWidgets
-from avalon.tools import libraryloader
from pype.api import Logger
-from pype.tools.launcher import LauncherWindow, actions
class AvalonApps:
def __init__(self, main_parent=None, parent=None):
self.log = Logger().get_logger(__name__)
- self.main_parent = main_parent
+
+ self.tray_init(main_parent, parent)
+
+ def tray_init(self, main_parent, parent):
+ from avalon.tools.libraryloader import app
+ from avalon import style
+ from pype.tools.launcher import LauncherWindow, actions
+
self.parent = parent
+ self.main_parent = main_parent
self.app_launcher = LauncherWindow()
+ self.libraryloader = app.Window(
+ icon=self.parent.icon,
+ show_projects=True,
+ show_libraries=True
+ )
+ self.libraryloader.setStyleSheet(style.load_stylesheet())
# actions.register_default_actions()
actions.register_config_actions()
@@ -23,6 +34,7 @@ class AvalonApps:
# Definition of Tray menu
def tray_menu(self, parent_menu=None):
+ from Qt import QtWidgets
# Actions
if parent_menu is None:
if self.parent is None:
@@ -52,9 +64,11 @@ class AvalonApps:
self.app_launcher.activateWindow()
def show_library_loader(self):
- libraryloader.show(
- parent=self.main_parent,
- icon=self.parent.icon,
- show_projects=True,
- show_libraries=True
- )
+ self.libraryloader.show()
+
+ # Raise and activate the window
+ # for MacOS
+ self.libraryloader.raise_()
+ # for Windows
+ self.libraryloader.activateWindow()
+ self.libraryloader.refresh()
diff --git a/pype/modules/base.py b/pype/modules/base.py
new file mode 100644
index 0000000000..ee90aa4cbb
--- /dev/null
+++ b/pype/modules/base.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+"""Base class for Pype Modules."""
+from uuid import uuid4
+from abc import ABC, abstractmethod
+from pype.api import Logger
+
+
+class PypeModule(ABC):
+ """Base class of pype module.
+
+ Attributes:
+ id (UUID): Module id.
+ enabled (bool): Is module enabled.
+ name (str): Module name.
+ """
+
+ enabled = False
+ name = None
+ _id = None
+
+ def __init__(self, settings):
+ if self.name is None:
+ self.name = self.__class__.__name__
+
+ self.log = Logger().get_logger(self.name)
+
+ self.settings = settings.get(self.name)
+ self.enabled = settings.get("enabled", False)
+ self._id = uuid4()
+
+ @property
+ def id(self):
+ return self._id
+
+ @abstractmethod
+ def startup_environments(self):
+ """Get startup environments for module."""
+ return {}
diff --git a/pype/modules/clockify/clockify.py b/pype/modules/clockify/clockify.py
index fea15a1bea..4309bff9f2 100644
--- a/pype/modules/clockify/clockify.py
+++ b/pype/modules/clockify/clockify.py
@@ -1,9 +1,8 @@
import os
import threading
+import time
+
from pype.api import Logger
-from avalon import style
-from Qt import QtWidgets
-from .widgets import ClockifySettings, MessageWidget
from .clockify_api import ClockifyAPI
from .constants import CLOCKIFY_FTRACK_USER_PATH
@@ -17,11 +16,21 @@ class ClockifyModule:
os.environ["CLOCKIFY_WORKSPACE"] = self.workspace_name
+ self.timer_manager = None
+ self.MessageWidgetClass = None
+
+ self.clockapi = ClockifyAPI(master_parent=self)
+
self.log = Logger().get_logger(self.__class__.__name__, "PypeTray")
+ self.tray_init(main_parent, parent)
+
+ def tray_init(self, main_parent, parent):
+ from .widgets import ClockifySettings, MessageWidget
+
+ self.MessageWidgetClass = MessageWidget
self.main_parent = main_parent
self.parent = parent
- self.clockapi = ClockifyAPI(master_parent=self)
self.message_widget = None
self.widget_settings = ClockifySettings(main_parent, self)
self.widget_settings_required = None
@@ -57,11 +66,10 @@ class ClockifyModule:
)
if 'AvalonApps' in modules:
- from launcher import lib
- actions_path = os.path.sep.join([
+ actions_path = os.path.join(
os.path.dirname(__file__),
'launcher_actions'
- ])
+ )
current = os.environ.get('AVALON_ACTIONS', '')
if current:
current += os.pathsep
@@ -78,12 +86,12 @@ class ClockifyModule:
self.stop_timer()
def timer_started(self, data):
- if hasattr(self, 'timer_manager'):
+ if self.timer_manager:
self.timer_manager.start_timers(data)
def timer_stopped(self):
self.bool_timer_run = False
- if hasattr(self, 'timer_manager'):
+ if self.timer_manager:
self.timer_manager.stop_timers()
def start_timer_check(self):
@@ -102,7 +110,7 @@ class ClockifyModule:
self.thread_timer_check = None
def check_running(self):
- import time
+
while self.bool_thread_check_running is True:
bool_timer_run = False
if self.clockapi.get_in_progress() is not None:
@@ -156,15 +164,14 @@ class ClockifyModule:
self.timer_stopped()
def signed_in(self):
- if hasattr(self, 'timer_manager'):
- if not self.timer_manager:
- return
+ if not self.timer_manager:
+ return
- if not self.timer_manager.last_task:
- return
+ if not self.timer_manager.last_task:
+ return
- if self.timer_manager.is_running:
- self.start_timer_manager(self.timer_manager.last_task)
+ if self.timer_manager.is_running:
+ self.start_timer_manager(self.timer_manager.last_task)
def start_timer(self, input_data):
# If not api key is not entered then skip
@@ -197,11 +204,14 @@ class ClockifyModule:
"
Please inform your Project Manager."
).format(project_name, str(self.clockapi.workspace_name))
- self.message_widget = MessageWidget(
- self.main_parent, msg, "Clockify - Info Message"
- )
- self.message_widget.closed.connect(self.on_message_widget_close)
- self.message_widget.show()
+ if self.MessageWidgetClass:
+ self.message_widget = self.MessageWidgetClass(
+ self.main_parent, msg, "Clockify - Info Message"
+ )
+ self.message_widget.closed.connect(
+ self.on_message_widget_close
+ )
+ self.message_widget.show()
return
@@ -227,31 +237,29 @@ class ClockifyModule:
# Definition of Tray menu
def tray_menu(self, parent_menu):
# Menu for Tray App
- self.menu = QtWidgets.QMenu('Clockify', parent_menu)
- self.menu.setProperty('submenu', 'on')
- self.menu.setStyleSheet(style.load_stylesheet())
+ from Qt import QtWidgets
+ menu = QtWidgets.QMenu("Clockify", parent_menu)
+ menu.setProperty("submenu", "on")
# Actions
- self.aShowSettings = QtWidgets.QAction(
- "Settings", self.menu
- )
- self.aStopTimer = QtWidgets.QAction(
- "Stop timer", self.menu
- )
+ action_show_settings = QtWidgets.QAction("Settings", menu)
+ action_stop_timer = QtWidgets.QAction("Stop timer", menu)
- self.menu.addAction(self.aShowSettings)
- self.menu.addAction(self.aStopTimer)
+ menu.addAction(action_show_settings)
+ menu.addAction(action_stop_timer)
- self.aShowSettings.triggered.connect(self.show_settings)
- self.aStopTimer.triggered.connect(self.stop_timer)
+ action_show_settings.triggered.connect(self.show_settings)
+ action_stop_timer.triggered.connect(self.stop_timer)
+
+ self.action_stop_timer = action_stop_timer
self.set_menu_visibility()
- parent_menu.addMenu(self.menu)
+ parent_menu.addMenu(menu)
def show_settings(self):
self.widget_settings.input_api_key.setText(self.clockapi.get_api_key())
self.widget_settings.show()
def set_menu_visibility(self):
- self.aStopTimer.setVisible(self.bool_timer_run)
+ self.action_stop_timer.setVisible(self.bool_timer_run)
diff --git a/pype/modules/ftrack/__init__.py b/pype/modules/ftrack/__init__.py
index aa8f04bffb..fad771f084 100644
--- a/pype/modules/ftrack/__init__.py
+++ b/pype/modules/ftrack/__init__.py
@@ -1,2 +1,12 @@
-from .lib import *
+from . import ftrack_server
from .ftrack_server import FtrackServer, check_ftrack_url
+from .lib import BaseHandler, BaseEvent, BaseAction
+
+__all__ = (
+ "ftrack_server",
+ "FtrackServer",
+ "check_ftrack_url",
+ "BaseHandler",
+ "BaseEvent",
+ "BaseAction"
+)
diff --git a/pype/modules/ftrack/ftrack_server/__init__.py b/pype/modules/ftrack/ftrack_server/__init__.py
index fcae4e0690..9e3920b500 100644
--- a/pype/modules/ftrack/ftrack_server/__init__.py
+++ b/pype/modules/ftrack/ftrack_server/__init__.py
@@ -1,2 +1,8 @@
from .ftrack_server import FtrackServer
from .lib import check_ftrack_url
+
+
+__all__ = (
+ "FtrackServer",
+ "check_ftrack_url"
+)
diff --git a/pype/modules/ftrack/lib/custom_db_connector.py b/pype/modules/ftrack/ftrack_server/custom_db_connector.py
similarity index 71%
rename from pype/modules/ftrack/lib/custom_db_connector.py
rename to pype/modules/ftrack/ftrack_server/custom_db_connector.py
index d498d041dc..8a8ba4ccbb 100644
--- a/pype/modules/ftrack/lib/custom_db_connector.py
+++ b/pype/modules/ftrack/ftrack_server/custom_db_connector.py
@@ -16,9 +16,9 @@ import pymongo
from pype.api import decompose_url
-class NotActiveTable(Exception):
+class NotActiveCollection(Exception):
def __init__(self, *args, **kwargs):
- msg = "Active table is not set. (This is bug)"
+ msg = "Active collection is not set. (This is bug)"
if not (args or kwargs):
args = [msg]
super().__init__(*args, **kwargs)
@@ -40,12 +40,12 @@ def auto_reconnect(func):
return decorated
-def check_active_table(func):
+def check_active_collection(func):
"""Check if CustomDbConnector has active collection."""
@functools.wraps(func)
def decorated(obj, *args, **kwargs):
- if not obj.active_table:
- raise NotActiveTable()
+ if not obj.active_collection:
+ raise NotActiveCollection()
return func(obj, *args, **kwargs)
return decorated
@@ -55,7 +55,7 @@ class CustomDbConnector:
timeout = int(os.environ["AVALON_TIMEOUT"])
def __init__(
- self, uri, database_name, port=None, table_name=None
+ self, uri, database_name, port=None, collection_name=None
):
self._mongo_client = None
self._sentry_client = None
@@ -76,10 +76,10 @@ class CustomDbConnector:
self._port = port
self._database_name = database_name
- self.active_table = table_name
+ self.active_collection = collection_name
def __getitem__(self, key):
- # gives direct access to collection withou setting `active_table`
+ # gives direct access to collection withou setting `active_collection`
return self._database[key]
def __getattribute__(self, attr):
@@ -88,9 +88,11 @@ class CustomDbConnector:
try:
return super(CustomDbConnector, self).__getattribute__(attr)
except AttributeError:
- if self.active_table is None:
- raise NotActiveTable()
- return self._database[self.active_table].__getattribute__(attr)
+ if self.active_collection is None:
+ raise NotActiveCollection()
+ return self._database[self.active_collection].__getattribute__(
+ attr
+ )
def install(self):
"""Establish a persistent connection to the database"""
@@ -146,46 +148,30 @@ class CustomDbConnector:
self._is_installed = False
atexit.unregister(self.uninstall)
- def create_table(self, name, **options):
- if self.exist_table(name):
+ def collection_exists(self, collection_name):
+ return collection_name in self.collections()
+
+ def create_collection(self, name, **options):
+ if self.collection_exists(name):
return
return self._database.create_collection(name, **options)
- def exist_table(self, table_name):
- return table_name in self.tables()
-
- def create_table(self, name, **options):
- if self.exist_table(name):
- return
-
- return self._database.create_collection(name, **options)
-
- def exist_table(self, table_name):
- return table_name in self.tables()
-
- def tables(self):
- """List available tables
- Returns:
- list of table names
- """
- collection_names = self.collections()
- for table_name in collection_names:
- if table_name in ("system.indexes",):
- continue
- yield table_name
-
@auto_reconnect
def collections(self):
- return self._database.collection_names()
+ for col_name in self._database.collection_names():
+ if col_name not in ("system.indexes",):
+ yield col_name
- @check_active_table
+ @check_active_collection
@auto_reconnect
def insert_one(self, item, **options):
assert isinstance(item, dict), "item must be of type "
- return self._database[self.active_table].insert_one(item, **options)
+ return self._database[self.active_collection].insert_one(
+ item, **options
+ )
- @check_active_table
+ @check_active_collection
@auto_reconnect
def insert_many(self, items, ordered=True, **options):
# check if all items are valid
@@ -194,72 +180,74 @@ class CustomDbConnector:
assert isinstance(item, dict), "`item` must be of type "
options["ordered"] = ordered
- return self._database[self.active_table].insert_many(items, **options)
+ return self._database[self.active_collection].insert_many(
+ items, **options
+ )
- @check_active_table
+ @check_active_collection
@auto_reconnect
def find(self, filter, projection=None, sort=None, **options):
options["sort"] = sort
- return self._database[self.active_table].find(
+ return self._database[self.active_collection].find(
filter, projection, **options
)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def find_one(self, filter, projection=None, sort=None, **options):
assert isinstance(filter, dict), "filter must be "
options["sort"] = sort
- return self._database[self.active_table].find_one(
+ return self._database[self.active_collection].find_one(
filter,
projection,
**options
)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def replace_one(self, filter, replacement, **options):
- return self._database[self.active_table].replace_one(
+ return self._database[self.active_collection].replace_one(
filter, replacement, **options
)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def update_one(self, filter, update, **options):
- return self._database[self.active_table].update_one(
+ return self._database[self.active_collection].update_one(
filter, update, **options
)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def update_many(self, filter, update, **options):
- return self._database[self.active_table].update_many(
+ return self._database[self.active_collection].update_many(
filter, update, **options
)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def distinct(self, **options):
- return self._database[self.active_table].distinct(**options)
+ return self._database[self.active_collection].distinct(**options)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def drop_collection(self, name_or_collection, **options):
- return self._database[self.active_table].drop(
+ return self._database[self.active_collection].drop(
name_or_collection, **options
)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def delete_one(self, filter, collation=None, **options):
options["collation"] = collation
- return self._database[self.active_table].delete_one(
+ return self._database[self.active_collection].delete_one(
filter, **options
)
- @check_active_table
+ @check_active_collection
@auto_reconnect
def delete_many(self, filter, collation=None, **options):
options["collation"] = collation
- return self._database[self.active_table].delete_many(
+ return self._database[self.active_collection].delete_many(
filter, **options
)
diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py
index ee6b1216dc..79b708b17a 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 CustomDbConnector
+from .custom_db_connector import CustomDbConnector
TOPIC_STATUS_SERVER = "pype.event.server.status"
@@ -153,9 +153,9 @@ class StorerEventHub(SocketBaseEventHub):
class ProcessEventHub(SocketBaseEventHub):
hearbeat_msg = b"processor"
- uri, port, database, table_name = get_ftrack_event_mongo_info()
+ uri, port, database, collection_name = get_ftrack_event_mongo_info()
- is_table_created = False
+ is_collection_created = False
pypelog = Logger().get_logger("Session Processor")
def __init__(self, *args, **kwargs):
@@ -163,7 +163,7 @@ class ProcessEventHub(SocketBaseEventHub):
self.uri,
self.database,
self.port,
- self.table_name
+ self.collection_name
)
super(ProcessEventHub, self).__init__(*args, **kwargs)
@@ -184,7 +184,7 @@ class ProcessEventHub(SocketBaseEventHub):
"Error with Mongo access, probably permissions."
"Check if exist database with name \"{}\""
" and collection \"{}\" inside."
- ).format(self.database, self.table_name))
+ ).format(self.database, self.collection_name))
self.sock.sendall(b"MongoError")
sys.exit(0)
diff --git a/pype/modules/ftrack/ftrack_server/sub_event_storer.py b/pype/modules/ftrack/ftrack_server/sub_event_storer.py
index 1635f6cea3..2f4395c8db 100644
--- a/pype/modules/ftrack/ftrack_server/sub_event_storer.py
+++ b/pype/modules/ftrack/ftrack_server/sub_event_storer.py
@@ -12,7 +12,9 @@ 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 CustomDbConnector
+from pype.modules.ftrack.ftrack_server.custom_db_connector import (
+ CustomDbConnector
+)
from pype.api import Logger
log = Logger().get_logger("Event storer")
@@ -23,8 +25,8 @@ class SessionFactory:
session = None
-uri, port, database, table_name = get_ftrack_event_mongo_info()
-dbcon = CustomDbConnector(uri, database, port, table_name)
+uri, port, database, collection_name = get_ftrack_event_mongo_info()
+dbcon = CustomDbConnector(uri, database, port, collection_name)
# ignore_topics = ["ftrack.meta.connected"]
ignore_topics = []
@@ -200,7 +202,7 @@ def main(args):
"Error with Mongo access, probably permissions."
"Check if exist database with name \"{}\""
" and collection \"{}\" inside."
- ).format(database, table_name))
+ ).format(database, collection_name))
sock.sendall(b"MongoError")
finally:
diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py
index 03124ab10d..292ce752cf 100644
--- a/pype/modules/ftrack/lib/avalon_sync.py
+++ b/pype/modules/ftrack/lib/avalon_sync.py
@@ -1022,7 +1022,7 @@ class SyncEntitiesFactory:
continue
ent_path_items = [ent["name"] for ent in entity["link"]]
- parents = ent_path_items[1:len(ent_path_items)-1:]
+ parents = ent_path_items[1:len(ent_path_items) - 1:]
hierarchy = ""
if len(parents) > 0:
hierarchy = os.path.sep.join(parents)
@@ -1141,7 +1141,7 @@ class SyncEntitiesFactory:
if not is_right and not else_match_better:
entity = entity_dict["entity"]
ent_path_items = [ent["name"] for ent in entity["link"]]
- parents = ent_path_items[1:len(ent_path_items)-1:]
+ parents = ent_path_items[1:len(ent_path_items) - 1:]
av_parents = av_ent_by_mongo_id["data"]["parents"]
if av_parents == parents:
is_right = True
diff --git a/pype/modules/ftrack/lib/ftrack_base_handler.py b/pype/modules/ftrack/lib/ftrack_base_handler.py
index ce6607d6bf..d322fbaf23 100644
--- a/pype/modules/ftrack/lib/ftrack_base_handler.py
+++ b/pype/modules/ftrack/lib/ftrack_base_handler.py
@@ -2,7 +2,7 @@ import functools
import time
from pype.api import Logger
import ftrack_api
-from pype.modules.ftrack.ftrack_server.lib import SocketSession
+from pype.modules.ftrack import ftrack_server
class MissingPermision(Exception):
@@ -41,7 +41,7 @@ class BaseHandler(object):
self.log = Logger().get_logger(self.__class__.__name__)
if not(
isinstance(session, ftrack_api.session.Session) or
- isinstance(session, SocketSession)
+ isinstance(session, ftrack_server.lib.SocketSession)
):
raise Exception((
"Session object entered with args is instance of \"{}\""
@@ -49,7 +49,7 @@ class BaseHandler(object):
).format(
str(type(session)),
str(ftrack_api.session.Session),
- str(SocketSession)
+ str(ftrack_server.lib.SocketSession)
))
self._session = session
diff --git a/pype/modules/ftrack/tray/login_dialog.py b/pype/modules/ftrack/tray/login_dialog.py
index 7730ee1609..94ad29e478 100644
--- a/pype/modules/ftrack/tray/login_dialog.py
+++ b/pype/modules/ftrack/tray/login_dialog.py
@@ -1,7 +1,7 @@
import os
import requests
from avalon import style
-from pype.modules.ftrack import credentials
+from pype.modules.ftrack.lib import credentials
from . import login_tools
from pype.api import resources
from Qt import QtCore, QtGui, QtWidgets
@@ -238,6 +238,8 @@ class CredentialsDialog(QtWidgets.QDialog):
# If there is an existing server thread running we need to stop it.
if self._login_server_thread:
+ if self._login_server_thread.isAlive():
+ self._login_server_thread.stop()
self._login_server_thread.join()
self._login_server_thread = None
diff --git a/pype/modules/ftrack/tray/login_tools.py b/pype/modules/ftrack/tray/login_tools.py
index e7d22fbc19..d3297eaa76 100644
--- a/pype/modules/ftrack/tray/login_tools.py
+++ b/pype/modules/ftrack/tray/login_tools.py
@@ -61,12 +61,17 @@ class LoginServerThread(threading.Thread):
def __init__(self, url, callback):
self.url = url
self.callback = callback
+ self._server = None
super(LoginServerThread, self).__init__()
def _handle_login(self, api_user, api_key):
'''Login to server with *api_user* and *api_key*.'''
self.callback(api_user, api_key)
+ def stop(self):
+ if self._server:
+ self._server.server_close()
+
def run(self):
'''Listen for events.'''
self._server = HTTPServer(
diff --git a/pype/modules/logging/gui/__init__.py b/pype/modules/logging/tray/gui/__init__.py
similarity index 100%
rename from pype/modules/logging/gui/__init__.py
rename to pype/modules/logging/tray/gui/__init__.py
diff --git a/pype/modules/logging/gui/app.py b/pype/modules/logging/tray/gui/app.py
similarity index 100%
rename from pype/modules/logging/gui/app.py
rename to pype/modules/logging/tray/gui/app.py
diff --git a/pype/modules/logging/gui/models.py b/pype/modules/logging/tray/gui/models.py
similarity index 100%
rename from pype/modules/logging/gui/models.py
rename to pype/modules/logging/tray/gui/models.py
diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/tray/gui/widgets.py
similarity index 100%
rename from pype/modules/logging/gui/widgets.py
rename to pype/modules/logging/tray/gui/widgets.py
diff --git a/pype/modules/logging/tray/logging_module.py b/pype/modules/logging/tray/logging_module.py
index 9b26d5d9bf..84b40f68e1 100644
--- a/pype/modules/logging/tray/logging_module.py
+++ b/pype/modules/logging/tray/logging_module.py
@@ -1,6 +1,4 @@
-from Qt import QtWidgets
from pype.api import Logger
-from ..gui.app import LogsWindow
class LoggingModule:
@@ -8,7 +6,13 @@ class LoggingModule:
self.parent = parent
self.log = Logger().get_logger(self.__class__.__name__, "logging")
+ self.window = None
+
+ self.tray_init(main_parent, parent)
+
+ def tray_init(self, main_parent, parent):
try:
+ from .gui.app import LogsWindow
self.window = LogsWindow()
self.tray_menu = self._tray_menu
except Exception:
@@ -18,12 +22,12 @@ class LoggingModule:
# Definition of Tray menu
def _tray_menu(self, parent_menu):
+ from Qt import QtWidgets
# Menu for Tray App
menu = QtWidgets.QMenu('Logging', parent_menu)
- # menu.setProperty('submenu', 'on')
show_action = QtWidgets.QAction("Show Logs", menu)
- show_action.triggered.connect(self.on_show_logs)
+ show_action.triggered.connect(self._show_logs_gui)
menu.addAction(show_action)
parent_menu.addMenu(menu)
@@ -34,5 +38,6 @@ class LoggingModule:
def process_modules(self, modules):
return
- def on_show_logs(self):
- self.window.show()
+ def _show_logs_gui(self):
+ if self.window:
+ self.window.show()
diff --git a/pype/modules/muster/muster.py b/pype/modules/muster/muster.py
index 629fb12635..beb30690ac 100644
--- a/pype/modules/muster/muster.py
+++ b/pype/modules/muster/muster.py
@@ -1,10 +1,7 @@
-import appdirs
-from avalon import style
-from Qt import QtWidgets
import os
import json
-from .widget_login import MusterLogin
-from avalon.vendor import requests
+import appdirs
+import requests
class MusterModule:
@@ -21,6 +18,11 @@ class MusterModule:
self.cred_path = os.path.join(
self.cred_folder_path, self.cred_filename
)
+ self.tray_init(main_parent, parent)
+
+ def tray_init(self, main_parent, parent):
+ from .widget_login import MusterLogin
+
self.main_parent = main_parent
self.parent = parent
self.widget_login = MusterLogin(main_parent, self)
@@ -38,10 +40,6 @@ class MusterModule:
pass
def process_modules(self, modules):
-
- def api_callback():
- self.aShowLogin.trigger()
-
if "RestApiServer" in modules:
def api_show_login():
self.aShowLogin.trigger()
@@ -51,13 +49,12 @@ class MusterModule:
# Definition of Tray menu
def tray_menu(self, parent):
- """
- Add **change credentials** option to tray menu.
- """
+ """Add **change credentials** option to tray menu."""
+ from Qt import QtWidgets
+
# Menu for Tray App
self.menu = QtWidgets.QMenu('Muster', parent)
self.menu.setProperty('submenu', 'on')
- self.menu.setStyleSheet(style.load_stylesheet())
# Actions
self.aShowLogin = QtWidgets.QAction(
@@ -91,9 +88,9 @@ class MusterModule:
if not MUSTER_REST_URL:
raise AttributeError("Muster REST API url not set")
params = {
- 'username': username,
- 'password': password
- }
+ 'username': username,
+ 'password': password
+ }
api_entry = '/api/login'
response = self._requests_post(
MUSTER_REST_URL + api_entry, params=params)
diff --git a/pype/modules/rest_api/rest_api.py b/pype/modules/rest_api/rest_api.py
index cc98b56a3b..3e0c646560 100644
--- a/pype/modules/rest_api/rest_api.py
+++ b/pype/modules/rest_api/rest_api.py
@@ -1,6 +1,6 @@
import os
import socket
-from Qt import QtCore
+import threading
from socketserver import ThreadingMixIn
from http.server import HTTPServer
@@ -155,14 +155,15 @@ class RestApiServer:
def is_running(self):
return self.rest_api_thread.is_running
+ def tray_exit(self):
+ self.stop()
+
def stop(self):
- self.rest_api_thread.is_running = False
-
- def thread_stopped(self):
- self._is_running = False
+ self.rest_api_thread.stop()
+ self.rest_api_thread.join()
-class RestApiThread(QtCore.QThread):
+class RestApiThread(threading.Thread):
""" Listener for REST requests.
It is possible to register callbacks for url paths.
@@ -174,6 +175,12 @@ class RestApiThread(QtCore.QThread):
self.is_running = False
self.module = module
self.port = port
+ self.httpd = None
+
+ def stop(self):
+ self.is_running = False
+ if self.httpd:
+ self.httpd.server_close()
def run(self):
self.is_running = True
@@ -185,12 +192,14 @@ class RestApiThread(QtCore.QThread):
)
with ThreadingSimpleServer(("", self.port), Handler) as httpd:
+ self.httpd = httpd
while self.is_running:
httpd.handle_request()
+
except Exception:
log.warning(
"Rest Api Server service has failed", exc_info=True
)
+ self.httpd = None
self.is_running = False
- self.module.thread_stopped()
diff --git a/pype/modules/standalonepublish/standalonepublish_module.py b/pype/modules/standalonepublish/standalonepublish_module.py
index ed997bfd9f..f8bc0c6f24 100644
--- a/pype/modules/standalonepublish/standalonepublish_module.py
+++ b/pype/modules/standalonepublish/standalonepublish_module.py
@@ -2,7 +2,6 @@ import os
import sys
import subprocess
import pype
-from pype import tools
class StandAlonePublishModule:
@@ -30,6 +29,7 @@ class StandAlonePublishModule:
))
def show(self):
+ from pype import tools
standalone_publisher_tool_path = os.path.join(
os.path.dirname(tools.__file__),
"standalonepublish"
diff --git a/pype/modules/timers_manager/__init__.py b/pype/modules/timers_manager/__init__.py
index a8a478d7ae..9de205f088 100644
--- a/pype/modules/timers_manager/__init__.py
+++ b/pype/modules/timers_manager/__init__.py
@@ -1,5 +1,4 @@
from .timers_manager import TimersManager
-from .widget_user_idle import WidgetUserIdle
CLASS_DEFINIION = TimersManager
diff --git a/pype/modules/timers_manager/timers_manager.py b/pype/modules/timers_manager/timers_manager.py
index 82ba1013f0..62767c24f1 100644
--- a/pype/modules/timers_manager/timers_manager.py
+++ b/pype/modules/timers_manager/timers_manager.py
@@ -1,21 +1,7 @@
-from .widget_user_idle import WidgetUserIdle, SignalHandler
-from pype.api import Logger, config
+from pype.api import Logger
-class Singleton(type):
- """ Signleton implementation
- """
- _instances = {}
-
- def __call__(cls, *args, **kwargs):
- if cls not in cls._instances:
- cls._instances[cls] = super(
- Singleton, cls
- ).__call__(*args, **kwargs)
- return cls._instances[cls]
-
-
-class TimersManager(metaclass=Singleton):
+class TimersManager:
""" Handles about Timers.
Should be able to start/stop all timers at once.
@@ -41,7 +27,13 @@ class TimersManager(metaclass=Singleton):
self.idle_man = None
self.signal_handler = None
+
+ self.trat_init(tray_widget, main_widget)
+
+ def trat_init(self, tray_widget, main_widget):
+ from .widget_user_idle import WidgetUserIdle, SignalHandler
self.widget_user_idle = WidgetUserIdle(self, tray_widget)
+ self.signal_handler = SignalHandler(self)
def set_signal_times(self):
try:
@@ -119,7 +111,6 @@ class TimersManager(metaclass=Singleton):
"""
if 'IdleManager' in modules:
- self.signal_handler = SignalHandler(self)
if self.set_signal_times() is True:
self.register_to_idle_manager(modules['IdleManager'])
diff --git a/pype/modules/user/user_module.py b/pype/modules/user/user_module.py
index f2de9dc2fb..dc57fe4a63 100644
--- a/pype/modules/user/user_module.py
+++ b/pype/modules/user/user_module.py
@@ -3,8 +3,6 @@ import json
import getpass
import appdirs
-from Qt import QtWidgets
-from .widget_user import UserWidget
from pype.api import Logger
@@ -24,6 +22,12 @@ class UserModule:
self.cred_path = os.path.normpath(os.path.join(
self.cred_folder_path, self.cred_filename
))
+ self.widget_login = None
+
+ self.tray_init(main_parent, parent)
+
+ def tray_init(self, main_parent=None, parent=None):
+ from .widget_user import UserWidget
self.widget_login = UserWidget(self)
self.load_credentials()
@@ -66,6 +70,7 @@ class UserModule:
# Definition of Tray menu
def tray_menu(self, parent_menu):
+ from Qt import QtWidgets
"""Add menu or action to Tray(or parent)'s menu"""
action = QtWidgets.QAction("Username", parent_menu)
action.triggered.connect(self.show_widget)
@@ -121,7 +126,8 @@ class UserModule:
self.cred = {"username": username}
os.environ[self.env_name] = username
- self.widget_login.set_user(username)
+ if self.widget_login:
+ self.widget_login.set_user(username)
try:
file = open(self.cred_path, "w")
file.write(json.dumps(self.cred))
diff --git a/pype/modules/websocket_server/websocket_server.py b/pype/modules/websocket_server/websocket_server.py
index 1152c65e00..daf4b03103 100644
--- a/pype/modules/websocket_server/websocket_server.py
+++ b/pype/modules/websocket_server/websocket_server.py
@@ -31,12 +31,13 @@ class WebSocketServer():
self.client = None
self.handlers = {}
+ port = None
websocket_url = os.getenv("WEBSOCKET_URL")
if websocket_url:
parsed = urllib.parse.urlparse(websocket_url)
port = parsed.port
if not port:
- port = 8099 # fallback
+ port = 8098 # fallback
self.app = web.Application()
diff --git a/pype/modules_manager.py b/pype/modules_manager.py
new file mode 100644
index 0000000000..6538187ea9
--- /dev/null
+++ b/pype/modules_manager.py
@@ -0,0 +1,102 @@
+import os
+import inspect
+
+import pype.modules
+from pype.modules import PypeModule
+from pype.settings import system_settings
+from pype.api import Logger
+
+
+class PypeModuleManager:
+ skip_module_names = ("__pycache__", )
+
+ def __init__(self):
+ self.log = Logger().get_logger(
+ "{}.{}".format(__name__, self.__class__.__name__)
+ )
+
+ self.pype_modules = self.find_pype_modules()
+
+ def modules_environments(self):
+ environments = {}
+ for pype_module in self.pype_modules.values():
+ environments.update(pype_module.startup_environments())
+ return environments
+
+ def find_pype_modules(self):
+ settings = system_settings()
+ modules = []
+ dirpath = os.path.dirname(pype.modules.__file__)
+ for module_name in os.listdir(dirpath):
+ # Check if path lead to a folder
+ full_path = os.path.join(dirpath, module_name)
+ if not os.path.isdir(full_path):
+ continue
+
+ # Skip known invalid names
+ if module_name in self.skip_module_names:
+ continue
+
+ import_name = "pype.modules.{}".format(module_name)
+ try:
+ modules.append(
+ __import__(import_name, fromlist=[""])
+ )
+
+ except Exception:
+ self.log.warning(
+ "Couldn't import {}".format(import_name), exc_info=True
+ )
+
+ pype_module_classes = []
+ for module in modules:
+ try:
+ pype_module_classes.extend(
+ self._classes_from_module(PypeModule, module)
+ )
+ except Exception:
+ self.log.warning(
+ "Couldn't import {}".format(import_name), exc_info=True
+ )
+
+ pype_modules = {}
+ for pype_module_class in pype_module_classes:
+ try:
+ pype_module = pype_module_class(settings)
+ if pype_module.enabled:
+ pype_modules[pype_module.id] = pype_module
+ except Exception:
+ self.log.warning(
+ "Couldn't create instance of {}".format(
+ pype_module_class.__class__.__name__
+ ),
+ exc_info=True
+ )
+ return pype_modules
+
+ def _classes_from_module(self, superclass, module):
+ classes = list()
+
+ def recursive_bases(klass):
+ output = []
+ output.extend(klass.__bases__)
+ for base in klass.__bases__:
+ output.extend(recursive_bases(base))
+ return output
+
+ for name in dir(module):
+ # It could be anything at this point
+ obj = getattr(module, name)
+
+ if not inspect.isclass(obj) or not len(obj.__bases__) > 0:
+ continue
+
+ # Use string comparison rather than `issubclass`
+ # in order to support reloading of this module.
+ bases = recursive_bases(obj)
+ if not any(base.__name__ == superclass.__name__ for base in bases):
+ continue
+
+ classes.append(obj)
+
+ return classes
diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_api.py b/pype/plugins/ftrack/publish/integrate_ftrack_api.py
index 0c4c6d49b5..2c8e06a099 100644
--- a/pype/plugins/ftrack/publish/integrate_ftrack_api.py
+++ b/pype/plugins/ftrack/publish/integrate_ftrack_api.py
@@ -97,6 +97,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
except Exception:
tp, value, tb = sys.exc_info()
session.rollback()
+ session._configure_locations()
six.reraise(tp, value, tb)
def process(self, instance):
@@ -178,6 +179,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
except Exception:
tp, value, tb = sys.exc_info()
session.rollback()
+ session._configure_locations()
six.reraise(tp, value, tb)
# Adding metadata
@@ -228,6 +230,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
except Exception:
tp, value, tb = sys.exc_info()
session.rollback()
+ session._configure_locations()
six.reraise(tp, value, tb)
# Adding metadata
@@ -242,6 +245,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
session.commit()
except Exception:
session.rollback()
+ session._configure_locations()
self.log.warning((
"Comment was not possible to set for AssetVersion"
"\"{0}\". Can't set it's value to: \"{1}\""
@@ -258,6 +262,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
continue
except Exception:
session.rollback()
+ session._configure_locations()
self.log.warning((
"Custom Attrubute \"{0}\""
@@ -272,6 +277,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
except Exception:
tp, value, tb = sys.exc_info()
session.rollback()
+ session._configure_locations()
six.reraise(tp, value, tb)
# Component
@@ -316,6 +322,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
except Exception:
tp, value, tb = sys.exc_info()
session.rollback()
+ session._configure_locations()
six.reraise(tp, value, tb)
# Reset members in memory
@@ -432,6 +439,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin):
except Exception:
tp, value, tb = sys.exc_info()
session.rollback()
+ session._configure_locations()
six.reraise(tp, value, tb)
if assetversion_entity not in used_asset_versions:
diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_note.py b/pype/plugins/ftrack/publish/integrate_ftrack_note.py
index 9566207145..acd295854d 100644
--- a/pype/plugins/ftrack/publish/integrate_ftrack_note.py
+++ b/pype/plugins/ftrack/publish/integrate_ftrack_note.py
@@ -145,4 +145,5 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin):
except Exception:
tp, value, tb = sys.exc_info()
session.rollback()
+ session._configure_locations()
six.reraise(tp, value, tb)
diff --git a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py
index e8a6151efd..7d4e0333d6 100644
--- a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py
+++ b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py
@@ -2,7 +2,6 @@ import sys
import six
import pyblish.api
from avalon import io
-from pprint import pformat
try:
from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_AUTO_SYNC
@@ -46,9 +45,6 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
hierarchy_context = self.context.data["hierarchyContext"]
- self.log.debug(
- f"__ hierarchy_context: `{pformat(hierarchy_context)}`")
-
self.session = self.context.data["ftrackSession"]
project_name = self.context.data["projectEntity"]["name"]
query = 'Project where full_name is "{}"'.format(project_name)
@@ -134,6 +130,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
+ self.session._configure_locations()
six.reraise(tp, value, tb)
# TASKS
@@ -162,6 +159,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
+ self.session._configure_locations()
six.reraise(tp, value, tb)
# Incoming links.
@@ -171,6 +169,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
+ self.session._configure_locations()
six.reraise(tp, value, tb)
# Create notes.
@@ -191,6 +190,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
+ self.session._configure_locations()
six.reraise(tp, value, tb)
# Import children.
@@ -207,6 +207,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
+ self.session._configure_locations()
six.reraise(tp, value, tb)
# Create new links.
@@ -248,6 +249,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
+ self.session._configure_locations()
six.reraise(tp, value, tb)
return task
@@ -262,6 +264,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
+ self.session._configure_locations()
six.reraise(tp, value, tb)
return entity
@@ -276,7 +279,8 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
- raise
+ self.session._configure_locations()
+ six.reraise(tp, value, tb)
def auto_sync_on(self, project):
@@ -289,4 +293,5 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
except Exception:
tp, value, tb = sys.exc_info()
self.session.rollback()
- raise
+ self.session._configure_locations()
+ six.reraise(tp, value, tb)
diff --git a/pype/plugins/global/load/copy_file.py b/pype/plugins/global/load/copy_file.py
index bbb8e1d6f7..1acacf6b27 100644
--- a/pype/plugins/global/load/copy_file.py
+++ b/pype/plugins/global/load/copy_file.py
@@ -20,8 +20,8 @@ class CopyFile(api.Loader):
def copy_file_to_clipboard(path):
from avalon.vendor.Qt import QtCore, QtWidgets
- app = QtWidgets.QApplication.instance()
- assert app, "Must have running QApplication instance"
+ clipboard = QtWidgets.QApplication.clipboard()
+ assert clipboard, "Must have running QApplication instance"
# Build mime data for clipboard
data = QtCore.QMimeData()
@@ -29,5 +29,4 @@ class CopyFile(api.Loader):
data.setUrls([url])
# Set to Clipboard
- clipboard = app.clipboard()
clipboard.setMimeData(data)
diff --git a/pype/plugins/global/load/copy_file_path.py b/pype/plugins/global/load/copy_file_path.py
index cfda9dc271..f64f3e76d8 100644
--- a/pype/plugins/global/load/copy_file_path.py
+++ b/pype/plugins/global/load/copy_file_path.py
@@ -19,11 +19,10 @@ class CopyFilePath(api.Loader):
@staticmethod
def copy_path_to_clipboard(path):
- from avalon.vendor.Qt import QtCore, QtWidgets
+ from avalon.vendor.Qt import QtWidgets
- app = QtWidgets.QApplication.instance()
- assert app, "Must have running QApplication instance"
+ clipboard = QtWidgets.QApplication.clipboard()
+ assert clipboard, "Must have running QApplication instance"
# Set to Clipboard
- clipboard = app.clipboard()
clipboard.setText(os.path.normpath(path))
diff --git a/pype/plugins/global/publish/collect_anatomy_instance_data.py b/pype/plugins/global/publish/collect_anatomy_instance_data.py
index 44a4d43946..446f671b86 100644
--- a/pype/plugins/global/publish/collect_anatomy_instance_data.py
+++ b/pype/plugins/global/publish/collect_anatomy_instance_data.py
@@ -23,123 +23,256 @@ Provides:
import copy
import json
+import collections
from avalon import io
import pyblish.api
-class CollectAnatomyInstanceData(pyblish.api.InstancePlugin):
- """Collect Instance specific Anatomy data."""
+class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
+ """Collect Instance specific Anatomy data.
+
+ Plugin is running for all instances on context even not active instances.
+ """
order = pyblish.api.CollectorOrder + 0.49
label = "Collect Anatomy Instance data"
- def process(self, instance):
- # get all the stuff from the database
- anatomy_data = copy.deepcopy(instance.context.data["anatomyData"])
- project_entity = instance.context.data["projectEntity"]
- context_asset_entity = instance.context.data["assetEntity"]
- instance_asset_entity = instance.data.get("assetEntity")
+ def process(self, context):
+ self.log.info("Collecting anatomy data for all instances.")
- asset_name = instance.data["asset"]
+ self.fill_missing_asset_docs(context)
+ self.fill_latest_versions(context)
+ self.fill_anatomy_data(context)
- # There is possibility that assetEntity on instance is already set
- # which can happen in standalone publisher
- if (
- instance_asset_entity
- and instance_asset_entity["name"] == asset_name
- ):
- asset_entity = instance_asset_entity
+ self.log.info("Anatomy Data collection finished.")
- # Check if asset name is the same as what is in context
- # - they may be different, e.g. in NukeStudio
- elif context_asset_entity["name"] == asset_name:
- asset_entity = context_asset_entity
+ def fill_missing_asset_docs(self, context):
+ self.log.debug("Qeurying asset documents for instances.")
- else:
- asset_entity = io.find_one({
- "type": "asset",
- "name": asset_name,
- "parent": project_entity["_id"]
- })
+ context_asset_doc = context.data["assetEntity"]
- subset_name = instance.data["subset"]
- version_number = instance.data.get("version")
- latest_version = None
+ instances_with_missing_asset_doc = collections.defaultdict(list)
+ for instance in context:
+ instance_asset_doc = instance.data.get("assetEntity")
+ _asset_name = instance.data["asset"]
- if asset_entity:
- subset_entity = io.find_one({
- "type": "subset",
- "name": subset_name,
- "parent": asset_entity["_id"]
- })
+ # There is possibility that assetEntity on instance is already set
+ # which can happen in standalone publisher
+ if (
+ instance_asset_doc
+ and instance_asset_doc["name"] == _asset_name
+ ):
+ continue
+
+ # Check if asset name is the same as what is in context
+ # - they may be different, e.g. in NukeStudio
+ if context_asset_doc["name"] == _asset_name:
+ instance.data["assetEntity"] = context_asset_doc
- if subset_entity is None:
- self.log.debug("Subset entity does not exist yet.")
else:
- version_entity = io.find_one(
- {
- "type": "version",
- "parent": subset_entity["_id"]
- },
- sort=[("name", -1)]
- )
- if version_entity:
- latest_version = version_entity["name"]
+ instances_with_missing_asset_doc[_asset_name].append(instance)
- # If version is not specified for instance or context
- if version_number is None:
- # TODO we should be able to change default version by studio
- # preferences (like start with version number `0`)
- version_number = 1
- # use latest version (+1) if already any exist
- if latest_version is not None:
- version_number += int(latest_version)
+ if not instances_with_missing_asset_doc:
+ self.log.debug("All instances already had right asset document.")
+ return
- anatomy_updates = {
- "asset": asset_name,
- "family": instance.data["family"],
- "subset": subset_name,
- "version": version_number
+ asset_names = list(instances_with_missing_asset_doc.keys())
+ self.log.debug("Querying asset documents with names: {}".format(
+ ", ".join(["\"{}\"".format(name) for name in asset_names])
+ ))
+ asset_docs = io.find({
+ "type": "asset",
+ "name": {"$in": asset_names}
+ })
+ asset_docs_by_name = {
+ asset_doc["name"]: asset_doc
+ for asset_doc in asset_docs
}
- if (
- asset_entity
- and asset_entity["_id"] != context_asset_entity["_id"]
- ):
- parents = asset_entity["data"].get("parents") or list()
- anatomy_updates["hierarchy"] = "/".join(parents)
- task_name = instance.data.get("task")
- if task_name:
- anatomy_updates["task"] = task_name
+ not_found_asset_names = []
+ for asset_name, instances in instances_with_missing_asset_doc.items():
+ asset_doc = asset_docs_by_name.get(asset_name)
+ if not asset_doc:
+ not_found_asset_names.append(asset_name)
+ continue
- # Version should not be collected since may be instance
- anatomy_data.update(anatomy_updates)
+ for _instance in instances:
+ _instance.data["assetEntity"] = asset_doc
- resolution_width = instance.data.get("resolutionWidth")
- if resolution_width:
- anatomy_data["resolution_width"] = resolution_width
+ if not_found_asset_names:
+ joined_asset_names = ", ".join(
+ ["\"{}\"".format(name) for name in not_found_asset_names]
+ )
+ self.log.warning((
+ "Not found asset documents with names \"{}\"."
+ ).format(joined_asset_names))
- resolution_height = instance.data.get("resolutionHeight")
- if resolution_height:
- anatomy_data["resolution_height"] = resolution_height
+ def fill_latest_versions(self, context):
+ """Try to find latest version for each instance's subset.
- pixel_aspect = instance.data.get("pixelAspect")
- if pixel_aspect:
- anatomy_data["pixel_aspect"] = float("{:0.2f}".format(
- float(pixel_aspect)))
+ Key "latestVersion" is always set to latest version or `None`.
- fps = instance.data.get("fps")
- if fps:
- anatomy_data["fps"] = float("{:0.2f}".format(
- float(fps)))
+ Args:
+ context (pyblish.Context)
- instance.data["projectEntity"] = project_entity
- instance.data["assetEntity"] = asset_entity
- instance.data["anatomyData"] = anatomy_data
- instance.data["latestVersion"] = latest_version
- # TODO should be version number set here?
- instance.data["version"] = version_number
+ Returns:
+ None
- self.log.info("Instance anatomy Data collected")
- self.log.debug(json.dumps(anatomy_data, indent=4))
+ """
+ self.log.debug("Qeurying latest versions for instances.")
+
+ hierarchy = {}
+ subset_names = set()
+ asset_ids = set()
+ for instance in context:
+ # Make sure `"latestVersion"` key is set
+ latest_version = instance.data.get("latestVersion")
+ instance.data["latestVersion"] = latest_version
+
+ # Skip instances withou "assetEntity"
+ asset_doc = instance.data.get("assetEntity")
+ if not asset_doc:
+ continue
+
+ # Store asset ids and subset names for queries
+ asset_id = asset_doc["_id"]
+ subset_name = instance.data["subset"]
+ asset_ids.add(asset_id)
+ subset_names.add(subset_name)
+
+ # Prepare instance hiearchy for faster filling latest versions
+ if asset_id not in hierarchy:
+ hierarchy[asset_id] = {}
+ if subset_name not in hierarchy[asset_id]:
+ hierarchy[asset_id][subset_name] = []
+ hierarchy[asset_id][subset_name].append(instance)
+
+ subset_docs = list(io.find({
+ "type": "subset",
+ "parent": {"$in": list(asset_ids)},
+ "name": {"$in": list(subset_names)}
+ }))
+
+ subset_ids = [
+ subset_doc["_id"]
+ for subset_doc in subset_docs
+ ]
+
+ last_version_by_subset_id = self._query_last_versions(subset_ids)
+ for subset_doc in subset_docs:
+ subset_id = subset_doc["_id"]
+ last_version = last_version_by_subset_id.get(subset_id)
+ if last_version is None:
+ continue
+
+ asset_id = subset_doc["parent"]
+ subset_name = subset_doc["name"]
+ _instances = hierarchy[asset_id][subset_name]
+ for _instance in _instances:
+ _instance.data["latestVersion"] = last_version
+
+ def _query_last_versions(self, subset_ids):
+ """Retrieve all latest versions for entered subset_ids.
+
+ Args:
+ subset_ids (list): List of subset ids with type `ObjectId`.
+
+ Returns:
+ dict: Key is subset id and value is last version name.
+ """
+ _pipeline = [
+ # Find all versions of those subsets
+ {"$match": {
+ "type": "version",
+ "parent": {"$in": subset_ids}
+ }},
+ # Sorting versions all together
+ {"$sort": {"name": 1}},
+ # Group them by "parent", but only take the last
+ {"$group": {
+ "_id": "$parent",
+ "_version_id": {"$last": "$_id"},
+ "name": {"$last": "$name"}
+ }}
+ ]
+
+ last_version_by_subset_id = {}
+ for doc in io.aggregate(_pipeline):
+ subset_id = doc["_id"]
+ last_version_by_subset_id[subset_id] = doc["name"]
+
+ return last_version_by_subset_id
+
+ def fill_anatomy_data(self, context):
+ self.log.debug("Storing anatomy data to instance data.")
+
+ project_doc = context.data["projectEntity"]
+ context_asset_doc = context.data["assetEntity"]
+
+ for instance in context:
+ version_number = instance.data.get("version")
+ # If version is not specified for instance or context
+ if version_number is None:
+ # TODO we should be able to change default version by studio
+ # preferences (like start with version number `0`)
+ version_number = 1
+ # use latest version (+1) if already any exist
+ latest_version = instance.data["latestVersion"]
+ if latest_version is not None:
+ version_number += int(latest_version)
+
+ anatomy_updates = {
+ "asset": instance.data["asset"],
+ "family": instance.data["family"],
+ "subset": instance.data["subset"],
+ "version": version_number
+ }
+
+ # Hiearchy
+ asset_doc = instance.data.get("assetEntity")
+ if asset_doc and asset_doc["_id"] != context_asset_doc["_id"]:
+ parents = asset_doc["data"].get("parents") or list()
+ anatomy_updates["hierarchy"] = "/".join(parents)
+
+ # Task
+ task_name = instance.data.get("task")
+ if task_name:
+ anatomy_updates["task"] = task_name
+
+ # Additional data
+ resolution_width = instance.data.get("resolutionWidth")
+ if resolution_width:
+ anatomy_updates["resolution_width"] = resolution_width
+
+ resolution_height = instance.data.get("resolutionHeight")
+ if resolution_height:
+ anatomy_updates["resolution_height"] = resolution_height
+
+ pixel_aspect = instance.data.get("pixelAspect")
+ if pixel_aspect:
+ anatomy_updates["pixel_aspect"] = float(
+ "{:0.2f}".format(float(pixel_aspect))
+ )
+
+ fps = instance.data.get("fps")
+ if fps:
+ anatomy_updates["fps"] = float("{:0.2f}".format(float(fps)))
+
+ anatomy_data = copy.deepcopy(context.data["anatomyData"])
+ anatomy_data.update(anatomy_updates)
+
+ # Store anatomy data
+ instance.data["projectEntity"] = project_doc
+ instance.data["anatomyData"] = anatomy_data
+ instance.data["version"] = version_number
+
+ # Log collected data
+ instance_name = instance.data["name"]
+ instance_label = instance.data.get("label")
+ if instance_label:
+ instance_name += "({})".format(instance_label)
+ self.log.debug("Anatomy data for instance {}: {}".format(
+ instance_name,
+ json.dumps(anatomy_data, indent=4)
+ ))
diff --git a/pype/plugins/global/publish/extract_burnin.py b/pype/plugins/global/publish/extract_burnin.py
index 4443cfe223..6e8da1b054 100644
--- a/pype/plugins/global/publish/extract_burnin.py
+++ b/pype/plugins/global/publish/extract_burnin.py
@@ -195,11 +195,14 @@ class ExtractBurnin(pype.api.Extractor):
if "delete" in new_repre["tags"]:
new_repre["tags"].remove("delete")
- # Update name and outputName to be able have multiple outputs
- # Join previous "outputName" with filename suffix
- new_name = "_".join([new_repre["outputName"], filename_suffix])
- new_repre["name"] = new_name
- new_repre["outputName"] = new_name
+ if len(repre_burnin_defs.keys()) > 1:
+ # Update name and outputName to be
+ # able have multiple outputs in case of more burnin presets
+ # Join previous "outputName" with filename suffix
+ new_name = "_".join(
+ [new_repre["outputName"], filename_suffix])
+ new_repre["name"] = new_name
+ new_repre["outputName"] = new_name
# Prepare paths and files for process.
self.input_output_paths(new_repre, temp_data, filename_suffix)
diff --git a/pype/plugins/global/publish/extract_hierarchy_avalon.py b/pype/plugins/global/publish/extract_hierarchy_avalon.py
index 4253c35929..5d11eae058 100644
--- a/pype/plugins/global/publish/extract_hierarchy_avalon.py
+++ b/pype/plugins/global/publish/extract_hierarchy_avalon.py
@@ -1,6 +1,6 @@
import pyblish.api
from avalon import io
-
+from copy import deepcopy
class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin):
"""Create entities in Avalon based on collected data."""
@@ -14,14 +14,12 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin):
if "hierarchyContext" not in context.data:
self.log.info("skipping IntegrateHierarchyToAvalon")
return
+ hierarchy_context = deepcopy(context.data["hierarchyContext"])
if not io.Session:
io.install()
active_assets = []
- hierarchy_context = context.data["hierarchyContext"]
- hierarchy_assets = self._get_assets(hierarchy_context)
-
# filter only the active publishing insatnces
for instance in context:
if instance.data.get("publish") is False:
@@ -32,13 +30,13 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin):
active_assets.append(instance.data["asset"])
- # filter out only assets which are activated as isntances
- new_hierarchy_assets = {k: v for k, v in hierarchy_assets.items()
- if k in active_assets}
+ # remove duplicity in list
+ self.active_assets = list(set(active_assets))
+ self.log.debug("__ self.active_assets: {}".format(self.active_assets))
- # modify the hierarchy context so there are only fitred assets
- self._set_assets(hierarchy_context, new_hierarchy_assets)
+ hierarchy_context = self._get_assets(hierarchy_context)
+ self.log.debug("__ hierarchy_context: {}".format(hierarchy_context))
input_data = context.data["hierarchyContext"] = hierarchy_context
self.project = None
@@ -178,35 +176,18 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin):
Usually the last part of deep dictionary which
is not having any children
"""
+ input_dict_copy = deepcopy(input_dict)
+
for key in input_dict.keys():
+ self.log.debug("__ key: {}".format(key))
# check if child key is available
if input_dict[key].get("childs"):
# loop deeper
- return self._get_assets(input_dict[key]["childs"])
+ input_dict_copy[key]["childs"] = self._get_assets(
+ input_dict[key]["childs"])
else:
- # give the dictionary with assets
- return input_dict
+ # filter out unwanted assets
+ if key not in self.active_assets:
+ input_dict_copy.pop(key, None)
- def _set_assets(self, input_dict, new_assets=None):
- """ Modify the hierarchy context dictionary.
- It will replace the asset dictionary with only the filtred one.
- """
- for key in input_dict.keys():
- # check if child key is available
- if input_dict[key].get("childs"):
- # return if this is just for testing purpose and no
- # new_assets property is avalable
- if not new_assets:
- return True
-
- # test for deeper inner children availabelity
- if self._set_assets(input_dict[key]["childs"]):
- # if one level deeper is still children available
- # then process farther
- self._set_assets(input_dict[key]["childs"], new_assets)
- else:
- # or just assign the filtred asset ditionary
- input_dict[key]["childs"] = new_assets
- else:
- # test didnt find more childs in input dictionary
- return None
+ return input_dict_copy
diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py
index 0bae1b2ddc..318c843b80 100644
--- a/pype/plugins/global/publish/extract_review.py
+++ b/pype/plugins/global/publish/extract_review.py
@@ -633,6 +633,26 @@ class ExtractReview(pyblish.api.InstancePlugin):
input_width = int(input_data["width"])
input_height = int(input_data["height"])
+ # Make sure input width and height is not an odd number
+ input_width_is_odd = bool(input_width % 2 != 0)
+ input_height_is_odd = bool(input_height % 2 != 0)
+ if input_width_is_odd or input_height_is_odd:
+ # Add padding to input and make sure this filter is at first place
+ filters.append("pad=width=ceil(iw/2)*2:height=ceil(ih/2)*2")
+
+ # Change input width or height as first filter will change them
+ if input_width_is_odd:
+ self.log.info((
+ "Converting input width from odd to even number. {} -> {}"
+ ).format(input_width, input_width + 1))
+ input_width += 1
+
+ if input_height_is_odd:
+ self.log.info((
+ "Converting input height from odd to even number. {} -> {}"
+ ).format(input_height, input_height + 1))
+ input_height += 1
+
self.log.debug("pixel_aspect: `{}`".format(pixel_aspect))
self.log.debug("input_width: `{}`".format(input_width))
self.log.debug("input_height: `{}`".format(input_height))
@@ -654,6 +674,22 @@ class ExtractReview(pyblish.api.InstancePlugin):
output_width = int(output_width)
output_height = int(output_height)
+ # Make sure output width and height is not an odd number
+ # When this can happen:
+ # - if output definition has set width and height with odd number
+ # - `instance.data` contain width and height with odd numbeer
+ if output_width % 2 != 0:
+ self.log.warning((
+ "Converting output width from odd to even number. {} -> {}"
+ ).format(output_width, output_width + 1))
+ output_width += 1
+
+ if output_height % 2 != 0:
+ self.log.warning((
+ "Converting output height from odd to even number. {} -> {}"
+ ).format(output_height, output_height + 1))
+ output_height += 1
+
self.log.debug(
"Output resolution is {}x{}".format(output_width, output_height)
)
diff --git a/pype/plugins/global/publish/validate_intent.py b/pype/plugins/global/publish/validate_intent.py
new file mode 100644
index 0000000000..80bcb0e164
--- /dev/null
+++ b/pype/plugins/global/publish/validate_intent.py
@@ -0,0 +1,31 @@
+import pyblish.api
+import os
+
+
+class ValidateIntent(pyblish.api.ContextPlugin):
+ """Validate intent of the publish.
+
+ It is required to fill the intent of this publish. Chech the log
+ for more details
+ """
+
+ order = pyblish.api.ValidatorOrder
+
+ label = "Validate Intent"
+ # TODO: this should be off by default and only activated viac config
+ tasks = ["animation"]
+ hosts = ["harmony"]
+ if os.environ.get("AVALON_TASK") not in tasks:
+ active = False
+
+ def process(self, context):
+ msg = (
+ "Please make sure that you select the intent of this publish."
+ )
+
+ intent = context.data.get("intent")
+ self.log.debug(intent)
+ assert intent, msg
+
+ intent_value = intent.get("value")
+ assert intent is not "", msg
diff --git a/pype/plugins/maya/load/load_audio.py b/pype/plugins/maya/load/load_audio.py
index ca38082ed0..81bcca48e1 100644
--- a/pype/plugins/maya/load/load_audio.py
+++ b/pype/plugins/maya/load/load_audio.py
@@ -1,7 +1,7 @@
from maya import cmds, mel
import pymel.core as pc
-from avalon import api
+from avalon import api, io
from avalon.maya.pipeline import containerise
from avalon.maya import lib
@@ -58,6 +58,13 @@ class AudioLoader(api.Loader):
type="string"
)
+ # Set frame range.
+ version = io.find_one({"_id": representation["parent"]})
+ subset = io.find_one({"_id": version["parent"]})
+ asset = io.find_one({"_id": subset["parent"]})
+ audio_node.sourceStart.set(1 - asset["data"]["frameStart"])
+ audio_node.sourceEnd.set(asset["data"]["frameEnd"])
+
def switch(self, container, representation):
self.update(container, representation)
diff --git a/pype/plugins/maya/load/load_image_plane.py b/pype/plugins/maya/load/load_image_plane.py
index 7d8ae27f89..e17382f7ed 100644
--- a/pype/plugins/maya/load/load_image_plane.py
+++ b/pype/plugins/maya/load/load_image_plane.py
@@ -1,7 +1,7 @@
import pymel.core as pc
import maya.cmds as cmds
-from avalon import api
+from avalon import api, io
from avalon.maya.pipeline import containerise
from avalon.maya import lib
from Qt import QtWidgets
@@ -147,6 +147,17 @@ class ImagePlaneLoader(api.Loader):
type="string"
)
+ # Set frame range.
+ version = io.find_one({"_id": representation["parent"]})
+ subset = io.find_one({"_id": version["parent"]})
+ asset = io.find_one({"_id": subset["parent"]})
+ start_frame = asset["data"]["frameStart"]
+ end_frame = asset["data"]["frameEnd"]
+ image_plane_shape.frameOffset.set(1 - start_frame)
+ image_plane_shape.frameIn.set(start_frame)
+ image_plane_shape.frameOut.set(end_frame)
+ image_plane_shape.frameCache.set(end_frame)
+
def switch(self, container, representation):
self.update(container, representation)
diff --git a/pype/plugins/maya/publish/extract_camera_mayaScene.py b/pype/plugins/maya/publish/extract_camera_mayaScene.py
index 03dde031e9..1a0f4694d1 100644
--- a/pype/plugins/maya/publish/extract_camera_mayaScene.py
+++ b/pype/plugins/maya/publish/extract_camera_mayaScene.py
@@ -101,7 +101,7 @@ class ExtractCameraMayaScene(pype.api.Extractor):
self.log.info(
"Using {} as scene type".format(self.scene_type))
break
- except AttributeError:
+ except KeyError:
# no preset found
pass
diff --git a/pype/plugins/maya/publish/extract_maya_scene_raw.py b/pype/plugins/maya/publish/extract_maya_scene_raw.py
index 2971572552..d273646af8 100644
--- a/pype/plugins/maya/publish/extract_maya_scene_raw.py
+++ b/pype/plugins/maya/publish/extract_maya_scene_raw.py
@@ -33,7 +33,7 @@ class ExtractMayaSceneRaw(pype.api.Extractor):
self.log.info(
"Using {} as scene type".format(self.scene_type))
break
- except AttributeError:
+ except KeyError:
# no preset found
pass
# Define extract output file path
diff --git a/pype/plugins/maya/publish/extract_model.py b/pype/plugins/maya/publish/extract_model.py
index 330e471e53..d77e65f989 100644
--- a/pype/plugins/maya/publish/extract_model.py
+++ b/pype/plugins/maya/publish/extract_model.py
@@ -41,7 +41,7 @@ class ExtractModel(pype.api.Extractor):
self.log.info(
"Using {} as scene type".format(self.scene_type))
break
- except AttributeError:
+ except KeyError:
# no preset found
pass
# Define extract output file path
diff --git a/pype/plugins/maya/publish/extract_yeti_rig.py b/pype/plugins/maya/publish/extract_yeti_rig.py
index 2f66d3e026..d48a956b88 100644
--- a/pype/plugins/maya/publish/extract_yeti_rig.py
+++ b/pype/plugins/maya/publish/extract_yeti_rig.py
@@ -111,7 +111,7 @@ class ExtractYetiRig(pype.api.Extractor):
self.log.info(
"Using {} as scene type".format(self.scene_type))
break
- except AttributeError:
+ except KeyError:
# no preset found
pass
yeti_nodes = cmds.ls(instance, type="pgYetiMaya")
diff --git a/pype/plugins/nukestudio/publish/extract_review_cutup_video.py b/pype/plugins/nukestudio/publish/extract_review_cutup_video.py
index a4fbf90bed..d1ce3675b1 100644
--- a/pype/plugins/nukestudio/publish/extract_review_cutup_video.py
+++ b/pype/plugins/nukestudio/publish/extract_review_cutup_video.py
@@ -76,7 +76,7 @@ class ExtractReviewCutUpVideo(pype.api.Extractor):
# check if audio stream is in input video file
ffprob_cmd = (
- "{ffprobe_path} -i {full_input_path} -show_streams "
+ "{ffprobe_path} -i \"{full_input_path}\" -show_streams "
"-select_streams a -loglevel error"
).format(**locals())
self.log.debug("ffprob_cmd: {}".format(ffprob_cmd))
@@ -106,7 +106,7 @@ class ExtractReviewCutUpVideo(pype.api.Extractor):
# try to get video native resolution data
try:
resolution_output = pype.api.subprocess((
- "{ffprobe_path} -i {full_input_path} -v error "
+ "{ffprobe_path} -i \"{full_input_path}\" -v error "
"-select_streams v:0 -show_entries "
"stream=width,height -of csv=s=x:p=0"
).format(**locals()))
@@ -193,7 +193,7 @@ class ExtractReviewCutUpVideo(pype.api.Extractor):
# append ffmpeg input video clip
input_args.append("-ss {:0.2f}".format(start_sec))
input_args.append("-t {:0.2f}".format(duration_sec))
- input_args.append("-i {}".format(full_input_path))
+ input_args.append("-i \"{}\"".format(full_input_path))
# add copy audio video codec if only shortening clip
if ("_cut-bigger" in tags) and (not empty_add):
@@ -203,8 +203,7 @@ class ExtractReviewCutUpVideo(pype.api.Extractor):
output_args.append("-intra")
# output filename
- output_args.append("-y")
- output_args.append(full_output_path)
+ output_args.append("-y \"{}\"".format(full_output_path))
mov_args = [
ffmpeg_path,
diff --git a/pype/plugins/standalonepublisher/publish/collect_clip_instances.py b/pype/plugins/standalonepublisher/publish/collect_clip_instances.py
index a7af8df143..def0c13a78 100644
--- a/pype/plugins/standalonepublisher/publish/collect_clip_instances.py
+++ b/pype/plugins/standalonepublisher/publish/collect_clip_instances.py
@@ -17,13 +17,13 @@ class CollectClipInstances(pyblish.api.InstancePlugin):
subsets = {
"referenceMain": {
"family": "review",
- "families": ["review", "ftrack"],
+ "families": ["clip", "ftrack"],
# "ftrackFamily": "review",
"extension": ".mp4"
},
"audioMain": {
"family": "audio",
- "families": ["ftrack"],
+ "families": ["clip", "ftrack"],
# "ftrackFamily": "audio",
"extension": ".wav",
# "version": 1
diff --git a/pype/plugins/standalonepublisher/publish/collect_instance_data.py b/pype/plugins/standalonepublisher/publish/collect_instance_data.py
new file mode 100644
index 0000000000..1b32ea9144
--- /dev/null
+++ b/pype/plugins/standalonepublisher/publish/collect_instance_data.py
@@ -0,0 +1,29 @@
+"""
+Requires:
+ Nothing
+
+Provides:
+ Instance
+"""
+
+import pyblish.api
+from pprint import pformat
+
+
+class CollectInstanceData(pyblish.api.InstancePlugin):
+ """
+ Collector with only one reason for its existence - remove 'ftrack'
+ family implicitly added by Standalone Publisher
+ """
+
+ label = "Collect instance data"
+ order = pyblish.api.CollectorOrder + 0.49
+ families = ["render", "plate"]
+ hosts = ["standalonepublisher"]
+
+ def process(self, instance):
+ fps = instance.data["assetEntity"]["data"]["fps"]
+ instance.data.update({
+ "fps": fps
+ })
+ self.log.debug(f"instance.data: {pformat(instance.data)}")
diff --git a/pype/plugins/standalonepublisher/publish/extract_shot_data.py b/pype/plugins/standalonepublisher/publish/extract_shot_data.py
index c39247d6d6..d5af7638ee 100644
--- a/pype/plugins/standalonepublisher/publish/extract_shot_data.py
+++ b/pype/plugins/standalonepublisher/publish/extract_shot_data.py
@@ -10,7 +10,7 @@ class ExtractShotData(pype.api.Extractor):
label = "Extract Shot Data"
hosts = ["standalonepublisher"]
- families = ["review", "audio"]
+ families = ["clip"]
# presets
diff --git a/pype/plugins/standalonepublisher/publish/extract_thumbnail.py b/pype/plugins/standalonepublisher/publish/extract_thumbnail.py
index cddc9c3a82..5882775083 100644
--- a/pype/plugins/standalonepublisher/publish/extract_thumbnail.py
+++ b/pype/plugins/standalonepublisher/publish/extract_thumbnail.py
@@ -64,6 +64,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
else:
# Convert to jpeg if not yet
full_input_path = os.path.join(thumbnail_repre["stagingDir"], file)
+ full_input_path = '"{}"'.format(full_input_path)
self.log.info("input {}".format(full_input_path))
full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1]
diff --git a/pype/plugins/standalonepublisher/publish/validate_editorial_resources.py b/pype/plugins/standalonepublisher/publish/validate_editorial_resources.py
index ebc449c4ec..7e1694fbd1 100644
--- a/pype/plugins/standalonepublisher/publish/validate_editorial_resources.py
+++ b/pype/plugins/standalonepublisher/publish/validate_editorial_resources.py
@@ -1,5 +1,3 @@
-import os
-
import pyblish.api
import pype.api
@@ -9,10 +7,14 @@ class ValidateEditorialResources(pyblish.api.InstancePlugin):
label = "Validate Editorial Resources"
hosts = ["standalonepublisher"]
- families = ["audio", "review"]
+ families = ["clip"]
+
order = pype.api.ValidateContentsOrder
def process(self, instance):
+ self.log.debug(
+ f"Instance: {instance}, Families: "
+ f"{[instance.data['family']] + instance.data['families']}")
check_file = instance.data["editorialVideoPath"]
msg = f"Missing \"{check_file}\"."
assert check_file, msg
diff --git a/pype/resources/app_icons/hiero.png b/pype/resources/app_icons/hiero.png
new file mode 100644
index 0000000000..04bbf6265b
Binary files /dev/null and b/pype/resources/app_icons/hiero.png differ
diff --git a/pype/scripts/otio_burnin.py b/pype/scripts/otio_burnin.py
index 156896a759..6607726c73 100644
--- a/pype/scripts/otio_burnin.py
+++ b/pype/scripts/otio_burnin.py
@@ -15,7 +15,7 @@ ffprobe_path = pype.lib.get_ffmpeg_tool_path("ffprobe")
FFMPEG = (
- '{} -loglevel panic -i %(input)s %(filters)s %(args)s%(output)s'
+ '{} -loglevel panic -i "%(input)s" %(filters)s %(args)s%(output)s'
).format(ffmpeg_path)
FFPROBE = (
diff --git a/pype/settings/__init__.py b/pype/settings/__init__.py
index 7e73d541a4..7a99ba0b2f 100644
--- a/pype/settings/__init__.py
+++ b/pype/settings/__init__.py
@@ -1,9 +1,11 @@
from .lib import (
system_settings,
- project_settings
+ project_settings,
+ environments
)
__all__ = (
"system_settings",
- "project_settings"
+ "project_settings",
+ "environments"
)
diff --git a/pype/settings/defaults/system_settings/environments/avalon.json b/pype/settings/defaults/environments/avalon.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/avalon.json
rename to pype/settings/defaults/environments/avalon.json
diff --git a/pype/settings/defaults/system_settings/environments/blender.json b/pype/settings/defaults/environments/blender.json
similarity index 82%
rename from pype/settings/defaults/system_settings/environments/blender.json
rename to pype/settings/defaults/environments/blender.json
index 6f4f6a012d..00a4070b8e 100644
--- a/pype/settings/defaults/system_settings/environments/blender.json
+++ b/pype/settings/defaults/environments/blender.json
@@ -3,5 +3,6 @@
"PYTHONPATH": [
"{PYPE_SETUP_PATH}/repos/avalon-core/setup/blender",
"{PYTHONPATH}"
- ]
+ ],
+ "CREATE_NEW_CONSOLE": "yes"
}
diff --git a/pype/settings/defaults/system_settings/environments/celaction.json b/pype/settings/defaults/environments/celaction.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/celaction.json
rename to pype/settings/defaults/environments/celaction.json
diff --git a/pype/settings/defaults/system_settings/environments/deadline.json b/pype/settings/defaults/environments/deadline.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/deadline.json
rename to pype/settings/defaults/environments/deadline.json
diff --git a/pype/settings/defaults/system_settings/environments/ftrack.json b/pype/settings/defaults/environments/ftrack.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/ftrack.json
rename to pype/settings/defaults/environments/ftrack.json
diff --git a/pype/settings/defaults/system_settings/environments/global.json b/pype/settings/defaults/environments/global.json
similarity index 91%
rename from pype/settings/defaults/system_settings/environments/global.json
rename to pype/settings/defaults/environments/global.json
index ef528e6857..ba467d2f5d 100644
--- a/pype/settings/defaults/system_settings/environments/global.json
+++ b/pype/settings/defaults/environments/global.json
@@ -6,9 +6,9 @@
"PYPE_PROJECT_PLUGINS": "",
"STUDIO_SOFT": "{PYP_SETUP_ROOT}/soft",
"FFMPEG_PATH": {
- "windows": "{VIRTUAL_ENV}/localized/ffmpeg_exec/windows/bin;{PYPE_SETUP_PATH}/vendor/ffmpeg_exec/windows/bin",
- "darwin": "{VIRTUAL_ENV}/localized/ffmpeg_exec/darwin/bin:{PYPE_SETUP_PATH}/vendor/ffmpeg_exec/darwin/bin",
- "linux": "{VIRTUAL_ENV}/localized/ffmpeg_exec/linux:{PYPE_SETUP_PATH}/vendor/ffmpeg_exec/linux"
+ "windows": "{VIRTUAL_ENV}/localized/ffmpeg_exec/windows/bin;{PYPE_SETUP_PATH}/vendor/bin/ffmpeg_exec/windows/bin",
+ "darwin": "{VIRTUAL_ENV}/localized/ffmpeg_exec/darwin/bin:{PYPE_SETUP_PATH}/vendor/bin/ffmpeg_exec/darwin/bin",
+ "linux": "{VIRTUAL_ENV}/localized/ffmpeg_exec/linux:{PYPE_SETUP_PATH}/vendor/bin/ffmpeg_exec/linux"
},
"DJV_PATH": {
"windows": [
diff --git a/pype/settings/defaults/system_settings/environments/harmony.json b/pype/settings/defaults/environments/harmony.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/harmony.json
rename to pype/settings/defaults/environments/harmony.json
diff --git a/pype/settings/defaults/system_settings/environments/houdini.json b/pype/settings/defaults/environments/houdini.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/houdini.json
rename to pype/settings/defaults/environments/houdini.json
diff --git a/pype/settings/defaults/system_settings/environments/maya.json b/pype/settings/defaults/environments/maya.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/maya.json
rename to pype/settings/defaults/environments/maya.json
diff --git a/pype/settings/defaults/system_settings/environments/maya_2018.json b/pype/settings/defaults/environments/maya_2018.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/maya_2018.json
rename to pype/settings/defaults/environments/maya_2018.json
diff --git a/pype/settings/defaults/system_settings/environments/maya_2020.json b/pype/settings/defaults/environments/maya_2020.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/maya_2020.json
rename to pype/settings/defaults/environments/maya_2020.json
diff --git a/pype/settings/defaults/system_settings/environments/mayabatch.json b/pype/settings/defaults/environments/mayabatch.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/mayabatch.json
rename to pype/settings/defaults/environments/mayabatch.json
diff --git a/pype/settings/defaults/system_settings/environments/mayabatch_2019.json b/pype/settings/defaults/environments/mayabatch_2019.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/mayabatch_2019.json
rename to pype/settings/defaults/environments/mayabatch_2019.json
diff --git a/pype/settings/defaults/system_settings/environments/mtoa_3.1.1.json b/pype/settings/defaults/environments/mtoa_3.1.1.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/mtoa_3.1.1.json
rename to pype/settings/defaults/environments/mtoa_3.1.1.json
diff --git a/pype/settings/defaults/system_settings/environments/muster.json b/pype/settings/defaults/environments/muster.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/muster.json
rename to pype/settings/defaults/environments/muster.json
diff --git a/pype/settings/defaults/system_settings/environments/nuke.json b/pype/settings/defaults/environments/nuke.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/nuke.json
rename to pype/settings/defaults/environments/nuke.json
diff --git a/pype/settings/defaults/system_settings/environments/nukestudio.json b/pype/settings/defaults/environments/nukestudio.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/nukestudio.json
rename to pype/settings/defaults/environments/nukestudio.json
diff --git a/pype/settings/defaults/system_settings/environments/nukestudio_10.0.json b/pype/settings/defaults/environments/nukestudio_10.0.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/nukestudio_10.0.json
rename to pype/settings/defaults/environments/nukestudio_10.0.json
diff --git a/pype/settings/defaults/system_settings/environments/nukex.json b/pype/settings/defaults/environments/nukex.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/nukex.json
rename to pype/settings/defaults/environments/nukex.json
diff --git a/pype/settings/defaults/system_settings/environments/nukex_10.0.json b/pype/settings/defaults/environments/nukex_10.0.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/nukex_10.0.json
rename to pype/settings/defaults/environments/nukex_10.0.json
diff --git a/pype/settings/defaults/environments/photoshop.json b/pype/settings/defaults/environments/photoshop.json
new file mode 100644
index 0000000000..d39634ce20
--- /dev/null
+++ b/pype/settings/defaults/environments/photoshop.json
@@ -0,0 +1,7 @@
+{
+ "AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH": "1",
+ "PYTHONPATH": "{PYTHONPATH}",
+ "PYPE_LOG_NO_COLORS": "Yes",
+ "WEBSOCKET_URL": "ws://localhost:8099/ws/",
+ "WORKFILES_SAVE_AS": "Yes"
+}
diff --git a/pype/settings/defaults/system_settings/environments/premiere.json b/pype/settings/defaults/environments/premiere.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/premiere.json
rename to pype/settings/defaults/environments/premiere.json
diff --git a/pype/settings/defaults/system_settings/environments/resolve.json b/pype/settings/defaults/environments/resolve.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/resolve.json
rename to pype/settings/defaults/environments/resolve.json
diff --git a/pype/settings/defaults/system_settings/environments/storyboardpro.json b/pype/settings/defaults/environments/storyboardpro.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/storyboardpro.json
rename to pype/settings/defaults/environments/storyboardpro.json
diff --git a/pype/settings/defaults/system_settings/environments/unreal_4.24.json b/pype/settings/defaults/environments/unreal_4.24.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/unreal_4.24.json
rename to pype/settings/defaults/environments/unreal_4.24.json
diff --git a/pype/settings/defaults/system_settings/environments/vray_4300.json b/pype/settings/defaults/environments/vray_4300.json
similarity index 100%
rename from pype/settings/defaults/system_settings/environments/vray_4300.json
rename to pype/settings/defaults/environments/vray_4300.json
diff --git a/pype/settings/defaults/launchers/blender_2.80.toml b/pype/settings/defaults/launchers/blender_2.80.toml
new file mode 100644
index 0000000000..88b5ea0c11
--- /dev/null
+++ b/pype/settings/defaults/launchers/blender_2.80.toml
@@ -0,0 +1,8 @@
+application_dir = "blender"
+executable = "blender_2.80"
+schema = "avalon-core:application-1.0"
+label = "Blender"
+label_variant = "2.80"
+ftrack_label = "Blender"
+icon = "app_icons/blender.png"
+ftrack_icon = "{}/app_icons/blender.png"
diff --git a/pype/settings/defaults/launchers/blender_2.81.toml b/pype/settings/defaults/launchers/blender_2.81.toml
new file mode 100644
index 0000000000..072eaa8141
--- /dev/null
+++ b/pype/settings/defaults/launchers/blender_2.81.toml
@@ -0,0 +1,9 @@
+application_dir = "blender"
+executable = "blender_2.81"
+schema = "avalon-core:application-1.0"
+label = "Blender"
+label_variant = "2.81"
+icon = "app_icons/blender.png"
+
+ftrack_label = "Blender"
+ftrack_icon = '{}/app_icons/blender.png'
diff --git a/pype/settings/defaults/launchers/blender_2.82.toml b/pype/settings/defaults/launchers/blender_2.82.toml
new file mode 100644
index 0000000000..a485f790f1
--- /dev/null
+++ b/pype/settings/defaults/launchers/blender_2.82.toml
@@ -0,0 +1,9 @@
+application_dir = "blender"
+executable = "blender_2.82"
+schema = "avalon-core:application-1.0"
+label = "Blender"
+label_variant = "2.82"
+icon = "app_icons/blender.png"
+
+ftrack_label = "Blender"
+ftrack_icon = '{}/app_icons/blender.png'
diff --git a/pype/settings/defaults/launchers/blender_2.83.toml b/pype/settings/defaults/launchers/blender_2.83.toml
new file mode 100644
index 0000000000..0f98151d01
--- /dev/null
+++ b/pype/settings/defaults/launchers/blender_2.83.toml
@@ -0,0 +1,9 @@
+application_dir = "blender"
+executable = "blender_2.83"
+schema = "avalon-core:application-1.0"
+label = "Blender"
+label_variant = "2.83"
+icon = "app_icons/blender.png"
+
+ftrack_label = "Blender"
+ftrack_icon = '{}/app_icons/blender.png'
diff --git a/pype/settings/defaults/launchers/celaction_local.toml b/pype/settings/defaults/launchers/celaction_local.toml
new file mode 100644
index 0000000000..6cc5d4fa0e
--- /dev/null
+++ b/pype/settings/defaults/launchers/celaction_local.toml
@@ -0,0 +1,9 @@
+executable = "celaction_local"
+schema = "avalon-core:application-1.0"
+application_dir = "celaction"
+label = "CelAction2D"
+icon = "app_icons/celaction_local.png"
+launch_hook = "pype/hooks/celaction/prelaunch.py/CelactionPrelaunchHook"
+
+ftrack_label = "CelAction2D"
+ftrack_icon = '{}/app_icons/celaction_local.png'
diff --git a/pype/settings/defaults/launchers/celaction_publish.toml b/pype/settings/defaults/launchers/celaction_publish.toml
new file mode 100644
index 0000000000..dc7ac82673
--- /dev/null
+++ b/pype/settings/defaults/launchers/celaction_publish.toml
@@ -0,0 +1,8 @@
+schema = "avalon-core:application-1.0"
+application_dir = "shell"
+executable = "celaction_publish"
+label = "Celaction Shell"
+icon = "app_icons/celaction.png"
+
+[environment]
+CREATE_NEW_CONSOLE = "Yes"
diff --git a/pype/settings/defaults/launchers/darwin/blender_2.82 b/pype/settings/defaults/launchers/darwin/blender_2.82
new file mode 100644
index 0000000000..8254411ea2
--- /dev/null
+++ b/pype/settings/defaults/launchers/darwin/blender_2.82
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+open -a blender $@
diff --git a/pype/settings/defaults/launchers/darwin/harmony_17 b/pype/settings/defaults/launchers/darwin/harmony_17
new file mode 100644
index 0000000000..b7eba2c2d0
--- /dev/null
+++ b/pype/settings/defaults/launchers/darwin/harmony_17
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+DIRNAME="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+set >~/environment.tmp
+if [ $? -ne -0 ] ; then
+ echo "ERROR: cannot write to '~/environment.tmp'!"
+ read -n 1 -s -r -p "Press any key to exit"
+ return
+fi
+open -a Terminal.app "$DIRNAME/harmony_17_launch"
diff --git a/pype/settings/defaults/launchers/darwin/harmony_17_launch b/pype/settings/defaults/launchers/darwin/harmony_17_launch
new file mode 100644
index 0000000000..5dcf5db57e
--- /dev/null
+++ b/pype/settings/defaults/launchers/darwin/harmony_17_launch
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+source ~/environment.tmp
+export $(cut -d= -f1 ~/environment.tmp)
+exe="/Applications/Toon Boom Harmony 17 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium"
+$PYPE_PYTHON_EXE -c "import avalon.harmony;avalon.harmony.launch('$exe')"
diff --git a/pype/settings/defaults/launchers/darwin/python3 b/pype/settings/defaults/launchers/darwin/python3
new file mode 100644
index 0000000000..c2b82c7638
--- /dev/null
+++ b/pype/settings/defaults/launchers/darwin/python3
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+open /usr/bin/python3 --args $@
diff --git a/pype/settings/defaults/launchers/harmony_17.toml b/pype/settings/defaults/launchers/harmony_17.toml
new file mode 100644
index 0000000000..dd1c929b1b
--- /dev/null
+++ b/pype/settings/defaults/launchers/harmony_17.toml
@@ -0,0 +1,9 @@
+application_dir = "harmony"
+label = "Harmony"
+label_variant = "17"
+ftrack_label = "Harmony"
+schema = "avalon-core:application-1.0"
+executable = "harmony_17"
+description = ""
+icon = "app_icons/harmony.png"
+ftrack_icon = '{}/app_icons/harmony.png'
diff --git a/pype/settings/defaults/launchers/houdini_16.toml b/pype/settings/defaults/launchers/houdini_16.toml
new file mode 100644
index 0000000000..0a0876a264
--- /dev/null
+++ b/pype/settings/defaults/launchers/houdini_16.toml
@@ -0,0 +1,8 @@
+executable = "houdini_16"
+schema = "avalon-core:application-1.0"
+application_dir = "houdini"
+label = "Houdini"
+label_variant = "16"
+ftrack_label = "Houdini"
+icon = "app_icons/houdini.png"
+ftrack_icon = '{}/app_icons/houdini.png'
diff --git a/pype/settings/defaults/launchers/houdini_17.toml b/pype/settings/defaults/launchers/houdini_17.toml
new file mode 100644
index 0000000000..203f5cdb9b
--- /dev/null
+++ b/pype/settings/defaults/launchers/houdini_17.toml
@@ -0,0 +1,8 @@
+executable = "houdini_17"
+schema = "avalon-core:application-1.0"
+application_dir = "houdini"
+label = "Houdini"
+label_variant = "17"
+ftrack_label = "Houdini"
+icon = "app_icons/houdini.png"
+ftrack_icon = '{}/app_icons/houdini.png'
diff --git a/pype/settings/defaults/launchers/houdini_18.toml b/pype/settings/defaults/launchers/houdini_18.toml
new file mode 100644
index 0000000000..40f530c291
--- /dev/null
+++ b/pype/settings/defaults/launchers/houdini_18.toml
@@ -0,0 +1,8 @@
+executable = "houdini_18"
+schema = "avalon-core:application-1.0"
+application_dir = "houdini"
+label = "Houdini"
+label_variant = "18"
+ftrack_label = "Houdini"
+icon = "app_icons/houdini.png"
+ftrack_icon = '{}/app_icons/houdini.png'
diff --git a/pype/settings/defaults/launchers/linux/maya2016 b/pype/settings/defaults/launchers/linux/maya2016
new file mode 100644
index 0000000000..98424304b1
--- /dev/null
+++ b/pype/settings/defaults/launchers/linux/maya2016
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+maya_path = "/usr/autodesk/maya2016/bin/maya"
+
+if [[ -z $PYPE_LOG_NO_COLORS ]]; then
+ $maya_path -file "$AVALON_LAST_WORKFILE" $@
+else
+ $maya_path $@
diff --git a/pype/settings/defaults/launchers/linux/maya2017 b/pype/settings/defaults/launchers/linux/maya2017
new file mode 100644
index 0000000000..7a2662a55e
--- /dev/null
+++ b/pype/settings/defaults/launchers/linux/maya2017
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+maya_path = "/usr/autodesk/maya2017/bin/maya"
+
+if [[ -z $AVALON_LAST_WORKFILE ]]; then
+ $maya_path -file "$AVALON_LAST_WORKFILE" $@
+else
+ $maya_path $@
diff --git a/pype/settings/defaults/launchers/linux/maya2018 b/pype/settings/defaults/launchers/linux/maya2018
new file mode 100644
index 0000000000..db832b3fe7
--- /dev/null
+++ b/pype/settings/defaults/launchers/linux/maya2018
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+maya_path = "/usr/autodesk/maya2018/bin/maya"
+
+if [[ -z $AVALON_LAST_WORKFILE ]]; then
+ $maya_path -file "$AVALON_LAST_WORKFILE" $@
+else
+ $maya_path $@
diff --git a/pype/settings/defaults/launchers/linux/maya2019 b/pype/settings/defaults/launchers/linux/maya2019
new file mode 100644
index 0000000000..8398734ab9
--- /dev/null
+++ b/pype/settings/defaults/launchers/linux/maya2019
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+maya_path = "/usr/autodesk/maya2019/bin/maya"
+
+if [[ -z $AVALON_LAST_WORKFILE ]]; then
+ $maya_path -file "$AVALON_LAST_WORKFILE" $@
+else
+ $maya_path $@
diff --git a/pype/settings/defaults/launchers/linux/maya2020 b/pype/settings/defaults/launchers/linux/maya2020
new file mode 100644
index 0000000000..18a1edd598
--- /dev/null
+++ b/pype/settings/defaults/launchers/linux/maya2020
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+maya_path = "/usr/autodesk/maya2020/bin/maya"
+
+if [[ -z $AVALON_LAST_WORKFILE ]]; then
+ $maya_path -file "$AVALON_LAST_WORKFILE" $@
+else
+ $maya_path $@
diff --git a/pype/settings/defaults/launchers/linux/nuke11.3 b/pype/settings/defaults/launchers/linux/nuke11.3
new file mode 100644
index 0000000000..b1c9a90d74
--- /dev/null
+++ b/pype/settings/defaults/launchers/linux/nuke11.3
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+gnome-terminal -e '/usr/local/Nuke11.3v5/Nuke11.3'
diff --git a/pype/settings/defaults/launchers/linux/nuke12.0 b/pype/settings/defaults/launchers/linux/nuke12.0
new file mode 100644
index 0000000000..99ea1a6b0c
--- /dev/null
+++ b/pype/settings/defaults/launchers/linux/nuke12.0
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+gnome-terminal -e '/usr/local/Nuke12.0v1/Nuke12.0'
diff --git a/pype/settings/defaults/launchers/linux/nukestudio11.3 b/pype/settings/defaults/launchers/linux/nukestudio11.3
new file mode 100644
index 0000000000..750d54a7d5
--- /dev/null
+++ b/pype/settings/defaults/launchers/linux/nukestudio11.3
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+gnome-terminal -e '/usr/local/Nuke11.3v5/Nuke11.3 --studio'
diff --git a/pype/settings/defaults/launchers/linux/nukestudio12.0 b/pype/settings/defaults/launchers/linux/nukestudio12.0
new file mode 100644
index 0000000000..ba5cf654a8
--- /dev/null
+++ b/pype/settings/defaults/launchers/linux/nukestudio12.0
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+gnome-terminal -e '/usr/local/Nuke12.0v1/Nuke12.0 --studio'
diff --git a/pype/settings/defaults/launchers/linux/nukex11.3 b/pype/settings/defaults/launchers/linux/nukex11.3
new file mode 100644
index 0000000000..d913e4b961
--- /dev/null
+++ b/pype/settings/defaults/launchers/linux/nukex11.3
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+gnome-terminal -e '/usr/local/Nuke11.3v5/Nuke11.3 -nukex'
diff --git a/pype/settings/defaults/launchers/linux/nukex12.0 b/pype/settings/defaults/launchers/linux/nukex12.0
new file mode 100644
index 0000000000..da2721c48b
--- /dev/null
+++ b/pype/settings/defaults/launchers/linux/nukex12.0
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+gnome-terminal -e '/usr/local/Nuke12.0v1/Nuke12.0 -nukex'
diff --git a/pype/settings/defaults/launchers/maya_2016.toml b/pype/settings/defaults/launchers/maya_2016.toml
new file mode 100644
index 0000000000..24a463d9c6
--- /dev/null
+++ b/pype/settings/defaults/launchers/maya_2016.toml
@@ -0,0 +1,27 @@
+application_dir = "maya"
+default_dirs = [
+ "scenes",
+ "data",
+ "renderData/shaders",
+ "images"
+]
+label = "Autodesk Maya"
+label_variant = "2016"
+ftrack_label = "Maya"
+schema = "avalon-core:application-1.0"
+executable = "maya2016"
+description = ""
+icon = "app_icons/maya.png"
+ftrack_icon = '{}/app_icons/maya.png'
+
+[copy]
+"{PYPE_MODULE_ROOT}/pype/resources/maya/workspace.mel" = "workspace.mel"
+
+[environment]
+MAYA_DISABLE_CLIC_IPM = "Yes" # Disable the AdSSO process
+MAYA_DISABLE_CIP = "Yes" # Shorten time to boot
+MAYA_DISABLE_CER = "Yes"
+PYTHONPATH = [
+ "{AVALON_CORE}/setup/maya",
+ "{PYTHONPATH}"
+]
diff --git a/pype/settings/defaults/launchers/maya_2017.toml b/pype/settings/defaults/launchers/maya_2017.toml
new file mode 100644
index 0000000000..5295862e87
--- /dev/null
+++ b/pype/settings/defaults/launchers/maya_2017.toml
@@ -0,0 +1,29 @@
+application_dir = "maya"
+default_dirs = [
+ "scenes",
+ "data",
+ "renderData/shaders",
+ "images"
+]
+label = "Autodesk Maya"
+label_variant = "2017"
+ftrack_label = "Maya"
+schema = "avalon-core:application-1.0"
+executable = "maya2017"
+description = ""
+icon = "app_icons/maya.png"
+ftrack_icon = '{}/app_icons/maya.png'
+
+[copy]
+"{PYPE_MODULE_ROOT}/pype/resources/maya/workspace.mel" = "workspace.mel"
+
+[environment]
+MAYA_DISABLE_CLIC_IPM = "Yes" # Disable the AdSSO process
+MAYA_DISABLE_CIP = "Yes" # Shorten time to boot
+MAYA_DISABLE_CER = "Yes"
+PYMEL_SKIP_MEL_INIT = "Yes"
+LC_ALL= "C" # Mute color management warnings
+PYTHONPATH = [
+ "{AVALON_CORE}/setup/maya",
+ "{PYTHONPATH}"
+]
diff --git a/pype/settings/defaults/launchers/maya_2018.toml b/pype/settings/defaults/launchers/maya_2018.toml
new file mode 100644
index 0000000000..2bdff2094d
--- /dev/null
+++ b/pype/settings/defaults/launchers/maya_2018.toml
@@ -0,0 +1,15 @@
+application_dir = "maya"
+default_dirs = [
+ "renders"
+]
+label = "Autodesk Maya"
+label_variant = "2018"
+ftrack_label = "Maya"
+schema = "avalon-core:application-1.0"
+executable = "maya2018"
+description = ""
+icon = "app_icons/maya.png"
+ftrack_icon = '{}/app_icons/maya.png'
+
+[copy]
+"{PYPE_MODULE_ROOT}/pype/resources/maya/workspace.mel" = "workspace.mel"
diff --git a/pype/settings/defaults/launchers/maya_2019.toml b/pype/settings/defaults/launchers/maya_2019.toml
new file mode 100644
index 0000000000..8eb88179f9
--- /dev/null
+++ b/pype/settings/defaults/launchers/maya_2019.toml
@@ -0,0 +1,15 @@
+application_dir = "maya"
+default_dirs = [
+ "renders"
+]
+label = "Autodesk Maya"
+label_variant = "2019"
+ftrack_label = "Maya"
+schema = "avalon-core:application-1.0"
+executable = "maya2019"
+description = ""
+icon = "app_icons/maya.png"
+ftrack_icon = '{}/app_icons/maya.png'
+
+[copy]
+"{PYPE_MODULE_ROOT}/pype/resources/maya/workspace.mel" = "workspace.mel"
diff --git a/pype/settings/defaults/launchers/maya_2020.toml b/pype/settings/defaults/launchers/maya_2020.toml
new file mode 100644
index 0000000000..693de0cf9e
--- /dev/null
+++ b/pype/settings/defaults/launchers/maya_2020.toml
@@ -0,0 +1,15 @@
+application_dir = "maya"
+default_dirs = [
+ "renders"
+]
+label = "Autodesk Maya"
+label_variant = "2020"
+ftrack_label = "Maya"
+schema = "avalon-core:application-1.0"
+executable = "maya2020"
+description = ""
+icon = "app_icons/maya.png"
+ftrack_icon = '{}/app_icons/maya.png'
+
+[copy]
+"{PYPE_MODULE_ROOT}/pype/resources/maya/workspace.mel" = "workspace.mel"
diff --git a/pype/settings/defaults/launchers/mayabatch_2019.toml b/pype/settings/defaults/launchers/mayabatch_2019.toml
new file mode 100644
index 0000000000..a928618d2b
--- /dev/null
+++ b/pype/settings/defaults/launchers/mayabatch_2019.toml
@@ -0,0 +1,17 @@
+application_dir = "maya"
+default_dirs = [
+ "scenes",
+ "data",
+ "renderData/shaders",
+ "images"
+]
+label = "Autodesk Maya 2019x64"
+schema = "avalon-core:application-1.0"
+executable = "mayabatch2019"
+description = ""
+
+[environment]
+PYTHONPATH = [
+ "{AVALON_CORE}/setup/maya",
+ "{PYTHONPATH}"
+]
diff --git a/pype/settings/defaults/launchers/mayabatch_2020.toml b/pype/settings/defaults/launchers/mayabatch_2020.toml
new file mode 100644
index 0000000000..cd1e1e4474
--- /dev/null
+++ b/pype/settings/defaults/launchers/mayabatch_2020.toml
@@ -0,0 +1,17 @@
+application_dir = "maya"
+default_dirs = [
+ "scenes",
+ "data",
+ "renderData/shaders",
+ "images"
+]
+label = "Autodesk Maya 2020x64"
+schema = "avalon-core:application-1.0"
+executable = "mayabatch2020"
+description = ""
+
+[environment]
+PYTHONPATH = [
+ "{AVALON_CORE}/setup/maya",
+ "{PYTHONPATH}"
+]
diff --git a/pype/settings/defaults/launchers/mayapy2016.toml b/pype/settings/defaults/launchers/mayapy2016.toml
new file mode 100644
index 0000000000..ad1e3dee86
--- /dev/null
+++ b/pype/settings/defaults/launchers/mayapy2016.toml
@@ -0,0 +1,17 @@
+application_dir = "maya"
+default_dirs = [
+ "scenes",
+ "data",
+ "renderData/shaders",
+ "images"
+]
+label = "Autodesk Maya 2016x64"
+schema = "avalon-core:application-1.0"
+executable = "mayapy2016"
+description = ""
+
+[environment]
+PYTHONPATH = [
+ "{AVALON_CORE}/setup/maya",
+ "{PYTHONPATH}"
+]
diff --git a/pype/settings/defaults/launchers/mayapy2017.toml b/pype/settings/defaults/launchers/mayapy2017.toml
new file mode 100644
index 0000000000..8d2095ff47
--- /dev/null
+++ b/pype/settings/defaults/launchers/mayapy2017.toml
@@ -0,0 +1,17 @@
+application_dir = "maya"
+default_dirs = [
+ "scenes",
+ "data",
+ "renderData/shaders",
+ "images"
+]
+label = "Autodesk Maya 2017x64"
+schema = "avalon-core:application-1.0"
+executable = "mayapy2017"
+description = ""
+
+[environment]
+PYTHONPATH = [
+ "{AVALON_CORE}/setup/maya",
+ "{PYTHONPATH}"
+]
diff --git a/pype/settings/defaults/launchers/mayapy2018.toml b/pype/settings/defaults/launchers/mayapy2018.toml
new file mode 100644
index 0000000000..597744fd85
--- /dev/null
+++ b/pype/settings/defaults/launchers/mayapy2018.toml
@@ -0,0 +1,17 @@
+application_dir = "maya"
+default_dirs = [
+ "scenes",
+ "data",
+ "renderData/shaders",
+ "images"
+]
+label = "Autodesk Maya 2018x64"
+schema = "avalon-core:application-1.0"
+executable = "mayapy2017"
+description = ""
+
+[environment]
+PYTHONPATH = [
+ "{AVALON_CORE}/setup/maya",
+ "{PYTHONPATH}"
+]
diff --git a/pype/settings/defaults/launchers/mayapy2019.toml b/pype/settings/defaults/launchers/mayapy2019.toml
new file mode 100644
index 0000000000..3c8a9860f9
--- /dev/null
+++ b/pype/settings/defaults/launchers/mayapy2019.toml
@@ -0,0 +1,17 @@
+application_dir = "maya"
+default_dirs = [
+ "scenes",
+ "data",
+ "renderData/shaders",
+ "images"
+]
+label = "Autodesk Maya 2019x64"
+schema = "avalon-core:application-1.0"
+executable = "mayapy2019"
+description = ""
+
+[environment]
+PYTHONPATH = [
+ "{AVALON_CORE}/setup/maya",
+ "{PYTHONPATH}"
+]
diff --git a/pype/settings/defaults/launchers/mayapy2020.toml b/pype/settings/defaults/launchers/mayapy2020.toml
new file mode 100644
index 0000000000..8f2d2e4a67
--- /dev/null
+++ b/pype/settings/defaults/launchers/mayapy2020.toml
@@ -0,0 +1,17 @@
+application_dir = "maya"
+default_dirs = [
+ "scenes",
+ "data",
+ "renderData/shaders",
+ "images"
+]
+label = "Autodesk Maya 2020x64"
+schema = "avalon-core:application-1.0"
+executable = "mayapy2020"
+description = ""
+
+[environment]
+PYTHONPATH = [
+ "{AVALON_CORE}/setup/maya",
+ "{PYTHONPATH}"
+]
diff --git a/pype/settings/defaults/launchers/myapp.toml b/pype/settings/defaults/launchers/myapp.toml
new file mode 100644
index 0000000000..21da0d52b2
--- /dev/null
+++ b/pype/settings/defaults/launchers/myapp.toml
@@ -0,0 +1,5 @@
+executable = "python"
+schema = "avalon-core:application-1.0"
+application_dir = "myapp"
+label = "My App"
+arguments = [ "-c", "import sys; from Qt import QtWidgets; if __name__ == '__main__':;\n app = QtWidgets.QApplication(sys.argv);\n window = QtWidgets.QWidget();\n window.setWindowTitle(\"My App\");\n window.resize(400, 300);\n window.show();\n app.exec_();\n",]
\ No newline at end of file
diff --git a/pype/settings/defaults/launchers/nuke_10.0.toml b/pype/settings/defaults/launchers/nuke_10.0.toml
new file mode 100644
index 0000000000..d4dd028942
--- /dev/null
+++ b/pype/settings/defaults/launchers/nuke_10.0.toml
@@ -0,0 +1,8 @@
+executable = "nuke10.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "Nuke"
+label_variant = "10.0v4"
+ftrack_label = "Nuke"
+icon = "app_icons/nuke.png"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/launchers/nuke_11.0.toml b/pype/settings/defaults/launchers/nuke_11.0.toml
new file mode 100644
index 0000000000..10ff6aca37
--- /dev/null
+++ b/pype/settings/defaults/launchers/nuke_11.0.toml
@@ -0,0 +1,8 @@
+executable = "nuke11.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "Nuke"
+label_variant = "11.0"
+ftrack_label = "Nuke"
+icon = "app_icons/nuke.png"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/launchers/nuke_11.2.toml b/pype/settings/defaults/launchers/nuke_11.2.toml
new file mode 100644
index 0000000000..530c7f610e
--- /dev/null
+++ b/pype/settings/defaults/launchers/nuke_11.2.toml
@@ -0,0 +1,8 @@
+executable = "nuke11.2"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "Nuke"
+label_variant = "11.2"
+ftrack_label = "Nuke"
+icon = "app_icons/nuke.png"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/launchers/nuke_11.3.toml b/pype/settings/defaults/launchers/nuke_11.3.toml
new file mode 100644
index 0000000000..c9ff005feb
--- /dev/null
+++ b/pype/settings/defaults/launchers/nuke_11.3.toml
@@ -0,0 +1,8 @@
+executable = "nuke11.3"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "Nuke"
+label_variant = "11.3"
+ftrack_label = "Nuke"
+icon = "app_icons/nuke.png"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/launchers/nuke_12.0.toml b/pype/settings/defaults/launchers/nuke_12.0.toml
new file mode 100644
index 0000000000..9ac1084fbf
--- /dev/null
+++ b/pype/settings/defaults/launchers/nuke_12.0.toml
@@ -0,0 +1,8 @@
+executable = "nuke12.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "Nuke"
+label_variant = "12.0"
+ftrack_label = "Nuke"
+icon = "app_icons/nuke.png"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/launchers/nukestudio_10.0.toml b/pype/settings/defaults/launchers/nukestudio_10.0.toml
new file mode 100644
index 0000000000..6c554aff62
--- /dev/null
+++ b/pype/settings/defaults/launchers/nukestudio_10.0.toml
@@ -0,0 +1,8 @@
+executable = "nukestudio10.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nukestudio"
+label = "NukeStudio"
+label_variant = "10.0"
+ftrack_label = "NukeStudio"
+icon = "app_icons/nuke.png"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/launchers/nukestudio_11.0.toml b/pype/settings/defaults/launchers/nukestudio_11.0.toml
new file mode 100644
index 0000000000..482aa6587e
--- /dev/null
+++ b/pype/settings/defaults/launchers/nukestudio_11.0.toml
@@ -0,0 +1,8 @@
+executable = "nukestudio11.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nukestudio"
+label = "NukeStudio"
+label_variant = "11.0"
+ftrack_label = "NukeStudio"
+icon = "app_icons/nuke.png"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/launchers/nukestudio_11.2.toml b/pype/settings/defaults/launchers/nukestudio_11.2.toml
new file mode 100644
index 0000000000..78d1de3d8b
--- /dev/null
+++ b/pype/settings/defaults/launchers/nukestudio_11.2.toml
@@ -0,0 +1,8 @@
+executable = "nukestudio11.2"
+schema = "avalon-core:application-1.0"
+application_dir = "nukestudio"
+label = "NukeStudio"
+label_variant = "11.2"
+ftrack_label = "NukeStudio"
+icon = "app_icons/nuke.png"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/launchers/nukestudio_11.3.toml b/pype/settings/defaults/launchers/nukestudio_11.3.toml
new file mode 100644
index 0000000000..35c6a08b2f
--- /dev/null
+++ b/pype/settings/defaults/launchers/nukestudio_11.3.toml
@@ -0,0 +1,8 @@
+executable = "nukestudio11.3"
+schema = "avalon-core:application-1.0"
+application_dir = "nukestudio"
+label = "NukeStudio"
+label_variant = "11.3"
+ftrack_label = "NukeStudio"
+icon = "app_icons/nuke.png"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/launchers/nukestudio_12.0.toml b/pype/settings/defaults/launchers/nukestudio_12.0.toml
new file mode 100644
index 0000000000..2754116aef
--- /dev/null
+++ b/pype/settings/defaults/launchers/nukestudio_12.0.toml
@@ -0,0 +1,8 @@
+executable = "nukestudio12.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nukestudio"
+label = "NukeStudio"
+label_variant = "12.0"
+ftrack_label = "NukeStudio"
+icon = "app_icons/nuke.png"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/launchers/nukex_10.0.toml b/pype/settings/defaults/launchers/nukex_10.0.toml
new file mode 100644
index 0000000000..48da30fe16
--- /dev/null
+++ b/pype/settings/defaults/launchers/nukex_10.0.toml
@@ -0,0 +1,8 @@
+executable = "nukex10.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "NukeX"
+label_variant = "10.0"
+ftrack_label = "NukeX"
+icon = "app_icons/nuke.png"
+ftrack_icon = '{}/app_icons/nukex.png'
diff --git a/pype/settings/defaults/launchers/nukex_11.0.toml b/pype/settings/defaults/launchers/nukex_11.0.toml
new file mode 100644
index 0000000000..8f353e9e00
--- /dev/null
+++ b/pype/settings/defaults/launchers/nukex_11.0.toml
@@ -0,0 +1,8 @@
+executable = "nukex11.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "NukeX"
+label_variant = "11.0"
+ftrack_label = "NukeX"
+icon = "app_icons/nuke.png"
+ftrack_icon = '{}/app_icons/nukex.png'
diff --git a/pype/settings/defaults/launchers/nukex_11.2.toml b/pype/settings/defaults/launchers/nukex_11.2.toml
new file mode 100644
index 0000000000..38e37fa4c9
--- /dev/null
+++ b/pype/settings/defaults/launchers/nukex_11.2.toml
@@ -0,0 +1,8 @@
+executable = "nukex11.2"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "NukeX"
+label_variant = "11.2"
+ftrack_label = "NukeX"
+icon = "app_icons/nuke.png"
+ftrack_icon = '{}/app_icons/nukex.png'
diff --git a/pype/settings/defaults/launchers/nukex_11.3.toml b/pype/settings/defaults/launchers/nukex_11.3.toml
new file mode 100644
index 0000000000..42969c5e69
--- /dev/null
+++ b/pype/settings/defaults/launchers/nukex_11.3.toml
@@ -0,0 +1,8 @@
+executable = "nukex11.3"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "NukeX"
+label_variant = "11.3"
+ftrack_label = "NukeX"
+icon = "app_icons/nuke.png"
+ftrack_icon = '{}/app_icons/nukex.png'
diff --git a/pype/settings/defaults/launchers/nukex_12.0.toml b/pype/settings/defaults/launchers/nukex_12.0.toml
new file mode 100644
index 0000000000..19d27a12d7
--- /dev/null
+++ b/pype/settings/defaults/launchers/nukex_12.0.toml
@@ -0,0 +1,8 @@
+executable = "nukex12.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "NukeX"
+label_variant = "12.0"
+ftrack_label = "NukeX"
+icon = "app_icons/nuke.png"
+ftrack_icon = '{}/app_icons/nukex.png'
diff --git a/pype/settings/defaults/launchers/photoshop_2020.toml b/pype/settings/defaults/launchers/photoshop_2020.toml
new file mode 100644
index 0000000000..8164af929f
--- /dev/null
+++ b/pype/settings/defaults/launchers/photoshop_2020.toml
@@ -0,0 +1,9 @@
+executable = "photoshop_2020"
+schema = "avalon-core:application-1.0"
+application_dir = "photoshop"
+label = "Adobe Photoshop"
+label_variant = "2020"
+icon = "app_icons/photoshop.png"
+ftrack_label = "Photoshop"
+ftrack_icon = '{}/app_icons/photoshop.png'
+launch_hook = "pype/hooks/photoshop/prelaunch.py/PhotoshopPrelaunch"
diff --git a/pype/settings/defaults/launchers/premiere_2019.toml b/pype/settings/defaults/launchers/premiere_2019.toml
new file mode 100644
index 0000000000..d03395e022
--- /dev/null
+++ b/pype/settings/defaults/launchers/premiere_2019.toml
@@ -0,0 +1,9 @@
+executable = "premiere_pro_2019"
+schema = "avalon-core:application-1.0"
+application_dir = "premiere"
+label = "Adobe Premiere Pro CC"
+label_variant = "2019"
+icon = "app_icons/premiere.png"
+
+ftrack_label = "Premiere"
+ftrack_icon = '{}/app_icons/premiere.png'
diff --git a/pype/settings/defaults/launchers/premiere_2020.toml b/pype/settings/defaults/launchers/premiere_2020.toml
new file mode 100644
index 0000000000..01c7b5b745
--- /dev/null
+++ b/pype/settings/defaults/launchers/premiere_2020.toml
@@ -0,0 +1,10 @@
+executable = "premiere_pro_2020"
+schema = "avalon-core:application-1.0"
+application_dir = "premiere"
+label = "Adobe Premiere Pro CC"
+label_variant = "2020"
+launch_hook = "pype/hooks/premiere/prelaunch.py/PremierePrelaunch"
+icon = "app_icons/premiere.png"
+
+ftrack_label = "Premiere"
+ftrack_icon = '{}/app_icons/premiere.png'
diff --git a/pype/settings/defaults/launchers/python_2.toml b/pype/settings/defaults/launchers/python_2.toml
new file mode 100644
index 0000000000..f1c1ca7e68
--- /dev/null
+++ b/pype/settings/defaults/launchers/python_2.toml
@@ -0,0 +1,12 @@
+schema = "avalon-core:application-1.0"
+application_dir = "python"
+executable = "python"
+label = "Python"
+label_variant = "2"
+icon = "app_icons/python.png"
+
+ftrack_label = "Python"
+ftrack_icon = '{}/app_icons/python.png'
+
+[environment]
+CREATE_NEW_CONSOLE = "Yes"
diff --git a/pype/settings/defaults/launchers/python_3.toml b/pype/settings/defaults/launchers/python_3.toml
new file mode 100644
index 0000000000..90fb10eaeb
--- /dev/null
+++ b/pype/settings/defaults/launchers/python_3.toml
@@ -0,0 +1,12 @@
+schema = "avalon-core:application-1.0"
+application_dir = "python"
+executable = "python3"
+label = "Python"
+label_variant = "3"
+icon = "app_icons/python.png"
+
+ftrack_label = "Python"
+ftrack_icon = '{}/app_icons/python.png'
+
+[environment]
+CREATE_NEW_CONSOLE = "Yes"
diff --git a/pype/settings/defaults/launchers/resolve_16.toml b/pype/settings/defaults/launchers/resolve_16.toml
new file mode 100644
index 0000000000..47918a22a6
--- /dev/null
+++ b/pype/settings/defaults/launchers/resolve_16.toml
@@ -0,0 +1,10 @@
+executable = "resolve_16"
+schema = "avalon-core:application-1.0"
+application_dir = "resolve"
+label = "BM DaVinci Resolve"
+label_variant = "16"
+launch_hook = "pype/hooks/resolve/prelaunch.py/ResolvePrelaunch"
+icon = "app_icons/resolve.png"
+
+ftrack_label = "BM DaVinci Resolve"
+ftrack_icon = '{}/app_icons/resolve.png'
diff --git a/pype/settings/defaults/launchers/shell.toml b/pype/settings/defaults/launchers/shell.toml
new file mode 100644
index 0000000000..959ad392ea
--- /dev/null
+++ b/pype/settings/defaults/launchers/shell.toml
@@ -0,0 +1,7 @@
+schema = "avalon-core:application-1.0"
+application_dir = "shell"
+executable = "shell"
+label = "Shell"
+
+[environment]
+CREATE_NEW_CONSOLE = "Yes"
\ No newline at end of file
diff --git a/pype/settings/defaults/launchers/storyboardpro_7.toml b/pype/settings/defaults/launchers/storyboardpro_7.toml
new file mode 100644
index 0000000000..067f10a23a
--- /dev/null
+++ b/pype/settings/defaults/launchers/storyboardpro_7.toml
@@ -0,0 +1,9 @@
+application_dir = "storyboardpro"
+label = "Storyboard Pro"
+label_variant = "7"
+ftrack_label = "Storyboard Pro"
+schema = "avalon-core:application-1.0"
+executable = "storyboardpro_7"
+description = ""
+icon = "app_icons/storyboardpro.png"
+ftrack_icon = '{}/app_icons/storyboardpro.png'
diff --git a/pype/settings/defaults/launchers/unreal_4.24.toml b/pype/settings/defaults/launchers/unreal_4.24.toml
new file mode 100644
index 0000000000..10b14e7f59
--- /dev/null
+++ b/pype/settings/defaults/launchers/unreal_4.24.toml
@@ -0,0 +1,10 @@
+executable = "unreal"
+schema = "avalon-core:application-1.0"
+application_dir = "unreal"
+label = "Unreal Editor"
+label_variant = "4.24"
+icon = "app_icons/ue4.png"
+launch_hook = "pype/hooks/unreal/unreal_prelaunch.py/UnrealPrelaunch"
+
+ftrack_label = "UnrealEditor"
+ftrack_icon = '{}/app_icons/ue4.png'
diff --git a/pype/settings/defaults/launchers/windows/blender_2.80.bat b/pype/settings/defaults/launchers/windows/blender_2.80.bat
new file mode 100644
index 0000000000..5b8a37356b
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/blender_2.80.bat
@@ -0,0 +1,11 @@
+set __app__="Blender"
+set __exe__="C:\Program Files\Blender Foundation\Blender 2.80\blender.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/blender_2.81.bat b/pype/settings/defaults/launchers/windows/blender_2.81.bat
new file mode 100644
index 0000000000..a900b18eda
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/blender_2.81.bat
@@ -0,0 +1,11 @@
+set __app__="Blender"
+set __exe__="C:\Program Files\Blender Foundation\Blender 2.81\blender.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/blender_2.82.bat b/pype/settings/defaults/launchers/windows/blender_2.82.bat
new file mode 100644
index 0000000000..7105c1efe1
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/blender_2.82.bat
@@ -0,0 +1,11 @@
+set __app__="Blender"
+set __exe__="C:\Program Files\Blender Foundation\Blender 2.82\blender.exe" --python-use-system-env
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/blender_2.83.bat b/pype/settings/defaults/launchers/windows/blender_2.83.bat
new file mode 100644
index 0000000000..671952f0d7
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/blender_2.83.bat
@@ -0,0 +1,11 @@
+set __app__="Blender"
+set __exe__="C:\Program Files\Blender Foundation\Blender 2.83\blender.exe" --python-use-system-env
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/celaction_local.bat b/pype/settings/defaults/launchers/windows/celaction_local.bat
new file mode 100644
index 0000000000..8f2171617e
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/celaction_local.bat
@@ -0,0 +1,19 @@
+set __app__="CelAction2D"
+set __app_dir__="C:\Program Files (x86)\CelAction\"
+set __exe__="C:\Program Files (x86)\CelAction\CelAction2D.exe"
+
+if not exist %__exe__% goto :missing_app
+
+pushd %__app_dir__%
+
+if "%PYPE_CELACTION_PROJECT_FILE%"=="" (
+ start %__app__% %__exe__% %*
+) else (
+ start %__app__% %__exe__% "%PYPE_CELACTION_PROJECT_FILE%" %*
+)
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/celaction_publish.bat b/pype/settings/defaults/launchers/windows/celaction_publish.bat
new file mode 100644
index 0000000000..77ec2ac24e
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/celaction_publish.bat
@@ -0,0 +1,3 @@
+echo %*
+
+%PYPE_PYTHON_EXE% "%PYPE_MODULE_ROOT%\pype\hosts\celaction\cli.py" %*
diff --git a/pype/settings/defaults/launchers/windows/harmony_17.bat b/pype/settings/defaults/launchers/windows/harmony_17.bat
new file mode 100644
index 0000000000..0822650875
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/harmony_17.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Harmony 17"
+set __exe__="C:/Program Files (x86)/Toon Boom Animation/Toon Boom Harmony 17 Premium/win64/bin/HarmonyPremium.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% cmd.exe /k "python -c ^"import avalon.harmony;avalon.harmony.launch("%__exe__%")^""
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/houdini_16.bat b/pype/settings/defaults/launchers/windows/houdini_16.bat
new file mode 100644
index 0000000000..018ba08b4c
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/houdini_16.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Houdini 16.0"
+set __exe__="C:\Program Files\Side Effects Software\Houdini 16.0.621\bin\houdini.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/houdini_17.bat b/pype/settings/defaults/launchers/windows/houdini_17.bat
new file mode 100644
index 0000000000..950a599623
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/houdini_17.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Houdini 17.0"
+set __exe__="C:\Program Files\Side Effects Software\Houdini 17.0.459\bin\houdini.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/houdini_18.bat b/pype/settings/defaults/launchers/windows/houdini_18.bat
new file mode 100644
index 0000000000..3d6b1ae258
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/houdini_18.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Houdini 18.0"
+set __exe__="C:\Program Files\Side Effects Software\Houdini 18.0.287\bin\houdini.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/maya2016.bat b/pype/settings/defaults/launchers/windows/maya2016.bat
new file mode 100644
index 0000000000..54f15cf269
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/maya2016.bat
@@ -0,0 +1,17 @@
+@echo off
+
+set __app__="Maya 2016"
+set __exe__="C:\Program Files\Autodesk\Maya2016\bin\maya.exe"
+if not exist %__exe__% goto :missing_app
+
+if "%AVALON_LAST_WORKFILE%"=="" (
+ start %__app__% %__exe__% %*
+) else (
+ start %__app__% %__exe__% -file "%AVALON_LAST_WORKFILE%" %*
+)
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/maya2017.bat b/pype/settings/defaults/launchers/windows/maya2017.bat
new file mode 100644
index 0000000000..5c2aeb495c
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/maya2017.bat
@@ -0,0 +1,17 @@
+@echo off
+
+set __app__="Maya 2017"
+set __exe__="C:\Program Files\Autodesk\Maya2017\bin\maya.exe"
+if not exist %__exe__% goto :missing_app
+
+if "%AVALON_LAST_WORKFILE%"=="" (
+ start %__app__% %__exe__% %*
+) else (
+ start %__app__% %__exe__% -file "%AVALON_LAST_WORKFILE%" %*
+)
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/maya2018.bat b/pype/settings/defaults/launchers/windows/maya2018.bat
new file mode 100644
index 0000000000..28cf776c77
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/maya2018.bat
@@ -0,0 +1,17 @@
+@echo off
+
+set __app__="Maya 2018"
+set __exe__="C:\Program Files\Autodesk\Maya2018\bin\maya.exe"
+if not exist %__exe__% goto :missing_app
+
+if "%AVALON_LAST_WORKFILE%"=="" (
+ start %__app__% %__exe__% %*
+) else (
+ start %__app__% %__exe__% -file "%AVALON_LAST_WORKFILE%" %*
+)
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/maya2019.bat b/pype/settings/defaults/launchers/windows/maya2019.bat
new file mode 100644
index 0000000000..7e80dd2557
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/maya2019.bat
@@ -0,0 +1,17 @@
+@echo off
+
+set __app__="Maya 2019"
+set __exe__="C:\Program Files\Autodesk\Maya2019\bin\maya.exe"
+if not exist %__exe__% goto :missing_app
+
+if "%AVALON_LAST_WORKFILE%"=="" (
+ start %__app__% %__exe__% %*
+) else (
+ start %__app__% %__exe__% -file "%AVALON_LAST_WORKFILE%" %*
+)
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/maya2020.bat b/pype/settings/defaults/launchers/windows/maya2020.bat
new file mode 100644
index 0000000000..b2acb5df5a
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/maya2020.bat
@@ -0,0 +1,17 @@
+@echo off
+
+set __app__="Maya 2020"
+set __exe__="C:\Program Files\Autodesk\maya2020\bin\maya.exe"
+if not exist %__exe__% goto :missing_app
+
+if "%AVALON_LAST_WORKFILE%"=="" (
+ start %__app__% %__exe__% %*
+) else (
+ start %__app__% %__exe__% -file "%AVALON_LAST_WORKFILE%" %*
+)
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/mayabatch2019.bat b/pype/settings/defaults/launchers/windows/mayabatch2019.bat
new file mode 100644
index 0000000000..ddd9b9b956
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/mayabatch2019.bat
@@ -0,0 +1,14 @@
+@echo off
+
+set __app__="Maya Batch 2019"
+set __exe__="C:\Program Files\Autodesk\Maya2019\bin\mayabatch.exe"
+if not exist %__exe__% goto :missing_app
+
+echo "running maya : %*"
+%__exe__% %*
+echo "done."
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/mayabatch2020.bat b/pype/settings/defaults/launchers/windows/mayabatch2020.bat
new file mode 100644
index 0000000000..b1cbc6dbb6
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/mayabatch2020.bat
@@ -0,0 +1,14 @@
+@echo off
+
+set __app__="Maya Batch 2020"
+set __exe__="C:\Program Files\Autodesk\Maya2020\bin\mayabatch.exe"
+if not exist %__exe__% goto :missing_app
+
+echo "running maya : %*"
+%__exe__% %*
+echo "done."
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/mayapy2016.bat b/pype/settings/defaults/launchers/windows/mayapy2016.bat
new file mode 100644
index 0000000000..205991fd3d
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/mayapy2016.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Mayapy 2016"
+set __exe__="C:\Program Files\Autodesk\Maya2016\bin\mayapy.exe"
+if not exist %__exe__% goto :missing_app
+
+call %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found at %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/mayapy2017.bat b/pype/settings/defaults/launchers/windows/mayapy2017.bat
new file mode 100644
index 0000000000..14aacc5a7f
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/mayapy2017.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Mayapy 2017"
+set __exe__="C:\Program Files\Autodesk\Maya2017\bin\mayapy.exe"
+if not exist %__exe__% goto :missing_app
+
+call %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found at %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/mayapy2018.bat b/pype/settings/defaults/launchers/windows/mayapy2018.bat
new file mode 100644
index 0000000000..c47c472f46
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/mayapy2018.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Mayapy 2018"
+set __exe__="C:\Program Files\Autodesk\Maya2018\bin\mayapy.exe"
+if not exist %__exe__% goto :missing_app
+
+call %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found at %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/mayapy2019.bat b/pype/settings/defaults/launchers/windows/mayapy2019.bat
new file mode 100644
index 0000000000..73ca5b2d40
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/mayapy2019.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Mayapy 2019"
+set __exe__="C:\Program Files\Autodesk\Maya2019\bin\mayapy.exe"
+if not exist %__exe__% goto :missing_app
+
+call %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found at %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/mayapy2020.bat b/pype/settings/defaults/launchers/windows/mayapy2020.bat
new file mode 100644
index 0000000000..770a03dcf5
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/mayapy2020.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Mayapy 2020"
+set __exe__="C:\Program Files\Autodesk\Maya2020\bin\mayapy.exe"
+if not exist %__exe__% goto :missing_app
+
+call %__exe__% %*
+
+goto :eofS
+
+:missing_app
+ echo ERROR: %__app__% not found at %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/nuke10.0.bat b/pype/settings/defaults/launchers/windows/nuke10.0.bat
new file mode 100644
index 0000000000..a47cbdfb20
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/nuke10.0.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Nuke10.0v4"
+set __exe__="C:\Program Files\Nuke10.0v4\Nuke10.0.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/nuke11.0.bat b/pype/settings/defaults/launchers/windows/nuke11.0.bat
new file mode 100644
index 0000000000..a374c5cf5b
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/nuke11.0.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Nuke11.0v4"
+set __exe__="C:\Program Files\Nuke11.0v4\Nuke11.0.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/nuke11.2.bat b/pype/settings/defaults/launchers/windows/nuke11.2.bat
new file mode 100644
index 0000000000..4c777ac28c
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/nuke11.2.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Nuke11.2v3"
+set __exe__="C:\Program Files\Nuke11.2v3\Nuke11.2.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/nuke11.3.bat b/pype/settings/defaults/launchers/windows/nuke11.3.bat
new file mode 100644
index 0000000000..a023f5f46f
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/nuke11.3.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Nuke11.3v1"
+set __exe__="C:\Program Files\Nuke11.3v1\Nuke11.3.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/nuke12.0.bat b/pype/settings/defaults/launchers/windows/nuke12.0.bat
new file mode 100644
index 0000000000..d8fb5772bb
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/nuke12.0.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Nuke12.0v1"
+set __exe__="C:\Program Files\Nuke12.0v1\Nuke12.0.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/nukestudio10.0.bat b/pype/settings/defaults/launchers/windows/nukestudio10.0.bat
new file mode 100644
index 0000000000..82f833667c
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/nukestudio10.0.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="NukeStudio10.0v4"
+set __exe__="C:\Program Files\Nuke10.0v4\Nuke10.0.exe" --studio
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/nukestudio11.0.bat b/pype/settings/defaults/launchers/windows/nukestudio11.0.bat
new file mode 100644
index 0000000000..b66797727e
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/nukestudio11.0.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="NukeStudio11.0v4"
+set __exe__="C:\Program Files\Nuke11.0v4\Nuke11.0.exe" -studio
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/nukestudio11.2.bat b/pype/settings/defaults/launchers/windows/nukestudio11.2.bat
new file mode 100644
index 0000000000..a653d816b4
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/nukestudio11.2.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="NukeStudio11.2v3"
+set __exe__="C:\Program Files\Nuke11.2v3\Nuke11.2.exe" -studio
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/nukestudio11.3.bat b/pype/settings/defaults/launchers/windows/nukestudio11.3.bat
new file mode 100644
index 0000000000..62c8718873
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/nukestudio11.3.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="NukeStudio11.3v1"
+set __exe__="C:\Program Files\Nuke11.3v1\Nuke11.3.exe" --studio
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/nukestudio12.0.bat b/pype/settings/defaults/launchers/windows/nukestudio12.0.bat
new file mode 100644
index 0000000000..488232bcbf
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/nukestudio12.0.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="NukeStudio12.0v1"
+set __exe__="C:\Program Files\Nuke12.0v1\Nuke12.0.exe" --studio
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/nukex10.0.bat b/pype/settings/defaults/launchers/windows/nukex10.0.bat
new file mode 100644
index 0000000000..1759706a7b
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/nukex10.0.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="NukeX10.0v4"
+set __exe__="C:\Program Files\Nuke10.0v4\Nuke10.0.exe" -nukex
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/nukex11.0.bat b/pype/settings/defaults/launchers/windows/nukex11.0.bat
new file mode 100644
index 0000000000..b554a7b6fa
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/nukex11.0.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="NukeX11.0v4"
+set __exe__="C:\Program Files\Nuke11.0v4\Nuke11.0.exe" --nukex
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/nukex11.2.bat b/pype/settings/defaults/launchers/windows/nukex11.2.bat
new file mode 100644
index 0000000000..a4cb5dec5c
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/nukex11.2.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="NukeX11.2v3"
+set __exe__="C:\Program Files\Nuke11.2v3\Nuke11.2.exe" --nukex
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/nukex11.3.bat b/pype/settings/defaults/launchers/windows/nukex11.3.bat
new file mode 100644
index 0000000000..490b55cf4c
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/nukex11.3.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="NukeX11.3v1"
+set __exe__="C:\Program Files\Nuke11.3v1\Nuke11.3.exe" --nukex
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/nukex12.0.bat b/pype/settings/defaults/launchers/windows/nukex12.0.bat
new file mode 100644
index 0000000000..26adf0d3f1
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/nukex12.0.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="NukeX12.0v1"
+set __exe__="C:\Program Files\Nuke12.0v1\Nuke12.0.exe" --nukex
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/photoshop_2020.bat b/pype/settings/defaults/launchers/windows/photoshop_2020.bat
new file mode 100644
index 0000000000..6b90922ef6
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/photoshop_2020.bat
@@ -0,0 +1,15 @@
+@echo off
+
+set __app__="Photoshop 2020"
+set __exe__="C:\Program Files\Adobe\Adobe Photoshop 2020\Photoshop.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% cmd.exe /k "%PYPE_PYTHON_EXE% -c ^"import avalon.photoshop;avalon.photoshop.launch("%__exe__%")^""
+
+goto :eof
+
+pause
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/premiere_pro_2019.bat b/pype/settings/defaults/launchers/windows/premiere_pro_2019.bat
new file mode 100644
index 0000000000..4886737d2f
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/premiere_pro_2019.bat
@@ -0,0 +1,14 @@
+@echo off
+
+set __app__="Adobe Premiere Pro"
+set __exe__="C:\Program Files\Adobe\Adobe Premiere Pro CC 2019\Adobe Premiere Pro.exe"
+if not exist %__exe__% goto :missing_app
+
+python -u %PREMIERA_PATH%\init.py
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/premiere_pro_2020.bat b/pype/settings/defaults/launchers/windows/premiere_pro_2020.bat
new file mode 100644
index 0000000000..14662d3be3
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/premiere_pro_2020.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Adobe Premiere Pro"
+set __exe__="C:\Program Files\Adobe\Adobe Premiere Pro 2020\Adobe Premiere Pro.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/python3.bat b/pype/settings/defaults/launchers/windows/python3.bat
new file mode 100644
index 0000000000..c7c116fe72
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/python3.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Python36"
+set __exe__="C:\Python36\python.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/resolve_16.bat b/pype/settings/defaults/launchers/windows/resolve_16.bat
new file mode 100644
index 0000000000..1a5d964e6b
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/resolve_16.bat
@@ -0,0 +1,17 @@
+@echo off
+
+set __app__="Resolve"
+set __appy__="Resolve Python Console"
+set __exe__="C:/Program Files/Blackmagic Design/DaVinci Resolve/Resolve.exe"
+set __py__="%PYTHON36_RESOLVE%/python.exe"
+
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %*
+IF "%RESOLVE_DEV%"=="True" (start %__appy__% %__py__% -i %PRE_PYTHON_SCRIPT%)
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/shell.bat b/pype/settings/defaults/launchers/windows/shell.bat
new file mode 100644
index 0000000000..eb0895364f
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/shell.bat
@@ -0,0 +1,2 @@
+@echo off
+start cmd
diff --git a/pype/settings/defaults/launchers/windows/storyboardpro_7.bat b/pype/settings/defaults/launchers/windows/storyboardpro_7.bat
new file mode 100644
index 0000000000..122edac572
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/storyboardpro_7.bat
@@ -0,0 +1,13 @@
+@echo off
+
+set __app__="Storyboard Pro 7"
+set __exe__="C:/Program Files (x86)/Toon Boom Animation/Toon Boom Storyboard Pro 7/win64/bin/StoryboardPro.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% cmd.exe /k "python -c ^"import avalon.storyboardpro;avalon.storyboardpro.launch("%__exe__%")^""
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/launchers/windows/unreal.bat b/pype/settings/defaults/launchers/windows/unreal.bat
new file mode 100644
index 0000000000..7771aaa5a5
--- /dev/null
+++ b/pype/settings/defaults/launchers/windows/unreal.bat
@@ -0,0 +1,11 @@
+set __app__="Unreal Editor"
+set __exe__="%AVALON_CURRENT_UNREAL_ENGINE%\Engine\Binaries\Win64\UE4Editor.exe"
+if not exist %__exe__% goto :missing_app
+
+start %__app__% %__exe__% %PYPE_UNREAL_PROJECT_FILE% %*
+
+goto :eof
+
+:missing_app
+ echo ERROR: %__app__% not found in %__exe__%
+ exit /B 1
diff --git a/pype/settings/defaults/system_settings/environments/photoshop.json b/pype/settings/defaults/system_settings/environments/photoshop.json
deleted file mode 100644
index 2208a88665..0000000000
--- a/pype/settings/defaults/system_settings/environments/photoshop.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH": "1",
- "PYTHONPATH": "{PYTHONPATH}"
-}
diff --git a/pype/settings/lib.py b/pype/settings/lib.py
index 388557ca9b..96c3829388 100644
--- a/pype/settings/lib.py
+++ b/pype/settings/lib.py
@@ -6,9 +6,11 @@ import copy
log = logging.getLogger(__name__)
# Metadata keys for work with studio and project overrides
-OVERRIDEN_KEY = "__overriden_keys__"
+M_OVERRIDEN_KEY = "__overriden_keys__"
+# Metadata key for storing information about environments
+M_ENVIRONMENT_KEY = "__environment_keys__"
# NOTE key popping not implemented yet
-POP_KEY = "__pop_key__"
+M_POP_KEY = "__pop_key__"
# Folder where studio overrides are stored
STUDIO_OVERRIDES_PATH = os.environ["PYPE_PROJECT_CONFIGS"]
@@ -19,6 +21,12 @@ SYSTEM_SETTINGS_PATH = os.path.join(
STUDIO_OVERRIDES_PATH, SYSTEM_SETTINGS_KEY + ".json"
)
+# File where studio's environment overrides are stored
+ENVIRONMENTS_KEY = "environments"
+ENVIRONMENTS_PATH = os.path.join(
+ STUDIO_OVERRIDES_PATH, ENVIRONMENTS_KEY + ".json"
+)
+
# File where studio's default project overrides are stored
PROJECT_SETTINGS_KEY = "project_settings"
PROJECT_SETTINGS_FILENAME = PROJECT_SETTINGS_KEY + ".json"
@@ -105,6 +113,32 @@ def load_json(fpath):
return {}
+def find_environments(data):
+ if not data or not isinstance(data, dict):
+ return
+
+ output = {}
+ if M_ENVIRONMENT_KEY in data:
+ metadata = data.pop(M_ENVIRONMENT_KEY)
+ for env_group_key, env_keys in metadata.items():
+ output[env_group_key] = {}
+ for key in env_keys:
+ output[env_group_key][key] = data[key]
+
+ for value in data.values():
+ result = find_environments(value)
+ if not result:
+ continue
+
+ for env_group_key, env_values in result.items():
+ if env_group_key not in output:
+ output[env_group_key] = {}
+
+ for env_key, env_value in env_values.items():
+ output[env_group_key][env_key] = env_value
+ return output
+
+
def subkey_merge(_dict, value, keys):
key = keys.pop(0)
if not keys:
@@ -162,6 +196,12 @@ def studio_system_settings():
return {}
+def studio_environments():
+ if os.path.exists(ENVIRONMENTS_PATH):
+ return load_json(ENVIRONMENTS_PATH)
+ return {}
+
+
def studio_project_settings():
if os.path.exists(PROJECT_SETTINGS_PATH):
return load_json(PROJECT_SETTINGS_PATH)
@@ -211,13 +251,13 @@ def project_anatomy_overrides(project_name):
def merge_overrides(global_dict, override_dict):
- if OVERRIDEN_KEY in override_dict:
- overriden_keys = set(override_dict.pop(OVERRIDEN_KEY))
+ if M_OVERRIDEN_KEY in override_dict:
+ overriden_keys = set(override_dict.pop(M_OVERRIDEN_KEY))
else:
overriden_keys = set()
for key, value in override_dict.items():
- if value == POP_KEY:
+ if value == M_POP_KEY:
global_dict.pop(key)
elif (
@@ -256,3 +296,11 @@ def project_settings(project_name):
project_overrides = project_settings_overrides(project_name)
return apply_overrides(studio_overrides, project_overrides)
+
+
+def environments():
+ envs = copy.deepcopy(default_settings()[ENVIRONMENTS_KEY])
+ envs_from_system_settings = find_environments(system_settings())
+ for env_group_key, values in envs_from_system_settings.items():
+ envs[env_group_key] = values
+ return envs
diff --git a/pype/tools/settings/settings/README.md b/pype/tools/settings/settings/README.md
index e8b7fcdb57..4f4e9d305a 100644
--- a/pype/tools/settings/settings/README.md
+++ b/pype/tools/settings/settings/README.md
@@ -19,6 +19,7 @@
- GUI schemas are huge json files, to be able to split whole configuration into multiple schema there's type `schema`
- system configuration schemas are stored in `~/tools/settings/settings/gui_schemas/system_schema/` and project configurations in `~/tools/settings/settings/gui_schemas/projects_schema/`
- each schema name is filename of json file except extension (without ".json")
+- if content is dictionary content will be used as `schema` else will be used as `schema_template`
### schema
- can have only key `"children"` which is list of strings, each string should represent another schema (order matters) string represebts name of the schema
@@ -31,6 +32,87 @@
}
```
+### schema_template
+- allows to define schema "templates" to not duplicate same content multiple times
+```javascript
+// EXAMPLE json file content (filename: example_template.json)
+[
+ {
+ "__default_values__": {
+ "multipath_executables": true
+ }
+ }, {
+ "type": "raw-json",
+ "label": "{host_label} Environments",
+ "key": "{host_name}_environments",
+ "env_group_key": "{host_name}"
+ }, {
+ "type": "path-widget",
+ "key": "{host_name}_executables",
+ "label": "{host_label} - Full paths to executables",
+ "multiplatform": "{multipath_executables}",
+ "multipath": true
+ }
+]
+```
+```javascript
+// EXAMPLE usage of the template in schema
+{
+ "type": "dict",
+ "key": "schema_template_examples",
+ "label": "Schema template examples",
+ "children": [
+ {
+ "type": "schema_template",
+ // filename of template (example_template.json)
+ "name": "example_template",
+ "template_data": {
+ "host_label": "Maya 2019",
+ "host_name": "maya_2019",
+ "multipath_executables": false
+ }
+ }, {
+ "type": "schema_template",
+ "name": "example_template",
+ "template_data": {
+ "host_label": "Maya 2020",
+ "host_name": "maya_2020"
+ }
+ }
+ ]
+}
+```
+- item in schema mush contain `"type"` and `"name"` keys but it is also expected that `"template_data"` will be entered too
+- all items in the list, except `__default_values__`, will replace `schema_template` item in schema
+- template may contain another template or schema
+- it is expected that schema template will have unfilled fields as in example
+ - unfilled fields are allowed only in values of schema dictionary
+```javascript
+{
+ ...
+ // Allowed
+ "key": "{to_fill}"
+ ...
+ // Not allowed
+ "{to_fill}": "value"
+ ...
+}
+```
+- Unfilled fields can be also used for non string values, in that case value must contain only one key and value for fill must contain right type.
+```javascript
+{
+ ...
+ // Allowed
+ "multiplatform": "{executable_multiplatform}"
+ ...
+ // Not allowed
+ "multiplatform": "{executable_multiplatform}_enhanced_string"
+ ...
+}
+```
+- It is possible to define default values for unfilled fields to do so one of items in list must be dictionary with key `"__default_values__"` and value as dictionary with default key: values (as in example above).
+
+
## Basic Dictionary inputs
- these inputs wraps another inputs into {key: value} relation
diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json b/pype/tools/settings/settings/gui_schemas/system_schema/example_schema.json
similarity index 93%
rename from pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json
rename to pype/tools/settings/settings/gui_schemas/system_schema/example_schema.json
index dd0f7f20d1..7612e54116 100644
--- a/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json
+++ b/pype/tools/settings/settings/gui_schemas/system_schema/example_schema.json
@@ -5,6 +5,40 @@
"is_file": true,
"children": [
{
+ "type": "dict",
+ "key": "schema_template_exaples",
+ "label": "Schema template examples",
+ "children": [
+ {
+ "type": "schema_template",
+ "name": "example_template",
+ "template_data": {
+ "host_label": "Maya 2019",
+ "host_name": "maya_2019",
+ "multipath_executables": false
+ }
+ }, {
+ "type": "schema_template",
+ "name": "example_template",
+ "template_data": {
+ "host_label": "Maya 2020",
+ "host_name": "maya_2020"
+ }
+ }
+ ]
+ }, {
+ "key": "env_group_test",
+ "label": "EnvGroup Test",
+ "type": "dict",
+ "children": [
+ {
+ "key": "key_to_store_in_system_settings",
+ "label": "Testing environment group",
+ "type": "raw-json",
+ "env_group_key": "test_group"
+ }
+ ]
+ }, {
"key": "dict_wrapper",
"type": "dict-invisible",
"children": [
diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/example_template.json b/pype/tools/settings/settings/gui_schemas/system_schema/example_template.json
new file mode 100644
index 0000000000..48a3c955b9
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/system_schema/example_template.json
@@ -0,0 +1,18 @@
+[
+ {
+ "__default_values__": {
+ "multipath_executables": true
+ }
+ }, {
+ "type": "raw-json",
+ "label": "{host_label} Environments",
+ "key": "{host_name}_environments",
+ "env_group_key": "{host_name}"
+ }, {
+ "type": "path-widget",
+ "key": "{host_name}_executables",
+ "label": "{host_label} - Full paths to executables",
+ "multiplatform": "{multipath_executables}",
+ "multipath": true
+ }
+]
diff --git a/pype/tools/settings/settings/widgets/base.py b/pype/tools/settings/settings/widgets/base.py
index 404d7b700b..7cbe7c2f6f 100644
--- a/pype/tools/settings/settings/widgets/base.py
+++ b/pype/tools/settings/settings/widgets/base.py
@@ -47,6 +47,7 @@ class SystemWidget(QtWidgets.QWidget):
self._ignore_value_changes = False
self.input_fields = []
+ self.environ_fields = []
scroll_widget = QtWidgets.QScrollArea(self)
scroll_widget.setObjectName("GroupWidget")
@@ -130,14 +131,18 @@ class SystemWidget(QtWidgets.QWidget):
for input_field in self.input_fields:
input_field.hierarchical_style_update()
+ def add_environ_field(self, input_field):
+ self.environ_fields.append(input_field)
+
def reset(self):
reset_default_settings()
- if self.content_layout.count() != 0:
- for widget in self.input_fields:
- self.content_layout.removeWidget(widget)
- widget.deleteLater()
- self.input_fields.clear()
+ self.input_fields.clear()
+ self.environ_fields.clear()
+ while self.content_layout.count() != 0:
+ widget = self.content_layout.itemAt(0).widget()
+ self.content_layout.removeWidget(widget)
+ widget.deleteLater()
self.schema = lib.gui_schema("system_schema", "0_system_gui_schema")
self.keys = self.schema.get("keys", [])
@@ -214,7 +219,7 @@ class SystemWidget(QtWidgets.QWidget):
all_values = _all_values
# Skip first key
- all_values = all_values["system"]
+ all_values = lib.convert_gui_data_with_metadata(all_values["system"])
prject_defaults_dir = os.path.join(
DEFAULTS_DIR, SYSTEM_SETTINGS_KEY
@@ -246,16 +251,19 @@ class SystemWidget(QtWidgets.QWidget):
def _update_values(self):
self.ignore_value_changes = True
- default_values = {
+ default_values = lib.convert_data_to_gui_data({
"system": default_settings()[SYSTEM_SETTINGS_KEY]
- }
+ })
for input_field in self.input_fields:
input_field.update_default_values(default_values)
if self._hide_studio_overrides:
system_values = lib.NOT_SET
else:
- system_values = {"system": studio_system_settings()}
+ system_values = lib.convert_overrides_to_gui_data(
+ {"system": studio_system_settings()}
+ )
+
for input_field in self.input_fields:
input_field.update_studio_values(system_values)
@@ -730,17 +738,20 @@ class ProjectWidget(QtWidgets.QWidget):
def _update_values(self):
self.ignore_value_changes = True
- default_values = {"project": default_settings()}
+ default_values = default_values = lib.convert_data_to_gui_data(
+ {"project": default_settings()}
+ )
for input_field in self.input_fields:
input_field.update_default_values(default_values)
if self._hide_studio_overrides:
studio_values = lib.NOT_SET
else:
- studio_values = {"project": {
+ studio_values = lib.convert_overrides_to_gui_data({"project": {
PROJECT_SETTINGS_KEY: studio_project_settings(),
PROJECT_ANATOMY_KEY: studio_project_anatomy()
- }}
+ }})
+
for input_field in self.input_fields:
input_field.update_studio_values(studio_values)
diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py
index addce2b6b5..375dde566e 100644
--- a/pype/tools/settings/settings/widgets/item_types.py
+++ b/pype/tools/settings/settings/widgets/item_types.py
@@ -42,6 +42,8 @@ class SettingObject:
# Will allow to show actions for the item type (disabled for proxies) else
# item is skipped and try to trigger actions on it's parent.
allow_actions = True
+ # If item can store environment values
+ allow_to_environment = False
# All item types must have implemented Qt signal which is emitted when
# it's or it's children value has changed,
value_changed = None
@@ -61,6 +63,24 @@ class SettingObject:
key = getattr(self, "key", None)
raise InvalidValueType(self.valid_value_types, type(value), key)
+ def merge_metadata(self, current_metadata, new_metadata):
+ for key, value in new_metadata.items():
+ if key not in current_metadata:
+ current_metadata[key] = value
+
+ elif key == "groups":
+ current_metadata[key].extend(value)
+
+ elif key == "environments":
+ for group_key, subvalue in value.items():
+ if group_key not in current_metadata[key]:
+ current_metadata[key][group_key] = []
+ current_metadata[key][group_key].extend(subvalue)
+
+ else:
+ raise KeyError("Unknown metadata key: \"{}\"".format(key))
+ return current_metadata
+
def _set_default_attributes(self):
"""Create and reset attributes required for all item types.
@@ -80,6 +100,9 @@ class SettingObject:
self._as_widget = False
self._is_group = False
+ # If value should be stored to environments
+ self._env_group_key = None
+
self._any_parent_as_widget = None
self._any_parent_is_group = None
@@ -115,6 +138,20 @@ class SettingObject:
self._is_group = input_data.get("is_group", False)
# TODO not implemented yet
self._is_nullable = input_data.get("is_nullable", False)
+ self._env_group_key = input_data.get("env_group_key")
+
+ if self.is_environ:
+ if not self.allow_to_environment:
+ raise TypeError((
+ "Item {} does not allow to store environment values"
+ ).format(input_data["type"]))
+
+ if self.as_widget:
+ raise TypeError((
+ "Item is used as widget and"
+ " marked to store environments at the same time."
+ ))
+ self.add_environ_field(self)
any_parent_as_widget = parent.as_widget
if not any_parent_as_widget:
@@ -171,6 +208,17 @@ class SettingObject:
"""
return self._has_studio_override or self._parent.has_studio_override
+ @property
+ def is_environ(self):
+ return self._env_group_key is not None
+
+ @property
+ def env_group_key(self):
+ return self._env_group_key
+
+ def add_environ_field(self, input_field):
+ self._parent.add_environ_field(input_field)
+
@property
def as_widget(self):
"""Item is used as widget in parent item.
@@ -300,6 +348,13 @@ class SettingObject:
"""Output for saving changes or overrides."""
return {self.key: self.item_value()}
+ def environment_value(self):
+ raise NotImplementedError(
+ "{} Method `environment_value` not implemented!".format(
+ repr(self)
+ )
+ )
+
@classmethod
def style_state(
cls, has_studio_override, is_invalid, is_overriden, is_modified
@@ -1187,7 +1242,7 @@ class RawJsonInput(QtWidgets.QPlainTextEdit):
class RawJsonWidget(QtWidgets.QWidget, InputObject):
default_input_value = "{}"
value_changed = QtCore.Signal(object)
- valid_value_types = (str, dict, list, type(NOT_SET))
+ allow_to_environment = True
def __init__(
self, input_data, parent,
@@ -1238,7 +1293,21 @@ class RawJsonWidget(QtWidgets.QWidget, InputObject):
def item_value(self):
if self.is_invalid:
return NOT_SET
- return self.input_field.json_value()
+
+ value = self.input_field.json_value()
+ if not self.is_environ:
+ return value
+
+ output = {}
+ for key, value in value.items():
+ output[key.upper()] = value
+
+ output[METADATA_KEY] = {
+ "environments": {
+ self.env_group_key: list(output.keys())
+ }
+ }
+ return output
class ListItem(QtWidgets.QWidget, SettingObject):
@@ -2629,6 +2698,33 @@ class DictWidget(QtWidgets.QWidget, SettingObject):
output.update(input_field.config_value())
return output
+ def _override_values(self, project_overrides):
+ values = {}
+ groups = []
+ for input_field in self.input_fields:
+ if project_overrides:
+ value, is_group = input_field.overrides()
+ else:
+ value, is_group = input_field.studio_overrides()
+ if value is NOT_SET:
+ continue
+
+ if METADATA_KEY in value and METADATA_KEY in values:
+ new_metadata = value.pop(METADATA_KEY)
+ values[METADATA_KEY] = self.merge_metadata(
+ values[METADATA_KEY], new_metadata
+ )
+
+ values.update(value)
+ if is_group:
+ groups.extend(value.keys())
+
+ if groups:
+ if METADATA_KEY not in values:
+ values[METADATA_KEY] = {}
+ values[METADATA_KEY]["groups"] = groups
+ return {self.key: values}, self.is_group
+
def studio_overrides(self):
if (
not (self.as_widget or self.any_parent_as_widget)
@@ -2636,34 +2732,12 @@ class DictWidget(QtWidgets.QWidget, SettingObject):
and not self.child_has_studio_override
):
return NOT_SET, False
-
- values = {}
- groups = []
- for input_field in self.input_fields:
- value, is_group = input_field.studio_overrides()
- if value is not NOT_SET:
- values.update(value)
- if is_group:
- groups.extend(value.keys())
- if groups:
- values[METADATA_KEY] = {"groups": groups}
- return {self.key: values}, self.is_group
+ return self._override_values(False)
def overrides(self):
if not self.is_overriden and not self.child_overriden:
return NOT_SET, False
-
- values = {}
- groups = []
- for input_field in self.input_fields:
- value, is_group = input_field.overrides()
- if value is not NOT_SET:
- values.update(value)
- if is_group:
- groups.extend(value.keys())
- if groups:
- values[METADATA_KEY] = {"groups": groups}
- return {self.key: values}, self.is_group
+ return self._override_values(True)
class DictInvisible(QtWidgets.QWidget, SettingObject):
@@ -2897,6 +2971,33 @@ class DictInvisible(QtWidgets.QWidget, SettingObject):
)
self._was_overriden = bool(self._is_overriden)
+ def _override_values(self, project_overrides):
+ values = {}
+ groups = []
+ for input_field in self.input_fields:
+ if project_overrides:
+ value, is_group = input_field.overrides()
+ else:
+ value, is_group = input_field.studio_overrides()
+ if value is NOT_SET:
+ continue
+
+ if METADATA_KEY in value and METADATA_KEY in values:
+ new_metadata = value.pop(METADATA_KEY)
+ values[METADATA_KEY] = self.merge_metadata(
+ values[METADATA_KEY], new_metadata
+ )
+
+ values.update(value)
+ if is_group:
+ groups.extend(value.keys())
+
+ if groups:
+ if METADATA_KEY not in values:
+ values[METADATA_KEY] = {}
+ values[METADATA_KEY]["groups"] = groups
+ return {self.key: values}, self.is_group
+
def studio_overrides(self):
if (
not (self.as_widget or self.any_parent_as_widget)
@@ -2904,34 +3005,12 @@ class DictInvisible(QtWidgets.QWidget, SettingObject):
and not self.child_has_studio_override
):
return NOT_SET, False
-
- values = {}
- groups = []
- for input_field in self.input_fields:
- value, is_group = input_field.studio_overrides()
- if value is not NOT_SET:
- values.update(value)
- if is_group:
- groups.extend(value.keys())
- if groups:
- values[METADATA_KEY] = {"groups": groups}
- return {self.key: values}, self.is_group
+ return self._override_values(False)
def overrides(self):
if not self.is_overriden and not self.child_overriden:
return NOT_SET, False
-
- values = {}
- groups = []
- for input_field in self.input_fields:
- value, is_group = input_field.overrides()
- if value is not NOT_SET:
- values.update(value)
- if is_group:
- groups.extend(value.keys())
- if groups:
- values[METADATA_KEY] = {"groups": groups}
- return {self.key: values}, self.is_group
+ return self._override_values(True)
class PathWidget(QtWidgets.QWidget, SettingObject):
diff --git a/pype/tools/settings/settings/widgets/lib.py b/pype/tools/settings/settings/widgets/lib.py
index cf2bd7f8af..569e7bfbb7 100644
--- a/pype/tools/settings/settings/widgets/lib.py
+++ b/pype/tools/settings/settings/widgets/lib.py
@@ -1,7 +1,8 @@
import os
+import re
import json
import copy
-from pype.settings.lib import OVERRIDEN_KEY
+from pype.settings.lib import M_OVERRIDEN_KEY, M_ENVIRONMENT_KEY
from queue import Queue
@@ -11,10 +12,50 @@ class TypeToKlass:
NOT_SET = type("NOT_SET", (), {"__bool__": lambda obj: False})()
-METADATA_KEY = type("METADATA_KEY", (), {})
+METADATA_KEY = type("METADATA_KEY", (), {})()
OVERRIDE_VERSION = 1
CHILD_OFFSET = 15
+key_pattern = re.compile(r"(\{.*?[^{0]*\})")
+
+
+def convert_gui_data_with_metadata(data, ignored_keys=None):
+ if not data or not isinstance(data, dict):
+ return data
+
+ if ignored_keys is None:
+ ignored_keys = tuple()
+
+ output = {}
+ if METADATA_KEY in data:
+ metadata = data.pop(METADATA_KEY)
+ for key, value in metadata.items():
+ if key in ignored_keys or key == "groups":
+ continue
+
+ if key == "environments":
+ output[M_ENVIRONMENT_KEY] = value
+ else:
+ raise KeyError("Unknown metadata key \"{}\"".format(key))
+
+ for key, value in data.items():
+ output[key] = convert_gui_data_with_metadata(value, ignored_keys)
+ return output
+
+
+def convert_data_to_gui_data(data, first=True):
+ if not data or not isinstance(data, dict):
+ return data
+
+ output = {}
+ if M_ENVIRONMENT_KEY in data:
+ data.pop(M_ENVIRONMENT_KEY)
+
+ for key, value in data.items():
+ output[key] = convert_data_to_gui_data(value, False)
+
+ return output
+
def convert_gui_data_to_overrides(data, first=True):
if not data or not isinstance(data, dict):
@@ -23,14 +64,15 @@ def convert_gui_data_to_overrides(data, first=True):
output = {}
if first:
output["__override_version__"] = OVERRIDE_VERSION
+ data = convert_gui_data_with_metadata(data)
if METADATA_KEY in data:
metadata = data.pop(METADATA_KEY)
for key, value in metadata.items():
if key == "groups":
- output[OVERRIDEN_KEY] = value
+ output[M_OVERRIDEN_KEY] = value
else:
- KeyError("Unknown metadata key \"{}\"".format(key))
+ raise KeyError("Unknown metadata key \"{}\"".format(key))
for key, value in data.items():
output[key] = convert_gui_data_to_overrides(value, False)
@@ -41,9 +83,12 @@ def convert_overrides_to_gui_data(data, first=True):
if not data or not isinstance(data, dict):
return data
+ if first:
+ data = convert_data_to_gui_data(data)
+
output = {}
- if OVERRIDEN_KEY in data:
- groups = data.pop(OVERRIDEN_KEY)
+ if M_OVERRIDEN_KEY in data:
+ groups = data.pop(M_OVERRIDEN_KEY)
if METADATA_KEY not in output:
output[METADATA_KEY] = {}
output[METADATA_KEY]["groups"] = groups
@@ -54,7 +99,111 @@ def convert_overrides_to_gui_data(data, first=True):
return output
-def _fill_inner_schemas(schema_data, schema_collection):
+def _fill_schema_template_data(
+ template, template_data, required_keys=None, missing_keys=None
+):
+ first = False
+ if required_keys is None:
+ first = True
+ required_keys = set()
+ missing_keys = set()
+
+ _template = []
+ default_values = {}
+ for item in template:
+ if isinstance(item, dict) and "__default_values__" in item:
+ default_values = item["__default_values__"]
+ else:
+ _template.append(item)
+ template = _template
+
+ for key, value in default_values.items():
+ if key not in template_data:
+ template_data[key] = value
+
+ if not template:
+ output = template
+
+ elif isinstance(template, list):
+ output = []
+ for item in template:
+ output.append(_fill_schema_template_data(
+ item, template_data, required_keys, missing_keys
+ ))
+
+ elif isinstance(template, dict):
+ output = {}
+ for key, value in template.items():
+ output[key] = _fill_schema_template_data(
+ value, template_data, required_keys, missing_keys
+ )
+
+ elif isinstance(template, str):
+ # TODO find much better way how to handle filling template data
+ for replacement_string in key_pattern.findall(template):
+ key = str(replacement_string[1:-1])
+ required_keys.add(key)
+ if key not in template_data:
+ missing_keys.add(key)
+ continue
+
+ value = template_data[key]
+ if replacement_string == template:
+ # Replace the value with value from templates data
+ # - with this is possible to set value with different type
+ template = value
+ else:
+ # Only replace the key in string
+ template = template.replace(replacement_string, value)
+ output = template
+
+ else:
+ output = template
+
+ if first and missing_keys:
+ raise SchemaTemplateMissingKeys(missing_keys, required_keys)
+
+ return output
+
+
+def _fill_schema_template(child_data, schema_collection, schema_templates):
+ template_name = child_data["name"]
+ template = schema_templates.get(template_name)
+ if template is None:
+ if template_name in schema_collection:
+ raise KeyError((
+ "Schema \"{}\" is used as `schema_template`"
+ ).format(template_name))
+ raise KeyError("Schema template \"{}\" was not found".format(
+ template_name
+ ))
+
+ template_data = child_data.get("template_data") or {}
+ try:
+ filled_child = _fill_schema_template_data(
+ template, template_data
+ )
+
+ except SchemaTemplateMissingKeys as exc:
+ raise SchemaTemplateMissingKeys(
+ exc.missing_keys, exc.required_keys, template_name
+ )
+
+ output = []
+ for item in filled_child:
+ filled_item = _fill_inner_schemas(
+ item, schema_collection, schema_templates
+ )
+ if filled_item["type"] == "schema_template":
+ output.extend(_fill_schema_template(
+ filled_item, schema_collection, schema_templates
+ ))
+ else:
+ output.append(filled_item)
+ return output
+
+
+def _fill_inner_schemas(schema_data, schema_collection, schema_templates):
if schema_data["type"] == "schema":
raise ValueError("First item in schema data can't be schema.")
@@ -64,21 +213,62 @@ def _fill_inner_schemas(schema_data, schema_collection):
new_children = []
for child in children:
- if child["type"] != "schema":
- new_child = _fill_inner_schemas(child, schema_collection)
- new_children.append(new_child)
+ child_type = child["type"]
+ if child_type == "schema":
+ schema_name = child["name"]
+ if schema_name not in schema_collection:
+ if schema_name in schema_templates:
+ raise KeyError((
+ "Schema template \"{}\" is used as `schema`"
+ ).format(schema_name))
+ raise KeyError(
+ "Schema \"{}\" was not found".format(schema_name)
+ )
+
+ filled_child = _fill_inner_schemas(
+ schema_collection[schema_name],
+ schema_collection,
+ schema_templates
+ )
+
+ elif child_type == "schema_template":
+ for filled_child in _fill_schema_template(
+ child, schema_collection, schema_templates
+ ):
+ new_children.append(filled_child)
continue
- new_child = _fill_inner_schemas(
- schema_collection[child["name"]],
- schema_collection
- )
- new_children.append(new_child)
+ else:
+ filled_child = _fill_inner_schemas(
+ child, schema_collection, schema_templates
+ )
+
+ new_children.append(filled_child)
schema_data["children"] = new_children
return schema_data
+class SchemaTemplateMissingKeys(Exception):
+ def __init__(self, missing_keys, required_keys, template_name=None):
+ self.missing_keys = missing_keys
+ self.required_keys = required_keys
+ if template_name:
+ msg = f"Schema template \"{template_name}\" require more keys.\n"
+ else:
+ msg = ""
+ msg += "Required keys: {}\nMissing keys: {}".format(
+ self.join_keys(required_keys),
+ self.join_keys(missing_keys)
+ )
+ super(SchemaTemplateMissingKeys, self).__init__(msg)
+
+ def join_keys(self, keys):
+ return ", ".join([
+ f"\"{key}\"" for key in keys
+ ])
+
+
class SchemaMissingFileInfo(Exception):
def __init__(self, invalid):
full_path_keys = []
@@ -120,6 +310,21 @@ class SchemaDuplicatedKeys(Exception):
super(SchemaDuplicatedKeys, self).__init__(msg)
+class SchemaDuplicatedEnvGroupKeys(Exception):
+ def __init__(self, invalid):
+ items = []
+ for key_path, keys in invalid.items():
+ joined_keys = ", ".join([
+ "\"{}\"".format(key) for key in keys
+ ])
+ items.append("\"{}\" ({})".format(key_path, joined_keys))
+
+ msg = (
+ "Schema items contain duplicated environment group keys. {}"
+ ).format(" || ".join(items))
+ super(SchemaDuplicatedEnvGroupKeys, self).__init__(msg)
+
+
def file_keys_from_schema(schema_data):
output = []
item_type = schema_data["type"]
@@ -277,10 +482,50 @@ def validate_keys_are_unique(schema_data, keys=None):
raise SchemaDuplicatedKeys(invalid)
+def validate_environment_groups_uniquenes(
+ schema_data, env_groups=None, keys=None
+):
+ is_first = False
+ if env_groups is None:
+ is_first = True
+ env_groups = {}
+ keys = []
+
+ my_keys = copy.deepcopy(keys)
+ key = schema_data.get("key")
+ if key:
+ my_keys.append(key)
+
+ env_group_key = schema_data.get("env_group_key")
+ if env_group_key:
+ if env_group_key not in env_groups:
+ env_groups[env_group_key] = []
+ env_groups[env_group_key].append("/".join(my_keys))
+
+ children = schema_data.get("children")
+ if not children:
+ return
+
+ for child in children:
+ validate_environment_groups_uniquenes(
+ child, env_groups, copy.deepcopy(my_keys)
+ )
+
+ if is_first:
+ invalid = {}
+ for env_group_key, key_paths in env_groups.items():
+ if len(key_paths) > 1:
+ invalid[env_group_key] = key_paths
+
+ if invalid:
+ raise SchemaDuplicatedEnvGroupKeys(invalid)
+
+
def validate_schema(schema_data):
validate_all_has_ending_file(schema_data)
validate_is_group_is_unique_in_hierarchy(schema_data)
validate_keys_are_unique(schema_data)
+ validate_environment_groups_uniquenes(schema_data)
def gui_schema(subfolder, main_schema_name):
@@ -292,23 +537,30 @@ def gui_schema(subfolder, main_schema_name):
)
loaded_schemas = {}
- for filename in os.listdir(dirpath):
- basename, ext = os.path.splitext(filename)
- if ext != ".json":
- continue
+ loaded_schema_templates = {}
+ for root, _, filenames in os.walk(dirpath):
+ for filename in filenames:
+ basename, ext = os.path.splitext(filename)
+ if ext != ".json":
+ continue
- filepath = os.path.join(dirpath, filename)
- with open(filepath, "r") as json_stream:
- try:
- schema_data = json.load(json_stream)
- except Exception as e:
- raise Exception((f"Unable to parse JSON file {json_stream}\n "
- f" - {e}")) from e
- loaded_schemas[basename] = schema_data
+ filepath = os.path.join(root, filename)
+ with open(filepath, "r") as json_stream:
+ try:
+ schema_data = json.load(json_stream)
+ except Exception as exc:
+ raise Exception((
+ f"Unable to parse JSON file {filepath}\n{exc}"
+ )) from exc
+ if isinstance(schema_data, list):
+ loaded_schema_templates[basename] = schema_data
+ else:
+ loaded_schemas[basename] = schema_data
main_schema = _fill_inner_schemas(
loaded_schemas[main_schema_name],
- loaded_schemas
+ loaded_schemas,
+ loaded_schema_templates
)
validate_schema(main_schema)
return main_schema
diff --git a/pype/tools/standalonepublish/widgets/widget_drop_frame.py b/pype/tools/standalonepublish/widgets/widget_drop_frame.py
index e13f701b30..a7abe1b24c 100644
--- a/pype/tools/standalonepublish/widgets/widget_drop_frame.py
+++ b/pype/tools/standalonepublish/widgets/widget_drop_frame.py
@@ -268,9 +268,10 @@ class DropDataFrame(QtWidgets.QFrame):
args = [
ffprobe_path,
'-v', 'quiet',
- '-print_format', 'json',
+ '-print_format json',
'-show_format',
- '-show_streams', filepath
+ '-show_streams',
+ '"{}"'.format(filepath)
]
ffprobe_p = subprocess.Popen(
' '.join(args),
diff --git a/pype/version.py b/pype/version.py
index 96fc614cb2..0f90260218 100644
--- a/pype/version.py
+++ b/pype/version.py
@@ -1 +1 @@
-__version__ = "2.12.1"
+__version__ = "2.12.2"