diff --git a/pype/api.py b/pype/api.py
index c722757a3c..021080b4d5 100644
--- a/pype/api.py
+++ b/pype/api.py
@@ -1,3 +1,7 @@
+from .settings import (
+ system_settings,
+ project_settings
+)
from pypeapp import (
Logger,
Anatomy,
@@ -49,6 +53,9 @@ from .lib import (
from .lib import _subprocess as subprocess
__all__ = [
+ "system_settings",
+ "project_settings",
+
"Logger",
"Anatomy",
"project_overrides_dir_path",
diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py
index d4b7d91fdb..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"])
{
@@ -18,12 +22,7 @@ def set_scene_settings(settings):
if (args[0]["frameStart"] && args[0]["frameEnd"])
{
var duration = args[0]["frameEnd"] - args[0]["frameStart"] + 1
- if (frame.numberOf() > duration)
- {
- frame.remove(
- duration, frame.numberOf() - duration
- );
- }
+
if (frame.numberOf() < duration)
{
frame.insert(
@@ -41,8 +40,8 @@ def set_scene_settings(settings):
)
}
}
- func
- """
+ %s_func
+ """ % (signature, signature)
harmony.send({"function": func, "args": [settings]})
@@ -112,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":
@@ -149,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);
@@ -184,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": [
@@ -226,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/hosts/maya/plugin.py b/pype/hosts/maya/plugin.py
index ed244d56df..a5c57f1ab8 100644
--- a/pype/hosts/maya/plugin.py
+++ b/pype/hosts/maya/plugin.py
@@ -174,6 +174,25 @@ class ReferenceLoader(api.Loader):
assert os.path.exists(path), "%s does not exist." % path
+ # Need to save alembic settings and reapply, cause referencing resets
+ # them to incoming data.
+ alembic_attrs = ["speed", "offset", "cycleType"]
+ alembic_data = {}
+ if representation["name"] == "abc":
+ alembic_nodes = cmds.ls(
+ "{}:*".format(members[0].split(":")[0]), type="AlembicNode"
+ )
+ if alembic_nodes:
+ for attr in alembic_attrs:
+ node_attr = "{}.{}".format(alembic_nodes[0], attr)
+ alembic_data[attr] = cmds.getAttr(node_attr)
+ else:
+ cmds.warning(
+ "No alembic nodes found in {}".format(
+ cmds.ls("{}:*".format(members[0].split(":")[0]))
+ )
+ )
+
try:
content = cmds.file(path,
loadReference=reference_node,
@@ -195,6 +214,16 @@ class ReferenceLoader(api.Loader):
self.log.warning("Ignoring file read error:\n%s", exc)
+ # Reapply alembic settings.
+ if representation["name"] == "abc":
+ alembic_nodes = cmds.ls(
+ "{}:*".format(members[0].split(":")[0]), type="AlembicNode"
+ )
+ if alembic_nodes:
+ for attr in alembic_attrs:
+ value = alembic_data[attr]
+ cmds.setAttr("{}.{}".format(alembic_nodes[0], attr), value)
+
# Fix PLN-40 for older containers created with Avalon that had the
# `.verticesOnlySet` set to True.
if cmds.getAttr("{}.verticesOnlySet".format(node)):
diff --git a/pype/lib.py b/pype/lib.py
index 601c85f521..6fa204b379 100644
--- a/pype/lib.py
+++ b/pype/lib.py
@@ -19,7 +19,7 @@ from abc import ABCMeta, abstractmethod
from avalon import io, pipeline
import six
import avalon.api
-from .api import config, Anatomy
+from .api import config, Anatomy, Logger
log = logging.getLogger(__name__)
@@ -1622,7 +1622,7 @@ class ApplicationAction(avalon.api.Action):
parsed application `.toml` this can launch the application.
"""
-
+ _log = None
config = None
group = None
variant = None
@@ -1632,6 +1632,12 @@ class ApplicationAction(avalon.api.Action):
"AVALON_TASK"
)
+ @property
+ def log(self):
+ if self._log is None:
+ self._log = Logger().get_logger(self.__class__.__name__)
+ return self._log
+
def is_compatible(self, session):
for key in self.required_session_keys:
if key not in session:
@@ -1644,6 +1650,165 @@ class ApplicationAction(avalon.api.Action):
project_name = session["AVALON_PROJECT"]
asset_name = session["AVALON_ASSET"]
task_name = session["AVALON_TASK"]
- return launch_application(
+ launch_application(
project_name, asset_name, task_name, self.name
)
+
+ self._ftrack_after_launch_procedure(
+ project_name, asset_name, task_name
+ )
+
+ def _ftrack_after_launch_procedure(
+ self, project_name, asset_name, task_name
+ ):
+ # TODO move to launch hook
+ required_keys = ("FTRACK_SERVER", "FTRACK_API_USER", "FTRACK_API_KEY")
+ for key in required_keys:
+ if not os.environ.get(key):
+ self.log.debug((
+ "Missing required environment \"{}\""
+ " for Ftrack after launch procedure."
+ ).format(key))
+ return
+
+ try:
+ import ftrack_api
+ session = ftrack_api.Session(auto_connect_event_hub=True)
+ self.log.debug("Ftrack session created")
+ except Exception:
+ self.log.warning("Couldn't create Ftrack session")
+ return
+
+ try:
+ entity = self._find_ftrack_task_entity(
+ session, project_name, asset_name, task_name
+ )
+ self._ftrack_status_change(session, entity, project_name)
+ self._start_timer(session, entity, ftrack_api)
+ except Exception:
+ self.log.warning(
+ "Couldn't finish Ftrack procedure.", exc_info=True
+ )
+ return
+
+ finally:
+ session.close()
+
+ def _find_ftrack_task_entity(
+ self, session, project_name, asset_name, task_name
+ ):
+ project_entity = session.query(
+ "Project where full_name is \"{}\"".format(project_name)
+ ).first()
+ if not project_entity:
+ self.log.warning(
+ "Couldn't find project \"{}\" in Ftrack.".format(project_name)
+ )
+ return
+
+ potential_task_entities = session.query((
+ "TypedContext where parent.name is \"{}\" and project_id is \"{}\""
+ ).format(asset_name, project_entity["id"])).all()
+ filtered_entities = []
+ for _entity in potential_task_entities:
+ if (
+ _entity.entity_type.lower() == "task"
+ and _entity["name"] == task_name
+ ):
+ filtered_entities.append(_entity)
+
+ if not filtered_entities:
+ self.log.warning((
+ "Couldn't find task \"{}\" under parent \"{}\" in Ftrack."
+ ).format(task_name, asset_name))
+ return
+
+ if len(filtered_entities) > 1:
+ self.log.warning((
+ "Found more than one task \"{}\""
+ " under parent \"{}\" in Ftrack."
+ ).format(task_name, asset_name))
+ return
+
+ return filtered_entities[0]
+
+ def _ftrack_status_change(self, session, entity, project_name):
+ presets = config.get_presets(project_name)["ftrack"]["ftrack_config"]
+ statuses = presets.get("status_update")
+ if not statuses:
+ return
+
+ actual_status = entity["status"]["name"].lower()
+ already_tested = set()
+ ent_path = "/".join(
+ [ent["name"] for ent in entity["link"]]
+ )
+ while True:
+ next_status_name = None
+ for key, value in statuses.items():
+ if key in already_tested:
+ continue
+ if actual_status in value or "_any_" in value:
+ if key != "_ignore_":
+ next_status_name = key
+ already_tested.add(key)
+ break
+ already_tested.add(key)
+
+ if next_status_name is None:
+ break
+
+ try:
+ query = "Status where name is \"{}\"".format(
+ next_status_name
+ )
+ status = session.query(query).one()
+
+ entity["status"] = status
+ session.commit()
+ self.log.debug("Changing status to \"{}\" <{}>".format(
+ next_status_name, ent_path
+ ))
+ break
+
+ except Exception:
+ session.rollback()
+ msg = (
+ "Status \"{}\" in presets wasn't found"
+ " on Ftrack entity type \"{}\""
+ ).format(next_status_name, entity.entity_type)
+ self.log.warning(msg)
+
+ def _start_timer(self, session, entity, _ftrack_api):
+ self.log.debug("Triggering timer start.")
+
+ user_entity = session.query("User where username is \"{}\"".format(
+ os.environ["FTRACK_API_USER"]
+ )).first()
+ if not user_entity:
+ self.log.warning(
+ "Couldn't find user with username \"{}\" in Ftrack".format(
+ os.environ["FTRACK_API_USER"]
+ )
+ )
+ return
+
+ source = {
+ "user": {
+ "id": user_entity["id"],
+ "username": user_entity["username"]
+ }
+ }
+ event_data = {
+ "actionIdentifier": "start.timer",
+ "selection": [{"entityId": entity["id"], "entityType": "task"}]
+ }
+ session.event_hub.publish(
+ _ftrack_api.event.base.Event(
+ topic="ftrack.action.launch",
+ data=event_data,
+ source=source
+ ),
+ on_error="ignore"
+ )
+ self.log.debug("Timer start triggered successfully.")
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/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/events/event_sync_to_avalon.py b/pype/modules/ftrack/events/event_sync_to_avalon.py
index 314871f5b3..d0c9ea2e96 100644
--- a/pype/modules/ftrack/events/event_sync_to_avalon.py
+++ b/pype/modules/ftrack/events/event_sync_to_avalon.py
@@ -717,6 +717,9 @@ class SyncToAvalonEvent(BaseEvent):
if not self.ftrack_removed:
return
ent_infos = self.ftrack_removed
+ self.log.debug(
+ "Processing removed entities: {}".format(str(ent_infos))
+ )
removable_ids = []
recreate_ents = []
removed_names = []
@@ -878,8 +881,9 @@ class SyncToAvalonEvent(BaseEvent):
self.process_session.commit()
found_idx = None
- for idx, _entity in enumerate(self._avalon_ents):
- if _entity["_id"] == avalon_entity["_id"]:
+ proj_doc, asset_docs = self._avalon_ents
+ for idx, asset_doc in enumerate(asset_docs):
+ if asset_doc["_id"] == avalon_entity["_id"]:
found_idx = idx
break
@@ -894,7 +898,8 @@ class SyncToAvalonEvent(BaseEvent):
new_entity_id
)
# Update cached entities
- self._avalon_ents[found_idx] = avalon_entity
+ asset_docs[found_idx] = avalon_entity
+ self._avalon_ents = proj_doc, asset_docs
if self._avalon_ents_by_id is not None:
mongo_id = avalon_entity["_id"]
@@ -1258,6 +1263,10 @@ class SyncToAvalonEvent(BaseEvent):
if not ent_infos:
return
+ self.log.debug(
+ "Processing renamed entities: {}".format(str(ent_infos))
+ )
+
renamed_tasks = {}
not_found = {}
changeable_queue = queue.Queue()
@@ -1453,6 +1462,10 @@ class SyncToAvalonEvent(BaseEvent):
if not ent_infos:
return
+ self.log.debug(
+ "Processing added entities: {}".format(str(ent_infos))
+ )
+
cust_attrs, hier_attrs = self.avalon_cust_attrs
entity_type_conf_ids = {}
# Skip if already exit in avalon db or tasks entities
@@ -1729,6 +1742,10 @@ class SyncToAvalonEvent(BaseEvent):
if not self.ftrack_moved:
return
+ self.log.debug(
+ "Processing moved entities: {}".format(str(self.ftrack_moved))
+ )
+
ftrack_moved = {k: v for k, v in sorted(
self.ftrack_moved.items(),
key=(lambda line: len(
@@ -1859,6 +1876,10 @@ class SyncToAvalonEvent(BaseEvent):
if not self.ftrack_updated:
return
+ self.log.debug(
+ "Processing updated entities: {}".format(str(self.ftrack_updated))
+ )
+
ent_infos = self.ftrack_updated
ftrack_mongo_mapping = {}
not_found_ids = []
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 acf31ab437..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)
@@ -205,10 +205,16 @@ class ProcessEventHub(SocketBaseEventHub):
else:
try:
self._handle(event)
+
+ mongo_id = event["data"].get("_event_mongo_id")
+ if mongo_id is None:
+ continue
+
self.dbcon.update_one(
- {"id": event["id"]},
+ {"_id": mongo_id},
{"$set": {"pype_data.is_processed": True}}
)
+
except pymongo.errors.AutoReconnect:
self.pypelog.error((
"Mongo server \"{}\" is not responding, exiting."
@@ -244,6 +250,7 @@ class ProcessEventHub(SocketBaseEventHub):
}
try:
event = ftrack_api.event.base.Event(**new_event_data)
+ event["data"]["_event_mongo_id"] = event_data["_id"]
except Exception:
self.logger.exception(L(
'Failed to convert payload into event: {0}',
diff --git a/pype/modules/ftrack/ftrack_server/sub_event_status.py b/pype/modules/ftrack/ftrack_server/sub_event_status.py
index c2e7b7477f..00a6687de3 100644
--- a/pype/modules/ftrack/ftrack_server/sub_event_status.py
+++ b/pype/modules/ftrack/ftrack_server/sub_event_status.py
@@ -12,7 +12,7 @@ from pype.modules.ftrack.ftrack_server.lib import (
SocketSession, StatusEventHub,
TOPIC_STATUS_SERVER, TOPIC_STATUS_SERVER_RESULT
)
-from pype.api import Logger, config
+from pype.api import Logger
log = Logger().get_logger("Event storer")
action_identifier = (
@@ -23,17 +23,7 @@ action_data = {
"label": "Pype Admin",
"variant": "- Event server Status ({})".format(host_ip),
"description": "Get Infromation about event server",
- "actionIdentifier": action_identifier,
- "icon": "{}/ftrack/action_icons/PypeAdmin.svg".format(
- os.environ.get(
- "PYPE_STATICS_SERVER",
- "http://localhost:{}".format(
- config.get_presets().get("services", {}).get(
- "rest_api", {}
- ).get("default_port", 8021)
- )
- )
- )
+ "actionIdentifier": action_identifier
}
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 40b14a02a8..28114c7fdc 100644
--- a/pype/modules/ftrack/lib/avalon_sync.py
+++ b/pype/modules/ftrack/lib/avalon_sync.py
@@ -1150,7 +1150,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)
@@ -1269,7 +1269,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
@@ -2272,6 +2272,7 @@ class SyncEntitiesFactory:
"name": _name,
"parent": parent_entity
})
+ self.session.commit()
final_entity = {}
for k, v in av_entity.items():
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/hosts/photoshop.py b/pype/modules/websocket_server/hosts/photoshop.py
new file mode 100644
index 0000000000..cdfb9413a0
--- /dev/null
+++ b/pype/modules/websocket_server/hosts/photoshop.py
@@ -0,0 +1,64 @@
+from pype.api import Logger
+from wsrpc_aiohttp import WebSocketRoute
+import functools
+
+import avalon.photoshop as photoshop
+
+log = Logger().get_logger("WebsocketServer")
+
+
+class Photoshop(WebSocketRoute):
+ """
+ One route, mimicking external application (like Harmony, etc).
+ All functions could be called from client.
+ 'do_notify' function calls function on the client - mimicking
+ notification after long running job on the server or similar
+ """
+ instance = None
+
+ def init(self, **kwargs):
+ # Python __init__ must be return "self".
+ # This method might return anything.
+ log.debug("someone called Photoshop route")
+ self.instance = self
+ return kwargs
+
+ # server functions
+ async def ping(self):
+ log.debug("someone called Photoshop route ping")
+
+ # This method calls function on the client side
+ # client functions
+
+ async def read(self):
+ log.debug("photoshop.read client calls server server calls "
+ "Photo client")
+ return await self.socket.call('Photoshop.read')
+
+ # panel routes for tools
+ async def creator_route(self):
+ self._tool_route("creator")
+
+ async def workfiles_route(self):
+ self._tool_route("workfiles")
+
+ async def loader_route(self):
+ self._tool_route("loader")
+
+ async def publish_route(self):
+ self._tool_route("publish")
+
+ async def sceneinventory_route(self):
+ self._tool_route("sceneinventory")
+
+ async def projectmanager_route(self):
+ self._tool_route("projectmanager")
+
+ def _tool_route(self, tool_name):
+ """The address accessed when clicking on the buttons."""
+ partial_method = functools.partial(photoshop.show, tool_name)
+
+ photoshop.execute_in_main_thread(partial_method)
+
+ # Required return statement.
+ return "nothing"
diff --git a/pype/modules/websocket_server/stubs/photoshop_server_stub.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py
new file mode 100644
index 0000000000..da69127799
--- /dev/null
+++ b/pype/modules/websocket_server/stubs/photoshop_server_stub.py
@@ -0,0 +1,283 @@
+from pype.modules.websocket_server import WebSocketServer
+"""
+ Stub handling connection from server to client.
+ Used anywhere solution is calling client methods.
+"""
+import json
+from collections import namedtuple
+
+
+class PhotoshopServerStub():
+ """
+ Stub for calling function on client (Photoshop js) side.
+ Expects that client is already connected (started when avalon menu
+ is opened).
+ 'self.websocketserver.call' is used as async wrapper
+ """
+
+ def __init__(self):
+ self.websocketserver = WebSocketServer.get_instance()
+ self.client = self.websocketserver.get_client()
+
+ def open(self, path):
+ """
+ Open file located at 'path' (local).
+ :param path: file path locally
+ :return: None
+ """
+ self.websocketserver.call(self.client.call
+ ('Photoshop.open', path=path)
+ )
+
+ def read(self, layer, layers_meta=None):
+ """
+ Parses layer metadata from Headline field of active document
+ :param layer: Layer("id": XXX, "name":'YYY')
+ :param data: json representation for single layer
+ :param all_layers: - for performance, could be
+ injected for usage in loop, if not, single call will be
+ triggered
+ :param layers_meta: json representation from Headline
+ (for performance - provide only if imprint is in
+ loop - value should be same)
+ :return: None
+ """
+ if not layers_meta:
+ layers_meta = self.get_layers_metadata()
+ # json.dumps writes integer values in a dictionary to string, so
+ # anticipating it here.
+ if str(layer.id) in layers_meta and layers_meta[str(layer.id)]:
+ layers_meta[str(layer.id)].update(data)
+ else:
+ layers_meta[str(layer.id)] = data
+
+ # Ensure only valid ids are stored.
+ if not all_layers:
+ all_layers = self.get_layers()
+ layer_ids = [layer.id for layer in all_layers]
+ cleaned_data = {}
+
+ for id in layers_meta:
+ if int(id) in layer_ids:
+ cleaned_data[id] = layers_meta[id]
+
+ payload = json.dumps(cleaned_data, indent=4)
+
+ self.websocketserver.call(self.client.call
+ ('Photoshop.imprint', payload=payload)
+ )
+
+ def get_layers(self):
+ """
+ Returns JSON document with all(?) layers in active document.
+
+ :return:
+ Format of tuple: { 'id':'123',
+ 'name': 'My Layer 1',
+ 'type': 'GUIDE'|'FG'|'BG'|'OBJ'
+ 'visible': 'true'|'false'
+ """
+ res = self.websocketserver.call(self.client.call
+ ('Photoshop.get_layers'))
+
+ return self._to_records(res)
+
+ def get_layers_in_layers(self, layers):
+ """
+ Return all layers that belong to layers (might be groups).
+ :param layers:
+ :return:
+ """
+ all_layers = self.get_layers()
+ ret = []
+ parent_ids = set([lay.id for lay in layers])
+
+ for layer in all_layers:
+ parents = set(layer.parents)
+ if len(parent_ids & parents) > 0:
+ ret.append(layer)
+ if layer.id in parent_ids:
+ ret.append(layer)
+
+ return ret
+
+ def create_group(self, name):
+ """
+ Create new group (eg. LayerSet)
+ :return:
+ """
+ ret = self.websocketserver.call(self.client.call
+ ('Photoshop.create_group',
+ name=name))
+ # create group on PS is asynchronous, returns only id
+ layer = {"id": ret, "name": name, "group": True}
+ return namedtuple('Layer', layer.keys())(*layer.values())
+
+ def group_selected_layers(self, name):
+ """
+ Group selected layers into new LayerSet (eg. group)
+ :return:
+ """
+ res = self.websocketserver.call(self.client.call
+ ('Photoshop.group_selected_layers',
+ name=name)
+ )
+ return self._to_records(res)
+
+ def get_selected_layers(self):
+ """
+ Get a list of actually selected layers
+ :return:
+ """
+ res = self.websocketserver.call(self.client.call
+ ('Photoshop.get_selected_layers'))
+ return self._to_records(res)
+
+ def select_layers(self, layers):
+ """
+ Selecte specified layers in Photoshop
+ :param layers:
+ :return: None
+ """
+ layer_ids = [layer.id for layer in layers]
+
+ self.websocketserver.call(self.client.call
+ ('Photoshop.get_layers',
+ layers=layer_ids)
+ )
+
+ def get_active_document_full_name(self):
+ """
+ Returns full name with path of active document via ws call
+ :return: full path with name
+ """
+ res = self.websocketserver.call(
+ self.client.call('Photoshop.get_active_document_full_name'))
+
+ return res
+
+ def get_active_document_name(self):
+ """
+ Returns just a name of active document via ws call
+ :return: file name
+ """
+ res = self.websocketserver.call(self.client.call
+ ('Photoshop.get_active_document_name'))
+
+ return res
+
+ def is_saved(self):
+ """
+ Returns true if no changes in active document
+ :return:
+ """
+ return self.websocketserver.call(self.client.call
+ ('Photoshop.is_saved'))
+
+ def save(self):
+ """
+ Saves active document
+ :return: None
+ """
+ self.websocketserver.call(self.client.call
+ ('Photoshop.save'))
+
+ def saveAs(self, image_path, ext, as_copy):
+ """
+ Saves active document to psd (copy) or png or jpg
+ :param image_path: full local path
+ :param ext:
+ :param as_copy:
+ :return: None
+ """
+ self.websocketserver.call(self.client.call
+ ('Photoshop.saveAs',
+ image_path=image_path,
+ ext=ext,
+ as_copy=as_copy))
+
+ def set_visible(self, layer_id, visibility):
+ """
+ Set layer with 'layer_id' to 'visibility'
+ :param layer_id:
+ :param visibility:
+ :return: None
+ """
+ self.websocketserver.call(self.client.call
+ ('Photoshop.set_visible',
+ layer_id=layer_id,
+ visibility=visibility))
+
+ def get_layers_metadata(self):
+ """
+ Reads layers metadata from Headline from active document in PS.
+ (Headline accessible by File > File Info)
+ :return: - json documents
+ """
+ layers_data = {}
+ res = self.websocketserver.call(self.client.call('Photoshop.read'))
+ try:
+ layers_data = json.loads(res)
+ except json.decoder.JSONDecodeError:
+ pass
+ return layers_data
+
+ def import_smart_object(self, path):
+ """
+ Import the file at `path` as a smart object to active document.
+
+ Args:
+ path (str): File path to import.
+ """
+ res = self.websocketserver.call(self.client.call
+ ('Photoshop.import_smart_object',
+ path=path))
+
+ return self._to_records(res).pop()
+
+ def replace_smart_object(self, layer, path):
+ """
+ Replace the smart object `layer` with file at `path`
+
+ Args:
+ layer (namedTuple): Layer("id":XX, "name":"YY"..).
+ path (str): File to import.
+ """
+ self.websocketserver.call(self.client.call
+ ('Photoshop.replace_smart_object',
+ layer=layer,
+ path=path))
+
+ def close(self):
+ self.client.close()
+
+ def _to_records(self, res):
+ """
+ Converts string json representation into list of named tuples for
+ dot notation access to work.
+ :return:
+ :param res: - json representation
+ """
+ try:
+ layers_data = json.loads(res)
+ except json.decoder.JSONDecodeError:
+ raise ValueError("Received broken JSON {}".format(res))
+ ret = []
+ # convert to namedtuple to use dot donation
+ if isinstance(layers_data, dict): # TODO refactore
+ layers_data = [layers_data]
+ for d in layers_data:
+ ret.append(namedtuple('Layer', d.keys())(*d.values()))
+ return ret
diff --git a/pype/modules/websocket_server/websocket_server.py b/pype/modules/websocket_server/websocket_server.py
index 56e71ea895..daf4b03103 100644
--- a/pype/modules/websocket_server/websocket_server.py
+++ b/pype/modules/websocket_server/websocket_server.py
@@ -1,4 +1,4 @@
-from pype.api import config, Logger
+from pype.api import Logger
import threading
from aiohttp import web
@@ -9,6 +9,7 @@ import os
import sys
import pyclbr
import importlib
+import urllib
log = Logger().get_logger("WebsocketServer")
@@ -19,24 +20,24 @@ class WebSocketServer():
Uses class in external_app_1.py to mimic implementation for single
external application.
'test_client' folder contains two test implementations of client
-
- WIP
"""
+ _instance = None
def __init__(self):
self.qaction = None
self.failed_icon = None
self._is_running = False
- default_port = 8099
+ WebSocketServer._instance = self
+ self.client = None
+ self.handlers = {}
- try:
- self.presets = config.get_presets()["services"]["websocket_server"]
- except Exception:
- self.presets = {"default_port": default_port, "exclude_ports": []}
- log.debug((
- "There are not set presets for WebsocketServer."
- " Using defaults \"{}\""
- ).format(str(self.presets)))
+ port = None
+ websocket_url = os.getenv("WEBSOCKET_URL")
+ if websocket_url:
+ parsed = urllib.parse.urlparse(websocket_url)
+ port = parsed.port
+ if not port:
+ port = 8098 # fallback
self.app = web.Application()
@@ -48,7 +49,7 @@ class WebSocketServer():
directories_with_routes = ['hosts']
self.add_routes_for_directories(directories_with_routes)
- self.websocket_thread = WebsocketServerThread(self, default_port)
+ self.websocket_thread = WebsocketServerThread(self, port)
def add_routes_for_directories(self, directories_with_routes):
""" Loops through selected directories to find all modules and
@@ -78,6 +79,33 @@ class WebSocketServer():
WebSocketAsync.add_route(class_name, cls)
sys.path.pop()
+ def call(self, func):
+ log.debug("websocket.call {}".format(func))
+ future = asyncio.run_coroutine_threadsafe(func,
+ self.websocket_thread.loop)
+ result = future.result()
+ return result
+
+ def get_client(self):
+ """
+ Return first connected client to WebSocket
+ TODO implement selection by Route
+ :return: client
+ """
+ clients = WebSocketAsync.get_clients()
+ client = None
+ if len(clients) > 0:
+ key = list(clients.keys())[0]
+ client = clients.get(key)
+
+ return client
+
+ @staticmethod
+ def get_instance():
+ if WebSocketServer._instance is None:
+ WebSocketServer()
+ return WebSocketServer._instance
+
def tray_start(self):
self.websocket_thread.start()
@@ -124,6 +152,7 @@ class WebsocketServerThread(threading.Thread):
self.loop = None
self.runner = None
self.site = None
+ self.tasks = []
def run(self):
self.is_running = True
@@ -169,6 +198,12 @@ class WebsocketServerThread(threading.Thread):
periodically.
"""
while self.is_running:
+ while self.tasks:
+ task = self.tasks.pop(0)
+ log.debug("waiting for task {}".format(task))
+ await task
+ log.debug("returned value {}".format(task.result))
+
await asyncio.sleep(0.5)
log.debug("Starting shutdown")
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 cc569ce2d1..7d4e0333d6 100644
--- a/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py
+++ b/pype/plugins/ftrack/publish/integrate_hierarchy_ftrack.py
@@ -40,9 +40,11 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
def process(self, context):
self.context = context
- if "hierarchyContext" not in context.data:
+ if "hierarchyContext" not in self.context.data:
return
+ hierarchy_context = self.context.data["hierarchyContext"]
+
self.session = self.context.data["ftrackSession"]
project_name = self.context.data["projectEntity"]["name"]
query = 'Project where full_name is "{}"'.format(project_name)
@@ -55,7 +57,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin):
self.ft_project = None
- input_data = context.data["hierarchyContext"]
+ input_data = hierarchy_context
# disable termporarily ftrack project's autosyncing
if auto_sync_state:
@@ -128,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
@@ -156,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.
@@ -165,8 +169,31 @@ 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.
+ user = self.session.query(
+ "User where username is \"{}\"".format(self.session.api_user)
+ ).first()
+ if user:
+ for comment in entity_data.get("comments", []):
+ entity.create_note(comment, user)
+ else:
+ self.log.warning(
+ "Was not able to query current User {}".format(
+ self.session.api_user
+ )
+ )
+ try:
+ self.session.commit()
+ except Exception:
+ tp, value, tb = sys.exc_info()
+ self.session.rollback()
+ self.session._configure_locations()
+ six.reraise(tp, value, tb)
+
+ # Import children.
if 'childs' in entity_data:
self.import_to_ftrack(
entity_data['childs'], entity)
@@ -180,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.
@@ -221,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
@@ -235,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
@@ -249,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):
@@ -262,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 b43678ff6c..2d82eca6b2 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."""
@@ -10,14 +10,35 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin):
families = ["clip", "shot"]
def process(self, context):
+ # processing starts here
if "hierarchyContext" not in context.data:
self.log.info("skipping IntegrateHierarchyToAvalon")
return
+ hierarchy_context = deepcopy(context.data["hierarchyContext"])
if not io.Session:
io.install()
- input_data = context.data["hierarchyContext"]
+ active_assets = []
+ # filter only the active publishing insatnces
+ for instance in context:
+ if instance.data.get("publish") is False:
+ continue
+
+ if not instance.data.get("asset"):
+ continue
+
+ active_assets.append(instance.data["asset"])
+
+ # remove duplicity in list
+ self.active_assets = list(set(active_assets))
+ self.log.debug("__ self.active_assets: {}".format(self.active_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
self.import_to_avalon(input_data)
@@ -151,3 +172,24 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin):
entity_id = io.insert_one(item).inserted_id
return io.find_one({"_id": entity_id})
+
+ def _get_assets(self, input_dict):
+ """ Returns only asset dictionary.
+ 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
+ input_dict_copy[key]["childs"] = self._get_assets(
+ input_dict[key]["childs"])
+ else:
+ # filter out unwanted assets
+ if key not in self.active_assets:
+ input_dict_copy.pop(key, None)
+
+ return input_dict_copy
diff --git a/pype/plugins/global/publish/extract_jpeg.py b/pype/plugins/global/publish/extract_jpeg.py
index 89a4bbd664..d23ce4360f 100644
--- a/pype/plugins/global/publish/extract_jpeg.py
+++ b/pype/plugins/global/publish/extract_jpeg.py
@@ -81,6 +81,11 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin):
jpeg_items.append("-i {}".format(full_input_path))
# output arguments from presets
jpeg_items.extend(ffmpeg_args.get("output") or [])
+
+ # If its a movie file, we just want one frame.
+ if repre["ext"] == "mov":
+ jpeg_items.append("-vframes 1")
+
# output file
jpeg_items.append(full_output_path)
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/integrate_new.py b/pype/plugins/global/publish/integrate_new.py
index f92968e554..68549e9186 100644
--- a/pype/plugins/global/publish/integrate_new.py
+++ b/pype/plugins/global/publish/integrate_new.py
@@ -682,6 +682,14 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
instance.data.get('subsetGroup')}}
)
+ # Update families on subset.
+ families = [instance.data["family"]]
+ families.extend(instance.data.get("families", []))
+ io.update_many(
+ {"type": "subset", "_id": io.ObjectId(subset["_id"])},
+ {"$set": {"data.families": families}}
+ )
+
return subset
def create_version(self, subset, version_number, data=None):
diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py
index 758872e717..99f0ae7cb6 100644
--- a/pype/plugins/global/publish/submit_publish_job.py
+++ b/pype/plugins/global/publish/submit_publish_job.py
@@ -718,7 +718,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
"resolutionWidth": data.get("resolutionWidth", 1920),
"resolutionHeight": data.get("resolutionHeight", 1080),
"multipartExr": data.get("multipartExr", False),
- "jobBatchName": data.get("jobBatchName", "")
+ "jobBatchName": data.get("jobBatchName", ""),
+ "review": data.get("review", True)
}
if "prerender" in instance.data["families"]:
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/global/publish/validate_version.py b/pype/plugins/global/publish/validate_version.py
index 9c7ce72307..6701041541 100644
--- a/pype/plugins/global/publish/validate_version.py
+++ b/pype/plugins/global/publish/validate_version.py
@@ -10,7 +10,7 @@ class ValidateVersion(pyblish.api.InstancePlugin):
order = pyblish.api.ValidatorOrder
label = "Validate Version"
- hosts = ["nuke", "maya", "blender"]
+ hosts = ["nuke", "maya", "blender", "standalonepublisher"]
def process(self, instance):
version = instance.data.get("version")
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 17a6866f80..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
@@ -12,7 +12,7 @@ class ImagePlaneLoader(api.Loader):
families = ["plate", "render"]
label = "Create imagePlane on selected camera."
- representations = ["mov", "exr", "preview"]
+ representations = ["mov", "exr", "preview", "png"]
icon = "image"
color = "orange"
@@ -29,6 +29,8 @@ class ImagePlaneLoader(api.Loader):
# Getting camera from selection.
selection = pc.ls(selection=True)
+ camera = None
+
if len(selection) > 1:
QtWidgets.QMessageBox.critical(
None,
@@ -39,25 +41,29 @@ class ImagePlaneLoader(api.Loader):
return
if len(selection) < 1:
- QtWidgets.QMessageBox.critical(
+ result = QtWidgets.QMessageBox.critical(
None,
"Error!",
- "No camera selected.",
- QtWidgets.QMessageBox.Ok
+ "No camera selected. Do you want to create a camera?",
+ QtWidgets.QMessageBox.Ok,
+ QtWidgets.QMessageBox.Cancel
)
- return
-
- relatives = pc.listRelatives(selection[0], shapes=True)
- if not pc.ls(relatives, type="camera"):
- QtWidgets.QMessageBox.critical(
- None,
- "Error!",
- "Selected node is not a camera.",
- QtWidgets.QMessageBox.Ok
- )
- return
-
- camera = selection[0]
+ if result == QtWidgets.QMessageBox.Ok:
+ camera = pc.createNode("camera")
+ else:
+ return
+ else:
+ relatives = pc.listRelatives(selection[0], shapes=True)
+ if pc.ls(relatives, type="camera"):
+ camera = selection[0]
+ else:
+ QtWidgets.QMessageBox.critical(
+ None,
+ "Error!",
+ "Selected node is not a camera.",
+ QtWidgets.QMessageBox.Ok
+ )
+ return
try:
camera.displayResolution.set(1)
@@ -81,6 +87,7 @@ class ImagePlaneLoader(api.Loader):
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)
image_plane_shape.useFrameExtension.set(1)
movie_representations = ["mov", "preview"]
@@ -140,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/nuke/publish/extract_thumbnail.py b/pype/plugins/nuke/publish/extract_thumbnail.py
index a3ef09bc9f..a53cb4d146 100644
--- a/pype/plugins/nuke/publish/extract_thumbnail.py
+++ b/pype/plugins/nuke/publish/extract_thumbnail.py
@@ -15,10 +15,12 @@ class ExtractThumbnail(pype.api.Extractor):
order = pyblish.api.ExtractorOrder + 0.01
label = "Extract Thumbnail"
- families = ["review", "render.farm"]
+ families = ["review"]
hosts = ["nuke"]
def process(self, instance):
+ if "render.farm" in instance.data["families"]:
+ return
with anlib.maintained_selection():
self.log.debug("instance: {}".format(instance))
diff --git a/pype/plugins/nukestudio/publish/collect_hierarchy_context.py b/pype/plugins/nukestudio/publish/collect_hierarchy_context.py
index a41e987bdb..930efd618e 100644
--- a/pype/plugins/nukestudio/publish/collect_hierarchy_context.py
+++ b/pype/plugins/nukestudio/publish/collect_hierarchy_context.py
@@ -273,8 +273,6 @@ class CollectHierarchyContext(pyblish.api.ContextPlugin):
instance.data["clipOut"] -
instance.data["clipIn"])
-
-
self.log.debug(
"__ instance.data[parents]: {}".format(
instance.data["parents"]
@@ -319,6 +317,7 @@ class CollectHierarchyContext(pyblish.api.ContextPlugin):
})
in_info['tasks'] = instance.data['tasks']
+ in_info["comments"] = instance.data.get("comments", [])
parents = instance.data.get('parents', [])
self.log.debug("__ in_info: {}".format(in_info))
diff --git a/pype/plugins/nukestudio/publish/collect_tag_comments.py b/pype/plugins/nukestudio/publish/collect_tag_comments.py
index 1ec98e3d3b..e14e53d439 100644
--- a/pype/plugins/nukestudio/publish/collect_tag_comments.py
+++ b/pype/plugins/nukestudio/publish/collect_tag_comments.py
@@ -17,7 +17,7 @@ class CollectClipTagComments(api.InstancePlugin):
for tag in instance.data["tags"]:
if tag["name"].lower() == "comment":
instance.data["comments"].append(
- tag.metadata().dict()["tag.note"]
+ tag["metadata"]["tag.note"]
)
# Find tags on the source clip.
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/photoshop/create/create_image.py b/pype/plugins/photoshop/create/create_image.py
index 5b2f9f7981..c1a7d92a2c 100644
--- a/pype/plugins/photoshop/create/create_image.py
+++ b/pype/plugins/photoshop/create/create_image.py
@@ -1,5 +1,6 @@
-from avalon import api, photoshop
+from avalon import api
from avalon.vendor import Qt
+from avalon import photoshop
class CreateImage(api.Creator):
@@ -13,11 +14,12 @@ class CreateImage(api.Creator):
groups = []
layers = []
create_group = False
- group_constant = photoshop.get_com_objects().constants().psLayerSet
+
+ stub = photoshop.stub()
if (self.options or {}).get("useSelection"):
multiple_instances = False
- selection = photoshop.get_selected_layers()
-
+ selection = stub.get_selected_layers()
+ self.log.info("selection {}".format(selection))
if len(selection) > 1:
# Ask user whether to create one image or image per selected
# item.
@@ -40,19 +42,18 @@ class CreateImage(api.Creator):
if multiple_instances:
for item in selection:
- if item.LayerType == group_constant:
+ if item.group:
groups.append(item)
else:
layers.append(item)
else:
- group = photoshop.group_selected_layers()
- group.Name = self.name
+ group = stub.group_selected_layers(self.name)
groups.append(group)
elif len(selection) == 1:
# One selected item. Use group if its a LayerSet (group), else
# create a new group.
- if selection[0].LayerType == group_constant:
+ if selection[0].group:
groups.append(selection[0])
else:
layers.append(selection[0])
@@ -63,16 +64,14 @@ class CreateImage(api.Creator):
create_group = True
if create_group:
- group = photoshop.app().ActiveDocument.LayerSets.Add()
- group.Name = self.name
+ group = stub.create_group(self.name)
groups.append(group)
for layer in layers:
- photoshop.select_layers([layer])
- group = photoshop.group_selected_layers()
- group.Name = layer.Name
+ stub.select_layers([layer])
+ group = stub.group_selected_layers(layer.name)
groups.append(group)
for group in groups:
- self.data.update({"subset": "image" + group.Name})
- photoshop.imprint(group, self.data)
+ self.data.update({"subset": "image" + group.name})
+ stub.imprint(group, self.data)
diff --git a/pype/plugins/photoshop/load/load_image.py b/pype/plugins/photoshop/load/load_image.py
index 18efe750d5..75c02bb327 100644
--- a/pype/plugins/photoshop/load/load_image.py
+++ b/pype/plugins/photoshop/load/load_image.py
@@ -1,5 +1,7 @@
from avalon import api, photoshop
+stub = photoshop.stub()
+
class ImageLoader(api.Loader):
"""Load images
@@ -12,7 +14,7 @@ class ImageLoader(api.Loader):
def load(self, context, name=None, namespace=None, data=None):
with photoshop.maintained_selection():
- layer = photoshop.import_smart_object(self.fname)
+ layer = stub.import_smart_object(self.fname)
self[:] = [layer]
@@ -28,11 +30,11 @@ class ImageLoader(api.Loader):
layer = container.pop("layer")
with photoshop.maintained_selection():
- photoshop.replace_smart_object(
+ stub.replace_smart_object(
layer, api.get_representation_path(representation)
)
- photoshop.imprint(
+ stub.imprint(
layer, {"representation": str(representation["_id"])}
)
diff --git a/pype/plugins/photoshop/publish/collect_current_file.py b/pype/plugins/photoshop/publish/collect_current_file.py
index 4308588559..3cc3e3f636 100644
--- a/pype/plugins/photoshop/publish/collect_current_file.py
+++ b/pype/plugins/photoshop/publish/collect_current_file.py
@@ -1,6 +1,7 @@
import os
import pyblish.api
+
from avalon import photoshop
@@ -13,5 +14,5 @@ class CollectCurrentFile(pyblish.api.ContextPlugin):
def process(self, context):
context.data["currentFile"] = os.path.normpath(
- photoshop.app().ActiveDocument.FullName
+ photoshop.stub().get_active_document_full_name()
).replace("\\", "/")
diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py
index 4937f2a1e4..81d1c80bf6 100644
--- a/pype/plugins/photoshop/publish/collect_instances.py
+++ b/pype/plugins/photoshop/publish/collect_instances.py
@@ -1,9 +1,9 @@
import pythoncom
-from avalon import photoshop
-
import pyblish.api
+from avalon import photoshop
+
class CollectInstances(pyblish.api.ContextPlugin):
"""Gather instances by LayerSet and file metadata
@@ -27,8 +27,11 @@ class CollectInstances(pyblish.api.ContextPlugin):
# can be.
pythoncom.CoInitialize()
- for layer in photoshop.get_layers_in_document():
- layer_data = photoshop.read(layer)
+ stub = photoshop.stub()
+ layers = stub.get_layers()
+ layers_meta = stub.get_layers_metadata()
+ for layer in layers:
+ layer_data = stub.read(layer, layers_meta)
# Skip layers without metadata.
if layer_data is None:
@@ -38,18 +41,19 @@ class CollectInstances(pyblish.api.ContextPlugin):
if "container" in layer_data["id"]:
continue
- child_layers = [*layer.Layers]
- if not child_layers:
- self.log.info("%s skipped, it was empty." % layer.Name)
- continue
+ # child_layers = [*layer.Layers]
+ # self.log.debug("child_layers {}".format(child_layers))
+ # if not child_layers:
+ # self.log.info("%s skipped, it was empty." % layer.Name)
+ # continue
- instance = context.create_instance(layer.Name)
+ instance = context.create_instance(layer.name)
instance.append(layer)
instance.data.update(layer_data)
instance.data["families"] = self.families_mapping[
layer_data["family"]
]
- instance.data["publish"] = layer.Visible
+ instance.data["publish"] = layer.visible
# Produce diagnostic message for any graphical
# user interface interested in visualising it.
diff --git a/pype/plugins/photoshop/publish/extract_image.py b/pype/plugins/photoshop/publish/extract_image.py
index 6dfccdc4f2..38920b5557 100644
--- a/pype/plugins/photoshop/publish/extract_image.py
+++ b/pype/plugins/photoshop/publish/extract_image.py
@@ -21,35 +21,37 @@ class ExtractImage(pype.api.Extractor):
self.log.info("Outputting image to {}".format(staging_dir))
# Perform extraction
+ stub = photoshop.stub()
files = {}
with photoshop.maintained_selection():
self.log.info("Extracting %s" % str(list(instance)))
with photoshop.maintained_visibility():
# Hide all other layers.
- extract_ids = [
- x.id for x in photoshop.get_layers_in_layers([instance[0]])
- ]
- for layer in photoshop.get_layers_in_document():
- if layer.id not in extract_ids:
- layer.Visible = False
+ extract_ids = set([ll.id for ll in stub.
+ get_layers_in_layers([instance[0]])])
- save_options = {}
+ for layer in stub.get_layers():
+ # limit unnecessary calls to client
+ if layer.visible and layer.id not in extract_ids:
+ stub.set_visible(layer.id, False)
+ if not layer.visible and layer.id in extract_ids:
+ stub.set_visible(layer.id, True)
+
+ save_options = []
if "png" in self.formats:
- save_options["png"] = photoshop.com_objects.PNGSaveOptions()
+ save_options.append('png')
if "jpg" in self.formats:
- save_options["jpg"] = photoshop.com_objects.JPEGSaveOptions()
+ save_options.append('jpg')
file_basename = os.path.splitext(
- photoshop.app().ActiveDocument.Name
+ stub.get_active_document_name()
)[0]
- for extension, save_option in save_options.items():
+ for extension in save_options:
_filename = "{}.{}".format(file_basename, extension)
files[extension] = _filename
full_filename = os.path.join(staging_dir, _filename)
- photoshop.app().ActiveDocument.SaveAs(
- full_filename, save_option, True
- )
+ stub.saveAs(full_filename, extension, True)
representations = []
for extension, filename in files.items():
diff --git a/pype/plugins/photoshop/publish/extract_review.py b/pype/plugins/photoshop/publish/extract_review.py
index 078ee53899..6fb50bba9f 100644
--- a/pype/plugins/photoshop/publish/extract_review.py
+++ b/pype/plugins/photoshop/publish/extract_review.py
@@ -13,10 +13,11 @@ class ExtractReview(pype.api.Extractor):
families = ["review"]
def process(self, instance):
-
staging_dir = self.staging_dir(instance)
self.log.info("Outputting image to {}".format(staging_dir))
+ stub = photoshop.stub()
+
layers = []
for image_instance in instance.context:
if image_instance.data["family"] != "image":
@@ -25,25 +26,22 @@ class ExtractReview(pype.api.Extractor):
# Perform extraction
output_image = "{}.jpg".format(
- os.path.splitext(photoshop.app().ActiveDocument.Name)[0]
+ os.path.splitext(stub.get_active_document_name())[0]
)
output_image_path = os.path.join(staging_dir, output_image)
with photoshop.maintained_visibility():
# Hide all other layers.
- extract_ids = [
- x.id for x in photoshop.get_layers_in_layers(layers)
- ]
- for layer in photoshop.get_layers_in_document():
- if layer.id in extract_ids:
- layer.Visible = True
- else:
- layer.Visible = False
+ extract_ids = set([ll.id for ll in stub.
+ get_layers_in_layers(layers)])
+ self.log.info("extract_ids {}".format(extract_ids))
+ for layer in stub.get_layers():
+ # limit unnecessary calls to client
+ if layer.visible and layer.id not in extract_ids:
+ stub.set_visible(layer.id, False)
+ if not layer.visible and layer.id in extract_ids:
+ stub.set_visible(layer.id, True)
- photoshop.app().ActiveDocument.SaveAs(
- output_image_path,
- photoshop.com_objects.JPEGSaveOptions(),
- True
- )
+ stub.saveAs(output_image_path, 'jpg', True)
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
@@ -66,8 +64,6 @@ class ExtractReview(pype.api.Extractor):
]
output = pype.lib._subprocess(args)
- self.log.debug(output)
-
instance.data["representations"].append({
"name": "thumbnail",
"ext": "jpg",
@@ -75,7 +71,6 @@ class ExtractReview(pype.api.Extractor):
"stagingDir": staging_dir,
"tags": ["thumbnail"]
})
-
# Generate mov.
mov_path = os.path.join(staging_dir, "review.mov")
args = [
@@ -86,9 +81,7 @@ class ExtractReview(pype.api.Extractor):
mov_path
]
output = pype.lib._subprocess(args)
-
self.log.debug(output)
-
instance.data["representations"].append({
"name": "mov",
"ext": "mov",
diff --git a/pype/plugins/photoshop/publish/extract_save_scene.py b/pype/plugins/photoshop/publish/extract_save_scene.py
index b3d4f0e447..63a4b7b7ea 100644
--- a/pype/plugins/photoshop/publish/extract_save_scene.py
+++ b/pype/plugins/photoshop/publish/extract_save_scene.py
@@ -11,4 +11,4 @@ class ExtractSaveScene(pype.api.Extractor):
families = ["workfile"]
def process(self, instance):
- photoshop.app().ActiveDocument.Save()
+ photoshop.stub().save()
diff --git a/pype/plugins/photoshop/publish/increment_workfile.py b/pype/plugins/photoshop/publish/increment_workfile.py
index ba9ab8606a..eca2583595 100644
--- a/pype/plugins/photoshop/publish/increment_workfile.py
+++ b/pype/plugins/photoshop/publish/increment_workfile.py
@@ -1,6 +1,7 @@
import pyblish.api
from pype.action import get_errored_plugins_from_data
from pype.lib import version_up
+
from avalon import photoshop
@@ -24,6 +25,6 @@ class IncrementWorkfile(pyblish.api.InstancePlugin):
)
scene_path = version_up(instance.context.data["currentFile"])
- photoshop.app().ActiveDocument.SaveAs(scene_path)
+ photoshop.stub().saveAs(scene_path, 'psd', True)
self.log.info("Incremented workfile to: {}".format(scene_path))
diff --git a/pype/plugins/photoshop/publish/validate_instance_asset.py b/pype/plugins/photoshop/publish/validate_instance_asset.py
index ab1d02269f..f05d9601dd 100644
--- a/pype/plugins/photoshop/publish/validate_instance_asset.py
+++ b/pype/plugins/photoshop/publish/validate_instance_asset.py
@@ -23,11 +23,12 @@ class ValidateInstanceAssetRepair(pyblish.api.Action):
# Apply pyblish.logic to get the instances for the plug-in
instances = pyblish.api.instances_by_plugin(failed, plugin)
-
+ stub = photoshop.stub()
for instance in instances:
- data = photoshop.read(instance[0])
+ data = stub.read(instance[0])
+
data["asset"] = os.environ["AVALON_ASSET"]
- photoshop.imprint(instance[0], data)
+ stub.imprint(instance[0], data)
class ValidateInstanceAsset(pyblish.api.InstancePlugin):
diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py
index 51e00da352..2483adcb5e 100644
--- a/pype/plugins/photoshop/publish/validate_naming.py
+++ b/pype/plugins/photoshop/publish/validate_naming.py
@@ -21,13 +21,14 @@ class ValidateNamingRepair(pyblish.api.Action):
# Apply pyblish.logic to get the instances for the plug-in
instances = pyblish.api.instances_by_plugin(failed, plugin)
-
+ stub = photoshop.stub()
for instance in instances:
+ self.log.info("validate_naming instance {}".format(instance))
name = instance.data["name"].replace(" ", "_")
instance[0].Name = name
- data = photoshop.read(instance[0])
+ data = stub.read(instance[0])
data["subset"] = "image" + name
- photoshop.imprint(instance[0], data)
+ stub.imprint(instance[0], data)
return True
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_context.py b/pype/plugins/standalonepublisher/publish/collect_context.py
index a5479fdf13..9dbeec93fb 100644
--- a/pype/plugins/standalonepublisher/publish/collect_context.py
+++ b/pype/plugins/standalonepublisher/publish/collect_context.py
@@ -123,7 +123,7 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin):
"label": subset,
"name": subset,
"family": in_data["family"],
- "version": in_data.get("version", 1),
+ # "version": in_data.get("version", 1),
"frameStart": in_data.get("representations", [None])[0].get(
"frameStart", None
),
diff --git a/pype/plugins/standalonepublisher/publish/collect_editorial.py b/pype/plugins/standalonepublisher/publish/collect_editorial.py
index a31125d9a8..5e6fd106e4 100644
--- a/pype/plugins/standalonepublisher/publish/collect_editorial.py
+++ b/pype/plugins/standalonepublisher/publish/collect_editorial.py
@@ -32,7 +32,7 @@ class CollectEditorial(pyblish.api.InstancePlugin):
actions = []
# presets
- extensions = [".mov"]
+ extensions = [".mov", ".mp4"]
def process(self, instance):
# remove context test attribute
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/collect_psd_instances.py b/pype/plugins/standalonepublisher/publish/collect_psd_instances.py
index 9c8e2eae83..b5db437473 100644
--- a/pype/plugins/standalonepublisher/publish/collect_psd_instances.py
+++ b/pype/plugins/standalonepublisher/publish/collect_psd_instances.py
@@ -9,7 +9,7 @@ class CollectPsdInstances(pyblish.api.InstancePlugin):
"""
label = "Collect Psd Instances"
- order = pyblish.api.CollectorOrder + 0.492
+ order = pyblish.api.CollectorOrder + 0.489
hosts = ["standalonepublisher"]
families = ["background_batch"]
@@ -34,8 +34,6 @@ class CollectPsdInstances(pyblish.api.InstancePlugin):
context = instance.context
asset_data = instance.data["assetEntity"]
asset_name = instance.data["asset"]
- anatomy_data = instance.data["anatomyData"]
-
for subset_name, subset_data in self.subsets.items():
instance_name = f"{asset_name}_{subset_name}"
task = subset_data.get("task", "background")
@@ -55,16 +53,8 @@ class CollectPsdInstances(pyblish.api.InstancePlugin):
new_instance.data["label"] = f"{instance_name}"
new_instance.data["subset"] = subset_name
+ new_instance.data["task"] = task
- # fix anatomy data
- anatomy_data_new = copy.deepcopy(anatomy_data)
- # updating hierarchy data
- anatomy_data_new.update({
- "asset": asset_data["name"],
- "task": task,
- "subset": subset_name
- })
- new_instance.data["anatomyData"] = anatomy_data_new
if subset_name in self.unchecked_by_default:
new_instance.data["publish"] = False
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/resources/app_icons/maya.png b/pype/resources/app_icons/maya.png
index e84a6a3742..95c605f50d 100644
Binary files a/pype/resources/app_icons/maya.png and b/pype/resources/app_icons/maya.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
new file mode 100644
index 0000000000..7e73d541a4
--- /dev/null
+++ b/pype/settings/__init__.py
@@ -0,0 +1,9 @@
+from .lib import (
+ system_settings,
+ project_settings
+)
+
+__all__ = (
+ "system_settings",
+ "project_settings"
+)
diff --git a/pype/settings/defaults/project_anatomy/colorspace.json b/pype/settings/defaults/project_anatomy/colorspace.json
new file mode 100644
index 0000000000..8b934f810d
--- /dev/null
+++ b/pype/settings/defaults/project_anatomy/colorspace.json
@@ -0,0 +1,42 @@
+{
+ "nuke": {
+ "root": {
+ "colorManagement": "Nuke",
+ "OCIO_config": "nuke-default",
+ "defaultViewerLUT": "Nuke Root LUTs",
+ "monitorLut": "sRGB",
+ "int8Lut": "sRGB",
+ "int16Lut": "sRGB",
+ "logLut": "Cineon",
+ "floatLut": "linear"
+ },
+ "viewer": {
+ "viewerProcess": "sRGB"
+ },
+ "write": {
+ "render": {
+ "colorspace": "linear"
+ },
+ "prerender": {
+ "colorspace": "linear"
+ },
+ "still": {
+ "colorspace": "sRGB"
+ }
+ },
+ "read": {
+ "[^-a-zA-Z0-9]beauty[^-a-zA-Z0-9]": "linear",
+ "[^-a-zA-Z0-9](P|N|Z|crypto)[^-a-zA-Z0-9]": "linear",
+ "[^-a-zA-Z0-9](plateRef)[^-a-zA-Z0-9]": "sRGB"
+ }
+ },
+ "maya": {
+
+ },
+ "houdini": {
+
+ },
+ "resolve": {
+
+ }
+}
diff --git a/pype/settings/defaults/project_anatomy/dataflow.json b/pype/settings/defaults/project_anatomy/dataflow.json
new file mode 100644
index 0000000000..d2f470b5bc
--- /dev/null
+++ b/pype/settings/defaults/project_anatomy/dataflow.json
@@ -0,0 +1,55 @@
+{
+ "nuke": {
+ "nodes": {
+ "connected": true,
+ "modifymetadata": {
+ "_id": "connect_metadata",
+ "_previous": "ENDING",
+ "metadata.set.pype_studio_name": "{PYPE_STUDIO_NAME}",
+ "metadata.set.avalon_project_name": "{AVALON_PROJECT}",
+ "metadata.set.avalon_project_code": "{PYPE_STUDIO_CODE}",
+ "metadata.set.avalon_asset_name": "{AVALON_ASSET}"
+ },
+ "crop": {
+ "_id": "connect_crop",
+ "_previous": "connect_metadata",
+ "box": [
+ "{metadata.crop.x}",
+ "{metadata.crop.y}",
+ "{metadata.crop.right}",
+ "{metadata.crop.top}"
+ ]
+ },
+ "write": {
+ "render": {
+ "_id": "output_write",
+ "_previous": "connect_crop",
+ "file_type": "exr",
+ "datatype": "16 bit half",
+ "compression": "Zip (1 scanline)",
+ "autocrop": true,
+ "tile_color": "0xff0000ff",
+ "channels": "rgb"
+ },
+ "prerender": {
+ "_id": "output_write",
+ "_previous": "connect_crop",
+ "file_type": "exr",
+ "datatype": "16 bit half",
+ "compression": "Zip (1 scanline)",
+ "autocrop": false,
+ "tile_color": "0xc9892aff",
+ "channels": "rgba"
+ },
+ "still": {
+ "_previous": "connect_crop",
+ "channels": "rgba",
+ "file_type": "tiff",
+ "datatype": "16 bit",
+ "compression": "LZW",
+ "tile_color": "0x4145afff"
+ }
+ }
+ }
+ }
+}
diff --git a/pype/settings/defaults/project_anatomy/roots.json b/pype/settings/defaults/project_anatomy/roots.json
new file mode 100644
index 0000000000..0282471a60
--- /dev/null
+++ b/pype/settings/defaults/project_anatomy/roots.json
@@ -0,0 +1,5 @@
+{
+ "windows": "C:/projects",
+ "linux": "/mnt/share/projects",
+ "darwin": "/Volumes/path"
+}
diff --git a/pype/settings/defaults/project_anatomy/templates.json b/pype/settings/defaults/project_anatomy/templates.json
new file mode 100644
index 0000000000..0fff0265b3
--- /dev/null
+++ b/pype/settings/defaults/project_anatomy/templates.json
@@ -0,0 +1,30 @@
+{
+ "version_padding": 3,
+ "version": "v{version:0>{@version_padding}}",
+ "frame_padding": 4,
+ "frame": "{frame:0>{@frame_padding}}",
+ "work": {
+ "folder": "{root}/{project[name]}/{hierarchy}/{asset}/work/{task}",
+ "file": "{project[code]}_{asset}_{task}_{@version}<_{comment}>.{ext}",
+ "path": "{@folder}/{@file}"
+ },
+ "render": {
+ "folder": "{root}/{project[name]}/{hierarchy}/{asset}/publish/render/{subset}/{@version}",
+ "file": "{project[code]}_{asset}_{subset}_{@version}<_{output}><.{@frame}>.{representation}",
+ "path": "{@folder}/{@file}"
+ },
+ "texture": {
+ "path": "{root}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}"
+ },
+ "publish": {
+ "folder": "{root}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}",
+ "file": "{project[code]}_{asset}_{subset}_{@version}<_{output}><.{@frame}>.{representation}",
+ "path": "{@folder}/{@file}",
+ "thumbnail": "{thumbnail_root}/{project[name]}/{_id}_{thumbnail_type}{ext}"
+ },
+ "master": {
+ "folder": "{root}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/master",
+ "file": "{project[code]}_{asset}_{subset}_master<_{output}><.{frame}>.{representation}",
+ "path": "{@folder}/{@file}"
+ }
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/project_settings/ftrack/ftrack_config.json b/pype/settings/defaults/project_settings/ftrack/ftrack_config.json
new file mode 100644
index 0000000000..1ef3a9d69f
--- /dev/null
+++ b/pype/settings/defaults/project_settings/ftrack/ftrack_config.json
@@ -0,0 +1,16 @@
+{
+ "sync_to_avalon": {
+ "statuses_name_change": ["not ready", "ready"]
+ },
+
+ "status_update": {
+ "_ignore_": ["in progress", "ommited", "on hold"],
+ "Ready": ["not ready"],
+ "In Progress" : ["_any_"]
+ },
+ "status_version_to_task": {
+ "__description__": "Status `from` (key) must be lowered!",
+ "in progress": "in progress",
+ "approved": "approved"
+ }
+}
diff --git a/pype/settings/defaults/project_settings/ftrack/ftrack_custom_attributes.json b/pype/settings/defaults/project_settings/ftrack/ftrack_custom_attributes.json
new file mode 100644
index 0000000000..f03d473cd0
--- /dev/null
+++ b/pype/settings/defaults/project_settings/ftrack/ftrack_custom_attributes.json
@@ -0,0 +1,165 @@
+[{
+ "label": "FPS",
+ "key": "fps",
+ "type": "number",
+ "is_hierarchical": true,
+ "group": "avalon",
+ "write_security_role": ["ALL"],
+ "read_security_role": ["ALL"],
+ "default": null,
+ "config": {
+ "isdecimal": true
+ }
+}, {
+ "label": "Applications",
+ "key": "applications",
+ "type": "enumerator",
+ "entity_type": "show",
+ "group": "avalon",
+ "config": {
+ "multiselect": true,
+ "data": [
+ {"blender_2.80": "Blender 2.80"},
+ {"blender_2.81": "Blender 2.81"},
+ {"blender_2.82": "Blender 2.82"},
+ {"blender_2.83": "Blender 2.83"},
+ {"celaction_local": "CelAction2D Local"},
+ {"maya_2017": "Maya 2017"},
+ {"maya_2018": "Maya 2018"},
+ {"maya_2019": "Maya 2019"},
+ {"nuke_10.0": "Nuke 10.0"},
+ {"nuke_11.2": "Nuke 11.2"},
+ {"nuke_11.3": "Nuke 11.3"},
+ {"nuke_12.0": "Nuke 12.0"},
+ {"nukex_10.0": "NukeX 10.0"},
+ {"nukex_11.2": "NukeX 11.2"},
+ {"nukex_11.3": "NukeX 11.3"},
+ {"nukex_12.0": "NukeX 12.0"},
+ {"nukestudio_10.0": "NukeStudio 10.0"},
+ {"nukestudio_11.2": "NukeStudio 11.2"},
+ {"nukestudio_11.3": "NukeStudio 11.3"},
+ {"nukestudio_12.0": "NukeStudio 12.0"},
+ {"harmony_17": "Harmony 17"},
+ {"houdini_16.5": "Houdini 16.5"},
+ {"houdini_17": "Houdini 17"},
+ {"houdini_18": "Houdini 18"},
+ {"photoshop_2020": "Photoshop 2020"},
+ {"python_3": "Python 3"},
+ {"python_2": "Python 2"},
+ {"premiere_2019": "Premiere Pro 2019"},
+ {"premiere_2020": "Premiere Pro 2020"},
+ {"resolve_16": "BM DaVinci Resolve 16"}
+ ]
+ }
+}, {
+ "label": "Avalon auto-sync",
+ "key": "avalon_auto_sync",
+ "type": "boolean",
+ "entity_type": "show",
+ "group": "avalon",
+ "write_security_role": ["API", "Administrator"],
+ "read_security_role": ["API", "Administrator"]
+}, {
+ "label": "Intent",
+ "key": "intent",
+ "type": "enumerator",
+ "entity_type": "assetversion",
+ "group": "avalon",
+ "config": {
+ "multiselect": false,
+ "data": [
+ {"test": "Test"},
+ {"wip": "WIP"},
+ {"final": "Final"}
+ ]
+ }
+}, {
+ "label": "Library Project",
+ "key": "library_project",
+ "type": "boolean",
+ "entity_type": "show",
+ "group": "avalon",
+ "write_security_role": ["API", "Administrator"],
+ "read_security_role": ["API", "Administrator"]
+}, {
+ "label": "Clip in",
+ "key": "clipIn",
+ "type": "number",
+ "is_hierarchical": true,
+ "group": "avalon",
+ "default": null
+}, {
+ "label": "Clip out",
+ "key": "clipOut",
+ "type": "number",
+ "is_hierarchical": true,
+ "group": "avalon",
+ "default": null
+}, {
+ "label": "Frame start",
+ "key": "frameStart",
+ "type": "number",
+ "is_hierarchical": true,
+ "group": "avalon",
+ "default": null
+}, {
+ "label": "Frame end",
+ "key": "frameEnd",
+ "type": "number",
+ "is_hierarchical": true,
+ "group": "avalon",
+ "default": null
+}, {
+ "label": "Tools",
+ "key": "tools_env",
+ "type": "enumerator",
+ "is_hierarchical": true,
+ "group": "avalon",
+ "config": {
+ "multiselect": true,
+ "data": [
+ {"mtoa_3.0.1": "mtoa_3.0.1"},
+ {"mtoa_3.1.1": "mtoa_3.1.1"},
+ {"mtoa_3.2.0": "mtoa_3.2.0"},
+ {"yeti_2.1.2": "yeti_2.1"}
+ ]
+ }
+}, {
+ "label": "Resolution Width",
+ "key": "resolutionWidth",
+ "type": "number",
+ "is_hierarchical": true,
+ "group": "avalon",
+ "default": null
+}, {
+ "label": "Resolution Height",
+ "key": "resolutionHeight",
+ "type": "number",
+ "is_hierarchical": true,
+ "group": "avalon",
+ "default": null
+}, {
+ "label": "Pixel aspect",
+ "key": "pixelAspect",
+ "type": "number",
+ "is_hierarchical": true,
+ "group": "avalon",
+ "config": {
+ "isdecimal": true
+ }
+}, {
+ "label": "Frame handles start",
+ "key": "handleStart",
+ "type": "number",
+ "is_hierarchical": true,
+ "group": "avalon",
+ "default": null
+}, {
+ "label": "Frame handles end",
+ "key": "handleEnd",
+ "type": "number",
+ "is_hierarchical": true,
+ "group": "avalon",
+ "default": null
+}
+]
diff --git a/pype/settings/defaults/project_settings/ftrack/partnership_ftrack_cred.json b/pype/settings/defaults/project_settings/ftrack/partnership_ftrack_cred.json
new file mode 100644
index 0000000000..6b3a32f181
--- /dev/null
+++ b/pype/settings/defaults/project_settings/ftrack/partnership_ftrack_cred.json
@@ -0,0 +1,5 @@
+{
+ "server_url": "",
+ "api_key": "",
+ "api_user": ""
+}
diff --git a/pype/settings/defaults/project_settings/ftrack/plugins/server.json b/pype/settings/defaults/project_settings/ftrack/plugins/server.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/pype/settings/defaults/project_settings/ftrack/plugins/server.json
@@ -0,0 +1 @@
+{}
diff --git a/pype/settings/defaults/project_settings/ftrack/plugins/user.json b/pype/settings/defaults/project_settings/ftrack/plugins/user.json
new file mode 100644
index 0000000000..1ba8e9b511
--- /dev/null
+++ b/pype/settings/defaults/project_settings/ftrack/plugins/user.json
@@ -0,0 +1,5 @@
+{
+ "TestAction": {
+ "ignore_me": true
+ }
+}
diff --git a/pype/settings/defaults/project_settings/ftrack/project_defaults.json b/pype/settings/defaults/project_settings/ftrack/project_defaults.json
new file mode 100644
index 0000000000..a4e3aa3362
--- /dev/null
+++ b/pype/settings/defaults/project_settings/ftrack/project_defaults.json
@@ -0,0 +1,18 @@
+{
+ "fps": 25,
+ "frameStart": 1001,
+ "frameEnd": 1100,
+ "clipIn": 1001,
+ "clipOut": 1100,
+ "handleStart": 10,
+ "handleEnd": 10,
+
+ "resolutionHeight": 1080,
+ "resolutionWidth": 1920,
+ "pixelAspect": 1.0,
+ "applications": [
+ "maya_2019", "nuke_11.3", "nukex_11.3", "nukestudio_11.3", "deadline"
+ ],
+ "tools_env": [],
+ "avalon_auto_sync": true
+}
diff --git a/pype/settings/defaults/project_settings/global/creator.json b/pype/settings/defaults/project_settings/global/creator.json
new file mode 100644
index 0000000000..d14e779f01
--- /dev/null
+++ b/pype/settings/defaults/project_settings/global/creator.json
@@ -0,0 +1,8 @@
+{
+ "Model": ["model"],
+ "Render Globals": ["light", "render"],
+ "Layout": ["layout"],
+ "Set Dress": ["setdress"],
+ "Look": ["look"],
+ "Rig": ["rigging"]
+}
diff --git a/pype/settings/defaults/project_settings/global/project_folder_structure.json b/pype/settings/defaults/project_settings/global/project_folder_structure.json
new file mode 100644
index 0000000000..83bd5f12a9
--- /dev/null
+++ b/pype/settings/defaults/project_settings/global/project_folder_structure.json
@@ -0,0 +1,22 @@
+{
+ "__project_root__": {
+ "prod" : {},
+ "resources" : {
+ "footage": {
+ "plates": {},
+ "offline": {}
+ },
+ "audio": {},
+ "art_dept": {}
+ },
+ "editorial" : {},
+ "assets[ftrack.Library]": {
+ "characters[ftrack]": {},
+ "locations[ftrack]": {}
+ },
+ "shots[ftrack.Sequence]": {
+ "scripts": {},
+ "editorial[ftrack.Folder]": {}
+ }
+ }
+}
diff --git a/pype/settings/defaults/project_settings/global/sw_folders.json b/pype/settings/defaults/project_settings/global/sw_folders.json
new file mode 100644
index 0000000000..a154935dce
--- /dev/null
+++ b/pype/settings/defaults/project_settings/global/sw_folders.json
@@ -0,0 +1,8 @@
+{
+ "compositing": ["nuke", "ae"],
+ "modeling": ["maya", "app2"],
+ "lookdev": ["substance"],
+ "animation": [],
+ "lighting": [],
+ "rigging": []
+}
diff --git a/pype/settings/defaults/project_settings/global/workfiles.json b/pype/settings/defaults/project_settings/global/workfiles.json
new file mode 100644
index 0000000000..393b2e3c10
--- /dev/null
+++ b/pype/settings/defaults/project_settings/global/workfiles.json
@@ -0,0 +1,7 @@
+{
+ "last_workfile_on_startup": [
+ {
+ "enabled": false
+ }
+ ]
+}
diff --git a/pype/settings/defaults/project_settings/maya/capture.json b/pype/settings/defaults/project_settings/maya/capture.json
new file mode 100644
index 0000000000..b6c4893034
--- /dev/null
+++ b/pype/settings/defaults/project_settings/maya/capture.json
@@ -0,0 +1,108 @@
+{
+ "Codec": {
+ "compression": "jpg",
+ "format": "image",
+ "quality": 95
+ },
+ "Display Options": {
+ "background": [
+ 0.7137254901960784,
+ 0.7137254901960784,
+ 0.7137254901960784
+ ],
+ "backgroundBottom": [
+ 0.7137254901960784,
+ 0.7137254901960784,
+ 0.7137254901960784
+ ],
+ "backgroundTop": [
+ 0.7137254901960784,
+ 0.7137254901960784,
+ 0.7137254901960784
+ ],
+ "override_display": true
+ },
+ "Generic": {
+ "isolate_view": true,
+ "off_screen": true
+ },
+ "IO": {
+ "name": "",
+ "open_finished": false,
+ "raw_frame_numbers": false,
+ "recent_playblasts": [],
+ "save_file": false
+ },
+ "PanZoom": {
+ "pan_zoom": true
+ },
+ "Renderer": {
+ "rendererName": "vp2Renderer"
+ },
+ "Resolution": {
+ "height": 1080,
+ "mode": "Custom",
+ "percent": 1.0,
+ "width": 1920
+ },
+ "Time Range": {
+ "end_frame": 25,
+ "frame": "",
+ "start_frame": 0,
+ "time": "Time Slider"
+ },
+ "Viewport Options": {
+ "cameras": false,
+ "clipGhosts": false,
+ "controlVertices": false,
+ "deformers": false,
+ "dimensions": false,
+ "displayLights": 0,
+ "dynamicConstraints": false,
+ "dynamics": false,
+ "fluids": false,
+ "follicles": false,
+ "gpuCacheDisplayFilter": false,
+ "greasePencils": false,
+ "grid": false,
+ "hairSystems": false,
+ "handles": false,
+ "high_quality": true,
+ "hud": false,
+ "hulls": false,
+ "ikHandles": false,
+ "imagePlane": false,
+ "joints": false,
+ "lights": false,
+ "locators": false,
+ "manipulators": false,
+ "motionTrails": false,
+ "nCloths": false,
+ "nParticles": false,
+ "nRigids": false,
+ "nurbsCurves": false,
+ "nurbsSurfaces": false,
+ "override_viewport_options": true,
+ "particleInstancers": false,
+ "pivots": false,
+ "planes": false,
+ "pluginShapes": false,
+ "polymeshes": true,
+ "shadows": false,
+ "strokes": false,
+ "subdivSurfaces": false,
+ "textures": false,
+ "twoSidedLighting": true
+ },
+ "Camera Options": {
+ "displayGateMask": false,
+ "displayResolution": false,
+ "displayFilmGate": false,
+ "displayFieldChart": false,
+ "displaySafeAction": false,
+ "displaySafeTitle": false,
+ "displayFilmPivot": false,
+ "displayFilmOrigin": false,
+ "overscan": 1.0
+ }
+}
diff --git a/pype/settings/defaults/project_settings/muster/templates_mapping.json b/pype/settings/defaults/project_settings/muster/templates_mapping.json
new file mode 100644
index 0000000000..4edab9077d
--- /dev/null
+++ b/pype/settings/defaults/project_settings/muster/templates_mapping.json
@@ -0,0 +1,19 @@
+{
+ "3delight": 41,
+ "arnold": 46,
+ "arnold_sf": 57,
+ "gelato": 30,
+ "harware": 3,
+ "krakatoa": 51,
+ "file_layers": 7,
+ "mentalray": 2,
+ "mentalray_sf": 6,
+ "redshift": 55,
+ "renderman": 29,
+ "software": 1,
+ "software_sf": 5,
+ "turtle": 10,
+ "vector": 4,
+ "vray": 37,
+ "ffmpeg": 48
+}
diff --git a/pype/settings/defaults/project_settings/plugins/celaction/publish.json b/pype/settings/defaults/project_settings/plugins/celaction/publish.json
new file mode 100644
index 0000000000..fd1af23d84
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/celaction/publish.json
@@ -0,0 +1,11 @@
+{
+ "ExtractCelactionDeadline": {
+ "enabled": true,
+ "deadline_department": "",
+ "deadline_priority": 50,
+ "deadline_pool": "",
+ "deadline_pool_secondary": "",
+ "deadline_group": "",
+ "deadline_chunk_size": 10
+ }
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/project_settings/plugins/config.json b/pype/settings/defaults/project_settings/plugins/config.json
new file mode 100644
index 0000000000..9e26dfeeb6
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/config.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/pype/settings/defaults/project_settings/plugins/ftrack/publish.json b/pype/settings/defaults/project_settings/plugins/ftrack/publish.json
new file mode 100644
index 0000000000..d8d93a36ee
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/ftrack/publish.json
@@ -0,0 +1,7 @@
+{
+ "IntegrateFtrackNote": {
+ "enabled": false,
+ "note_with_intent_template": "{intent}: {comment}",
+ "note_labels": []
+ }
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/project_settings/plugins/global/create.json b/pype/settings/defaults/project_settings/plugins/global/create.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/global/create.json
@@ -0,0 +1 @@
+{}
diff --git a/pype/settings/defaults/project_settings/plugins/global/filter.json b/pype/settings/defaults/project_settings/plugins/global/filter.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/global/filter.json
@@ -0,0 +1 @@
+{}
diff --git a/pype/settings/defaults/project_settings/plugins/global/load.json b/pype/settings/defaults/project_settings/plugins/global/load.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/global/load.json
@@ -0,0 +1 @@
+{}
diff --git a/pype/settings/defaults/project_settings/plugins/global/publish.json b/pype/settings/defaults/project_settings/plugins/global/publish.json
new file mode 100644
index 0000000000..0a7f6fbf3d
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/global/publish.json
@@ -0,0 +1,98 @@
+{
+ "IntegrateMasterVersion": {
+ "enabled": false
+ },
+ "ExtractJpegEXR": {
+ "enabled": true,
+ "ffmpeg_args": {
+ "input": [
+ "-gamma 2.2"
+ ],
+ "output": []
+ }
+ },
+ "ExtractReview": {
+ "enabled": true,
+ "profiles": [
+ {
+ "families": [],
+ "hosts": [],
+ "outputs": {
+ "h264": {
+ "ext": "mp4",
+ "tags": [
+ "burnin",
+ "ftrackreview"
+ ],
+ "ffmpeg_args": {
+ "video_filters": [],
+ "audio_filters": [],
+ "input": [
+ "-gamma 2.2"
+ ],
+ "output": [
+ "-pix_fmt yuv420p",
+ "-crf 18",
+ "-intra"
+ ]
+ },
+ "filter": {
+ "families": [
+ "render",
+ "review",
+ "ftrack"
+ ]
+ }
+ }
+ }
+ }
+ ]
+ },
+ "ExtractBurnin": {
+ "enabled": false,
+ "options": {
+ "font_size": 42,
+ "opacity": 1,
+ "bg_opacity": 0,
+ "x_offset": 5,
+ "y_offset": 5,
+ "bg_padding": 5
+ },
+ "fields": {},
+ "profiles": [
+ {
+ "burnins": {
+ "burnin": {
+ "TOP_LEFT": "{yy}-{mm}-{dd}",
+ "TOP_RIGHT": "{anatomy[version]}",
+ "TOP_CENTERED": "",
+ "BOTTOM_RIGHT": "{frame_start}-{current_frame}-{frame_end}",
+ "BOTTOM_CENTERED": "{asset}",
+ "BOTTOM_LEFT": "{username}"
+ }
+ }
+ }
+ ]
+ },
+ "IntegrateAssetNew": {
+ "template_name_profiles": {
+ "publish": {
+ "families": [],
+ "tasks": []
+ },
+ "render": {
+ "families": [
+ "review",
+ "render",
+ "prerender"
+ ]
+ }
+ }
+ },
+ "ProcessSubmittedJobOnFarm": {
+ "enabled": false,
+ "deadline_department": "",
+ "deadline_pool": "",
+ "deadline_group": ""
+ }
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/project_settings/plugins/maya/create.json b/pype/settings/defaults/project_settings/plugins/maya/create.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/maya/create.json
@@ -0,0 +1 @@
+{}
diff --git a/pype/settings/defaults/project_settings/plugins/maya/filter.json b/pype/settings/defaults/project_settings/plugins/maya/filter.json
new file mode 100644
index 0000000000..83d6f05f31
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/maya/filter.json
@@ -0,0 +1,9 @@
+{
+ "Preset n1": {
+ "ValidateNoAnimation": false,
+ "ValidateShapeDefaultNames": false
+ },
+ "Preset n2": {
+ "ValidateNoAnimation": false
+ }
+}
diff --git a/pype/settings/defaults/project_settings/plugins/maya/load.json b/pype/settings/defaults/project_settings/plugins/maya/load.json
new file mode 100644
index 0000000000..260fbb35ee
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/maya/load.json
@@ -0,0 +1,18 @@
+{
+ "colors": {
+ "model": [0.821, 0.518, 0.117],
+ "rig": [0.144, 0.443, 0.463],
+ "pointcache": [0.368, 0.821, 0.117],
+ "animation": [0.368, 0.821, 0.117],
+ "ass": [1.0, 0.332, 0.312],
+ "camera": [0.447, 0.312, 1.0],
+ "fbx": [1.0, 0.931, 0.312],
+ "mayaAscii": [0.312, 1.0, 0.747],
+ "setdress": [0.312, 1.0, 0.747],
+ "layout": [0.312, 1.0, 0.747],
+ "vdbcache": [0.312, 1.0, 0.428],
+ "vrayproxy": [0.258, 0.95, 0.541],
+ "yeticache": [0.2, 0.8, 0.3],
+ "yetiRig": [0, 0.8, 0.5]
+ }
+}
diff --git a/pype/settings/defaults/project_settings/plugins/maya/publish.json b/pype/settings/defaults/project_settings/plugins/maya/publish.json
new file mode 100644
index 0000000000..2b3637ff80
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/maya/publish.json
@@ -0,0 +1,17 @@
+{
+ "ValidateModelName": {
+ "enabled": false,
+ "material_file": "/path/to/shader_name_definition.txt",
+ "regex": "(.*)_(\\d)*_(?P.*)_(GEO)"
+ },
+ "ValidateAssemblyName": {
+ "enabled": false
+ },
+ "ValidateShaderName": {
+ "enabled": false,
+ "regex": "(?P.*)_(.*)_SHD"
+ },
+ "ValidateMeshHasOverlappingUVs": {
+ "enabled": false
+ }
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/project_settings/plugins/maya/workfile_build.json b/pype/settings/defaults/project_settings/plugins/maya/workfile_build.json
new file mode 100644
index 0000000000..443bc2cb2c
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/maya/workfile_build.json
@@ -0,0 +1,136 @@
+[
+ {
+ "tasks": [
+ "lighting"
+ ],
+ "current_context": [
+ {
+ "subset_name_filters": [
+ ".+[Mm]ain"
+ ],
+ "families": [
+ "model"
+ ],
+ "repre_names": [
+ "abc",
+ "ma"
+ ],
+ "loaders": [
+ "ReferenceLoader"
+ ]
+ },
+ {
+ "families": [
+ "animation",
+ "pointcache"
+ ],
+ "repre_names": [
+ "abc"
+ ],
+ "loaders": [
+ "ReferenceLoader"
+ ]
+ },
+ {
+ "families": [
+ "rendersetup"
+ ],
+ "repre_names": [
+ "json"
+ ],
+ "loaders": [
+ "RenderSetupLoader"
+ ]
+ },
+ {
+ "families": [
+ "camera"
+ ],
+ "repre_names": [
+ "abc"
+ ],
+ "loaders": [
+ "ReferenceLoader"
+ ]
+ }
+ ],
+ "linked_assets": [
+ {
+ "families": [
+ "setdress"
+ ],
+ "repre_names": [
+ "ma"
+ ],
+ "loaders": [
+ "ReferenceLoader"
+ ]
+ },
+ {
+ "families": [
+ "ass"
+ ],
+ "repre_names": [
+ "ass"
+ ],
+ "loaders": [
+ "assLoader"
+ ]
+ }
+ ]
+ },
+ {
+ "tasks": [
+ "animation"
+ ],
+ "current_context": [
+ {
+ "families": [
+ "camera"
+ ],
+ "repre_names": [
+ "abc",
+ "ma"
+ ],
+ "loaders": [
+ "ReferenceLoader"
+ ]
+ },
+ {
+ "families": [
+ "audio"
+ ],
+ "repre_names": [
+ "wav"
+ ],
+ "loaders": [
+ "RenderSetupLoader"
+ ]
+ }
+ ],
+ "linked_assets": [
+ {
+ "families": [
+ "setdress"
+ ],
+ "repre_names": [
+ "proxy"
+ ],
+ "loaders": [
+ "ReferenceLoader"
+ ]
+ },
+ {
+ "families": [
+ "rig"
+ ],
+ "repre_names": [
+ "ass"
+ ],
+ "loaders": [
+ "rigLoader"
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/pype/settings/defaults/project_settings/plugins/nuke/create.json b/pype/settings/defaults/project_settings/plugins/nuke/create.json
new file mode 100644
index 0000000000..79ab665696
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/nuke/create.json
@@ -0,0 +1,8 @@
+{
+ "CreateWriteRender": {
+ "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}"
+ },
+ "CreateWritePrerender": {
+ "fpath_template": "{work}/prerenders/nuke/{subset}/{subset}.{frame}.{ext}"
+ }
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/project_settings/plugins/nuke/load.json b/pype/settings/defaults/project_settings/plugins/nuke/load.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/nuke/load.json
@@ -0,0 +1 @@
+{}
diff --git a/pype/settings/defaults/project_settings/plugins/nuke/publish.json b/pype/settings/defaults/project_settings/plugins/nuke/publish.json
new file mode 100644
index 0000000000..08a099a0a0
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/nuke/publish.json
@@ -0,0 +1,53 @@
+{
+ "ExtractThumbnail": {
+ "enabled": true,
+ "nodes": {
+ "Reformat": [
+ [
+ "type",
+ "to format"
+ ],
+ [
+ "format",
+ "HD_1080"
+ ],
+ [
+ "filter",
+ "Lanczos6"
+ ],
+ [
+ "black_outside",
+ true
+ ],
+ [
+ "pbb",
+ false
+ ]
+ ]
+ }
+ },
+ "ValidateNukeWriteKnobs": {
+ "enabled": false,
+ "knobs": {
+ "render": {
+ "review": true
+ }
+ }
+ },
+ "ExtractReviewDataLut": {
+ "enabled": false
+ },
+ "ExtractReviewDataMov": {
+ "enabled": true,
+ "viewer_lut_raw": false
+ },
+ "ExtractSlateFrame": {
+ "viewer_lut_raw": false
+ },
+ "NukeSubmitDeadline": {
+ "deadline_priority": 50,
+ "deadline_pool": "",
+ "deadline_pool_secondary": "",
+ "deadline_chunk_size": 1
+ }
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/project_settings/plugins/nuke/workfile_build.json b/pype/settings/defaults/project_settings/plugins/nuke/workfile_build.json
new file mode 100644
index 0000000000..4b48b46184
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/nuke/workfile_build.json
@@ -0,0 +1,23 @@
+[
+ {
+ "tasks": [
+ "compositing"
+ ],
+ "current_context": [
+ {
+ "families": [
+ "render",
+ "plate"
+ ],
+ "repre_names": [
+ "exr",
+ "dpx"
+ ],
+ "loaders": [
+ "LoadSequence"
+ ]
+ }
+ ],
+ "linked_assets": []
+ }
+]
\ No newline at end of file
diff --git a/pype/settings/defaults/project_settings/plugins/nukestudio/filter.json b/pype/settings/defaults/project_settings/plugins/nukestudio/filter.json
new file mode 100644
index 0000000000..bd6a0dc1bd
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/nukestudio/filter.json
@@ -0,0 +1,10 @@
+{
+ "strict": {
+ "ValidateVersion": true,
+ "VersionUpWorkfile": true
+ },
+ "benevolent": {
+ "ValidateVersion": false,
+ "VersionUpWorkfile": false
+ }
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/project_settings/plugins/nukestudio/publish.json b/pype/settings/defaults/project_settings/plugins/nukestudio/publish.json
new file mode 100644
index 0000000000..d99a878c35
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/nukestudio/publish.json
@@ -0,0 +1,9 @@
+{
+ "CollectInstanceVersion": {
+ "enabled": false
+ },
+ "ExtractReviewCutUpVideo": {
+ "enabled": true,
+ "tags_addition": []
+ }
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/project_settings/plugins/resolve/create.json b/pype/settings/defaults/project_settings/plugins/resolve/create.json
new file mode 100644
index 0000000000..8ff5b15714
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/resolve/create.json
@@ -0,0 +1,7 @@
+{
+ "CreateShotClip": {
+ "clipName": "{track}{sequence}{shot}",
+ "folder": "takes",
+ "steps": 20
+ }
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/project_settings/plugins/standalonepublisher/publish.json b/pype/settings/defaults/project_settings/plugins/standalonepublisher/publish.json
new file mode 100644
index 0000000000..2f1a3e7aca
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/standalonepublisher/publish.json
@@ -0,0 +1,27 @@
+{
+ "ExtractThumbnailSP": {
+ "ffmpeg_args": {
+ "input": [
+ "-gamma 2.2"
+ ],
+ "output": []
+ }
+ },
+ "ExtractReviewSP": {
+ "outputs": {
+ "h264": {
+ "input": [
+ "-gamma 2.2"
+ ],
+ "output": [
+ "-pix_fmt yuv420p",
+ "-crf 18"
+ ],
+ "tags": [
+ "preview"
+ ],
+ "ext": "mov"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/project_settings/plugins/test/create.json b/pype/settings/defaults/project_settings/plugins/test/create.json
new file mode 100644
index 0000000000..fa0b2fc05f
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/test/create.json
@@ -0,0 +1,8 @@
+{
+ "MyTestCreator": {
+ "my_test_property": "B",
+ "active": false,
+ "new_property": "new",
+ "family": "new_family"
+ }
+}
diff --git a/pype/settings/defaults/project_settings/plugins/test/publish.json b/pype/settings/defaults/project_settings/plugins/test/publish.json
new file mode 100644
index 0000000000..3180dd5d8a
--- /dev/null
+++ b/pype/settings/defaults/project_settings/plugins/test/publish.json
@@ -0,0 +1,10 @@
+{
+ "MyTestPlugin": {
+ "label": "loaded from preset",
+ "optional": true,
+ "families": ["changed", "by", "preset"]
+ },
+ "MyTestRemovedPlugin": {
+ "enabled": false
+ }
+}
diff --git a/pype/settings/defaults/project_settings/premiere/asset_default.json b/pype/settings/defaults/project_settings/premiere/asset_default.json
new file mode 100644
index 0000000000..84d2bde3d8
--- /dev/null
+++ b/pype/settings/defaults/project_settings/premiere/asset_default.json
@@ -0,0 +1,5 @@
+{
+ "frameStart": 1001,
+ "handleStart": 0,
+ "handleEnd": 0
+}
diff --git a/pype/settings/defaults/project_settings/premiere/rules_tasks.json b/pype/settings/defaults/project_settings/premiere/rules_tasks.json
new file mode 100644
index 0000000000..333c9cd70b
--- /dev/null
+++ b/pype/settings/defaults/project_settings/premiere/rules_tasks.json
@@ -0,0 +1,21 @@
+{
+ "defaultTasks": ["Layout", "Animation"],
+ "taskToSubsets": {
+ "Layout": ["reference", "audio"],
+ "Animation": ["audio"]
+ },
+ "subsetToRepresentations": {
+ "reference": {
+ "preset": "h264",
+ "representation": "mp4"
+ },
+ "thumbnail": {
+ "preset": "jpeg_thumb",
+ "representation": "jpg"
+ },
+ "audio": {
+ "preset": "48khz",
+ "representation": "wav"
+ }
+ }
+}
diff --git a/pype/settings/defaults/project_settings/standalonepublisher/families.json b/pype/settings/defaults/project_settings/standalonepublisher/families.json
new file mode 100644
index 0000000000..d05941cc26
--- /dev/null
+++ b/pype/settings/defaults/project_settings/standalonepublisher/families.json
@@ -0,0 +1,90 @@
+{
+ "create_look": {
+ "name": "look",
+ "label": "Look",
+ "family": "look",
+ "icon": "paint-brush",
+ "defaults": ["Main"],
+ "help": "Shader connections defining shape look"
+ },
+ "create_model": {
+ "name": "model",
+ "label": "Model",
+ "family": "model",
+ "icon": "cube",
+ "defaults": ["Main", "Proxy", "Sculpt"],
+ "help": "Polygonal static geometry"
+ },
+ "create_workfile": {
+ "name": "workfile",
+ "label": "Workfile",
+ "family": "workfile",
+ "icon": "cube",
+ "defaults": ["Main"],
+ "help": "Working scene backup"
+ },
+ "create_camera": {
+ "name": "camera",
+ "label": "Camera",
+ "family": "camera",
+ "icon": "video-camera",
+ "defaults": ["Main"],
+ "help": "Single baked camera"
+ },
+ "create_pointcache": {
+ "name": "pointcache",
+ "label": "Pointcache",
+ "family": "pointcache",
+ "icon": "gears",
+ "defaults": ["Main"],
+ "help": "Alembic pointcache for animated data"
+ },
+ "create_rig": {
+ "name": "rig",
+ "label": "Rig",
+ "family": "rig",
+ "icon": "wheelchair",
+ "defaults": ["Main"],
+ "help": "Artist-friendly rig with controls"
+ },
+ "create_layout": {
+ "name": "layout",
+ "label": "Layout",
+ "family": "layout",
+ "icon": "cubes",
+ "defaults": ["Main"],
+ "help": "Simple scene for animators with camera"
+ },
+ "create_plate": {
+ "name": "plate",
+ "label": "Plate",
+ "family": "plate",
+ "icon": "camera",
+ "defaults": ["Main", "BG", "Reference"],
+ "help": "Plates for compositors"
+ },
+ "create_matchmove": {
+ "name": "matchmove",
+ "label": "Matchmove script",
+ "family": "matchmove",
+ "icon": "empire",
+ "defaults": ["Camera", "Object", "Mocap"],
+ "help": "Script exported from matchmoving application"
+ },
+ "create_images": {
+ "name": "image",
+ "label": "Image file",
+ "family": "image",
+ "icon": "image",
+ "defaults": ["ConceptArt", "Reference", "Texture", "MattePaint"],
+ "help": "Holder for all kinds of image data"
+ },
+ "create_editorial": {
+ "name": "editorial",
+ "label": "Editorial",
+ "family": "editorial",
+ "icon": "image",
+ "defaults": ["Main"],
+ "help": "Editorial files to generate shots."
+ }
+}
diff --git a/pype/settings/defaults/project_settings/tools/slates/example_HD.json b/pype/settings/defaults/project_settings/tools/slates/example_HD.json
new file mode 100644
index 0000000000..b06391fb63
--- /dev/null
+++ b/pype/settings/defaults/project_settings/tools/slates/example_HD.json
@@ -0,0 +1,212 @@
+{
+ "width": 1920,
+ "height": 1080,
+ "destination_path": "{destination_path}",
+ "style": {
+ "*": {
+ "font-family": "arial",
+ "font-color": "#ffffff",
+ "font-bold": false,
+ "font-italic": false,
+ "bg-color": "#0077ff",
+ "alignment-horizontal": "left",
+ "alignment-vertical": "top"
+ },
+ "layer": {
+ "padding": 0,
+ "margin": 0
+ },
+ "rectangle": {
+ "padding": 0,
+ "margin": 0,
+ "bg-color": "#E9324B",
+ "fill": true
+ },
+ "main_frame": {
+ "padding": 0,
+ "margin": 0,
+ "bg-color": "#252525"
+ },
+ "table": {
+ "padding": 0,
+ "margin": 0,
+ "bg-color": "transparent"
+ },
+ "table-item": {
+ "padding": 5,
+ "padding-bottom": 10,
+ "margin": 0,
+ "bg-color": "#212121",
+ "bg-alter-color": "#272727",
+ "font-color": "#dcdcdc",
+ "font-bold": false,
+ "font-italic": false,
+ "alignment-horizontal": "left",
+ "alignment-vertical": "top",
+ "word-wrap": false,
+ "ellide": true,
+ "max-lines": 1
+ },
+ "table-item-col[0]": {
+ "font-size": 20,
+ "font-color": "#898989",
+ "font-bold": true,
+ "ellide": false,
+ "word-wrap": true,
+ "max-lines": null
+ },
+ "table-item-col[1]": {
+ "font-size": 40,
+ "padding-left": 10
+ },
+ "#colorbar": {
+ "bg-color": "#9932CC"
+ }
+ },
+ "items": [{
+ "type": "layer",
+ "direction": 1,
+ "name": "MainLayer",
+ "style": {
+ "#MainLayer": {
+ "width": 1094,
+ "height": 1000,
+ "margin": 25,
+ "padding": 0
+ },
+ "#LeftSide": {
+ "margin-right": 25
+ }
+ },
+ "items": [{
+ "type": "layer",
+ "name": "LeftSide",
+ "items": [{
+ "type": "layer",
+ "direction": 1,
+ "style": {
+ "table-item": {
+ "bg-color": "transparent",
+ "padding-bottom": 20
+ },
+ "table-item-col[0]": {
+ "font-size": 20,
+ "font-color": "#898989",
+ "alignment-horizontal": "right"
+ },
+ "table-item-col[1]": {
+ "alignment-horizontal": "left",
+ "font-bold": true,
+ "font-size": 40
+ }
+ },
+ "items": [{
+ "type": "table",
+ "values": [
+ ["Show:", "{project[name]}"]
+ ],
+ "style": {
+ "table-item-field[0:0]": {
+ "width": 150
+ },
+ "table-item-field[0:1]": {
+ "width": 580
+ }
+ }
+ }, {
+ "type": "table",
+ "values": [
+ ["Submitting For:", "{intent}"]
+ ],
+ "style": {
+ "table-item-field[0:0]": {
+ "width": 160
+ },
+ "table-item-field[0:1]": {
+ "width": 218,
+ "alignment-horizontal": "right"
+ }
+ }
+ }]
+ }, {
+ "type": "rectangle",
+ "style": {
+ "bg-color": "#bc1015",
+ "width": 1108,
+ "height": 5,
+ "fill": true
+ }
+ }, {
+ "type": "table",
+ "use_alternate_color": true,
+ "values": [
+ ["Version name:", "{version_name}"],
+ ["Date:", "{date}"],
+ ["Shot Types:", "{shot_type}"],
+ ["Submission Note:", "{submission_note}"]
+ ],
+ "style": {
+ "table-item": {
+ "padding-bottom": 20
+ },
+ "table-item-field[0:1]": {
+ "font-bold": true
+ },
+ "table-item-field[3:0]": {
+ "word-wrap": true,
+ "ellide": true,
+ "max-lines": 4
+ },
+ "table-item-col[0]": {
+ "alignment-horizontal": "right",
+ "width": 150
+ },
+ "table-item-col[1]": {
+ "alignment-horizontal": "left",
+ "width": 958
+ }
+ }
+ }]
+ }, {
+ "type": "layer",
+ "name": "RightSide",
+ "items": [{
+ "type": "placeholder",
+ "name": "thumbnail",
+ "path": "{thumbnail_path}",
+ "style": {
+ "width": 730,
+ "height": 412
+ }
+ }, {
+ "type": "placeholder",
+ "name": "colorbar",
+ "path": "{color_bar_path}",
+ "return_data": true,
+ "style": {
+ "width": 730,
+ "height": 55
+ }
+ }, {
+ "type": "table",
+ "use_alternate_color": true,
+ "values": [
+ ["Vendor:", "{vendor}"],
+ ["Shot Name:", "{shot_name}"],
+ ["Frames:", "{frame_start} - {frame_end} ({duration})"]
+ ],
+ "style": {
+ "table-item-col[0]": {
+ "alignment-horizontal": "left",
+ "width": 200
+ },
+ "table-item-col[1]": {
+ "alignment-horizontal": "right",
+ "width": 530,
+ "font-size": 30
+ }
+ }
+ }]
+ }]
+ }]
+}
diff --git a/pype/settings/defaults/project_settings/unreal/project_setup.json b/pype/settings/defaults/project_settings/unreal/project_setup.json
new file mode 100644
index 0000000000..8a4dffc526
--- /dev/null
+++ b/pype/settings/defaults/project_settings/unreal/project_setup.json
@@ -0,0 +1,4 @@
+{
+ "dev_mode": false,
+ "install_unreal_python_engine": false
+}
diff --git a/pype/settings/defaults/system_settings/environments/avalon.json b/pype/settings/defaults/system_settings/environments/avalon.json
new file mode 100644
index 0000000000..832ba07e71
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/avalon.json
@@ -0,0 +1,16 @@
+{
+ "AVALON_CONFIG": "pype",
+ "AVALON_PROJECTS": "{PYPE_PROJECTS_PATH}",
+ "AVALON_USERNAME": "avalon",
+ "AVALON_PASSWORD": "secret",
+ "AVALON_DEBUG": "1",
+ "AVALON_MONGO": "mongodb://localhost:2707",
+ "AVALON_DB": "avalon",
+ "AVALON_DB_DATA": "{PYPE_SETUP_PATH}/../mongo_db_data",
+ "AVALON_EARLY_ADOPTER": "1",
+ "AVALON_SCHEMA": "{PYPE_MODULE_ROOT}/schema",
+ "AVALON_LOCATION": "http://127.0.0.1",
+ "AVALON_LABEL": "Pype",
+ "AVALON_TIMEOUT": "1000",
+ "AVALON_THUMBNAIL_ROOT": ""
+}
diff --git a/pype/settings/defaults/system_settings/environments/blender.json b/pype/settings/defaults/system_settings/environments/blender.json
new file mode 100644
index 0000000000..6f4f6a012d
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/blender.json
@@ -0,0 +1,7 @@
+{
+ "BLENDER_USER_SCRIPTS": "{PYPE_SETUP_PATH}/repos/avalon-core/setup/blender",
+ "PYTHONPATH": [
+ "{PYPE_SETUP_PATH}/repos/avalon-core/setup/blender",
+ "{PYTHONPATH}"
+ ]
+}
diff --git a/pype/settings/defaults/system_settings/environments/celaction.json b/pype/settings/defaults/system_settings/environments/celaction.json
new file mode 100644
index 0000000000..cdd4e609ab
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/celaction.json
@@ -0,0 +1,3 @@
+{
+ "CELACTION_TEMPLATE": "{PYPE_MODULE_ROOT}/pype/hosts/celaction/celaction_template_scene.scn"
+}
diff --git a/pype/settings/defaults/system_settings/environments/deadline.json b/pype/settings/defaults/system_settings/environments/deadline.json
new file mode 100644
index 0000000000..e8ef52805b
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/deadline.json
@@ -0,0 +1,3 @@
+{
+ "DEADLINE_REST_URL": "http://localhost:8082"
+}
diff --git a/pype/settings/defaults/system_settings/environments/ftrack.json b/pype/settings/defaults/system_settings/environments/ftrack.json
new file mode 100644
index 0000000000..4f25de027b
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/ftrack.json
@@ -0,0 +1,18 @@
+{
+ "FTRACK_SERVER": "https://pype.ftrackapp.com",
+ "FTRACK_ACTIONS_PATH": [
+ "{PYPE_MODULE_ROOT}/pype/modules/ftrack/actions"
+ ],
+ "FTRACK_EVENTS_PATH": [
+ "{PYPE_MODULE_ROOT}/pype/modules/ftrack/events"
+ ],
+ "PYTHONPATH": [
+ "{PYPE_MODULE_ROOT}/pype/vendor",
+ "{PYTHONPATH}"
+ ],
+ "PYBLISHPLUGINPATH": [
+ "{PYPE_MODULE_ROOT}/pype/plugins/ftrack/publish"
+ ],
+ "FTRACK_EVENTS_MONGO_DB": "pype",
+ "FTRACK_EVENTS_MONGO_COL": "ftrack_events"
+}
diff --git a/pype/settings/defaults/system_settings/environments/global.json b/pype/settings/defaults/system_settings/environments/global.json
new file mode 100644
index 0000000000..ef528e6857
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/global.json
@@ -0,0 +1,44 @@
+{
+ "PYPE_STUDIO_NAME": "Studio Name",
+ "PYPE_STUDIO_CODE": "stu",
+ "PYPE_APP_ROOT": "{PYPE_SETUP_PATH}/pypeapp",
+ "PYPE_MODULE_ROOT": "{PYPE_SETUP_PATH}/repos/pype",
+ "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"
+ },
+ "DJV_PATH": {
+ "windows": [
+ "C:/Program Files/djv-1.1.0-Windows-64/bin/djv_view.exe",
+ "C:/Program Files/DJV/bin/djv_view.exe",
+ "{STUDIO_SOFT}/djv/windows/bin/djv_view.exe"
+ ],
+ "linux": [
+ "usr/local/djv/djv_view",
+ "{STUDIO_SOFT}/djv/linux/bin/djv_view"
+ ],
+ "darwin": "Application/DJV.app/Contents/MacOS/DJV"
+ },
+ "PATH": [
+ "{PYPE_CONFIG}/launchers",
+ "{PYPE_APP_ROOT}",
+ "{FFMPEG_PATH}",
+ "{PATH}"
+ ],
+ "PYPE_OCIO_CONFIG": "{STUDIO_SOFT}/OpenColorIO-Configs",
+ "PYTHONPATH": {
+ "windows": "{VIRTUAL_ENV}/Lib/site-packages;{PYPE_MODULE_ROOT}/pype/tools;{PYTHONPATH}",
+ "linux": "{VIRTUAL_ENV}/lib/python{PYTHON_VERSION}/site-packages:{PYPE_MODULE_ROOT}/pype/tools:{PYTHONPATH}",
+ "darwin": "{VIRTUAL_ENV}/lib/python{PYTHON_VERSION}/site-packages:{PYPE_MODULE_ROOT}/pype/tools:{PYTHONPATH}"
+ },
+ "PYPE_PROJECT_CONFIGS": "{PYPE_SETUP_PATH}/../studio-project-configs",
+ "PYPE_PYTHON_EXE": {
+ "windows": "{VIRTUAL_ENV}/Scripts/python.exe",
+ "linux": "{VIRTUAL_ENV}/Scripts/python",
+ "darwin": "{VIRTUAL_ENV}/bin/python"
+ },
+ "PYBLISH_GUI": "pyblish_pype"
+}
diff --git a/pype/settings/defaults/system_settings/environments/harmony.json b/pype/settings/defaults/system_settings/environments/harmony.json
new file mode 100644
index 0000000000..d394343935
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/harmony.json
@@ -0,0 +1,4 @@
+{
+ "AVALON_HARMONY_WORKFILES_ON_LAUNCH": "1",
+ "PYBLISH_GUI_ALWAYS_EXEC": "1"
+}
diff --git a/pype/settings/defaults/system_settings/environments/houdini.json b/pype/settings/defaults/system_settings/environments/houdini.json
new file mode 100644
index 0000000000..95c7d19088
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/houdini.json
@@ -0,0 +1,12 @@
+{
+ "HOUDINI_PATH": {
+ "darwin": "{PYPE_MODULE_ROOT}/setup/houdini:&",
+ "linux": "{PYPE_MODULE_ROOT}/setup/houdini:&",
+ "windows": "{PYPE_MODULE_ROOT}/setup/houdini;&"
+ },
+ "HOUDINI_MENU_PATH": {
+ "darwin": "{PYPE_MODULE_ROOT}/setup/houdini:&",
+ "linux": "{PYPE_MODULE_ROOT}/setup/houdini:&",
+ "windows": "{PYPE_MODULE_ROOT}/setup/houdini;&"
+ }
+}
diff --git a/pype/settings/defaults/system_settings/environments/maya.json b/pype/settings/defaults/system_settings/environments/maya.json
new file mode 100644
index 0000000000..7785b108f7
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/maya.json
@@ -0,0 +1,14 @@
+{
+ "PYTHONPATH": [
+ "{PYPE_SETUP_PATH}/repos/avalon-core/setup/maya",
+ "{PYPE_SETUP_PATH}/repos/maya-look-assigner",
+ "{PYTHON_ENV}/python2/Lib/site-packages",
+ "{PYTHONPATH}"
+ ],
+ "MAYA_DISABLE_CLIC_IPM": "Yes",
+ "MAYA_DISABLE_CIP": "Yes",
+ "MAYA_DISABLE_CER": "Yes",
+ "PYMEL_SKIP_MEL_INIT": "Yes",
+ "LC_ALL": "C",
+ "PYPE_LOG_NO_COLORS": "Yes"
+}
diff --git a/pype/settings/defaults/system_settings/environments/maya_2018.json b/pype/settings/defaults/system_settings/environments/maya_2018.json
new file mode 100644
index 0000000000..72a0c57ce3
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/maya_2018.json
@@ -0,0 +1,11 @@
+{
+ "MAYA_VERSION": "2018",
+ "MAYA_LOCATION": {
+ "darwin": "/Applications/Autodesk/maya{MAYA_VERSION}/Maya.app/Contents",
+ "linux": "/usr/autodesk/maya{MAYA_VERSION}",
+ "windows": "C:/Program Files/Autodesk/Maya{MAYA_VERSION}"
+ },
+ "DYLD_LIBRARY_PATH": {
+ "darwin": "{MAYA_LOCATION}/MacOS"
+ }
+}
diff --git a/pype/settings/defaults/system_settings/environments/maya_2020.json b/pype/settings/defaults/system_settings/environments/maya_2020.json
new file mode 100644
index 0000000000..efd0250bc8
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/maya_2020.json
@@ -0,0 +1,11 @@
+{
+ "MAYA_VERSION": "2020",
+ "MAYA_LOCATION": {
+ "darwin": "/Applications/Autodesk/maya{MAYA_VERSION}/Maya.app/Contents",
+ "linux": "/usr/autodesk/maya{MAYA_VERSION}",
+ "windows": "C:/Program Files/Autodesk/Maya{MAYA_VERSION}"
+ },
+ "DYLD_LIBRARY_PATH": {
+ "darwin": "{MAYA_LOCATION}/MacOS"
+ }
+}
diff --git a/pype/settings/defaults/system_settings/environments/mayabatch.json b/pype/settings/defaults/system_settings/environments/mayabatch.json
new file mode 100644
index 0000000000..7785b108f7
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/mayabatch.json
@@ -0,0 +1,14 @@
+{
+ "PYTHONPATH": [
+ "{PYPE_SETUP_PATH}/repos/avalon-core/setup/maya",
+ "{PYPE_SETUP_PATH}/repos/maya-look-assigner",
+ "{PYTHON_ENV}/python2/Lib/site-packages",
+ "{PYTHONPATH}"
+ ],
+ "MAYA_DISABLE_CLIC_IPM": "Yes",
+ "MAYA_DISABLE_CIP": "Yes",
+ "MAYA_DISABLE_CER": "Yes",
+ "PYMEL_SKIP_MEL_INIT": "Yes",
+ "LC_ALL": "C",
+ "PYPE_LOG_NO_COLORS": "Yes"
+}
diff --git a/pype/settings/defaults/system_settings/environments/mayabatch_2019.json b/pype/settings/defaults/system_settings/environments/mayabatch_2019.json
new file mode 100644
index 0000000000..aa7360a943
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/mayabatch_2019.json
@@ -0,0 +1,11 @@
+{
+ "MAYA_VERSION": "2019",
+ "MAYA_LOCATION": {
+ "darwin": "/Applications/Autodesk/maya{MAYA_VERSION}/Maya.app/Contents",
+ "linux": "/usr/autodesk/maya{MAYA_VERSION}",
+ "windows": "C:/Program Files/Autodesk/Maya{MAYA_VERSION}"
+ },
+ "DYLD_LIBRARY_PATH": {
+ "darwin": "{MAYA_LOCATION}/MacOS"
+ }
+}
diff --git a/pype/settings/defaults/system_settings/environments/mtoa_3.1.1.json b/pype/settings/defaults/system_settings/environments/mtoa_3.1.1.json
new file mode 100644
index 0000000000..f7b9f94d4e
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/mtoa_3.1.1.json
@@ -0,0 +1,23 @@
+{
+ "MTOA": "{PYPE_STUDIO_SOFTWARE}/arnold/mtoa_{MAYA_VERSION}_{MTOA_VERSION}",
+ "MTOA_VERSION": "3.1.1",
+ "MAYA_RENDER_DESC_PATH": "{MTOA}",
+ "MAYA_MODULE_PATH": "{MTOA}",
+ "ARNOLD_PLUGIN_PATH": "{MTOA}/shaders",
+ "MTOA_EXTENSIONS_PATH": {
+ "darwin": "{MTOA}/extensions",
+ "linux": "{MTOA}/extensions",
+ "windows": "{MTOA}/extensions"
+ },
+ "MTOA_EXTENSIONS": {
+ "darwin": "{MTOA}/extensions",
+ "linux": "{MTOA}/extensions",
+ "windows": "{MTOA}/extensions"
+ },
+ "DYLD_LIBRARY_PATH": {
+ "darwin": "{MTOA}/bin"
+ },
+ "PATH": {
+ "windows": "{PATH};{MTOA}/bin"
+ }
+}
diff --git a/pype/settings/defaults/system_settings/environments/muster.json b/pype/settings/defaults/system_settings/environments/muster.json
new file mode 100644
index 0000000000..26f311146a
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/muster.json
@@ -0,0 +1,3 @@
+{
+ "MUSTER_REST_URL": "http://127.0.0.1:9890"
+}
diff --git a/pype/settings/defaults/system_settings/environments/nuke.json b/pype/settings/defaults/system_settings/environments/nuke.json
new file mode 100644
index 0000000000..50dd31ac91
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/nuke.json
@@ -0,0 +1,15 @@
+{
+ "NUKE_PATH": [
+ "{PYPE_SETUP_PATH}/repos/avalon-core/setup/nuke/nuke_path",
+ "{PYPE_MODULE_ROOT}/setup/nuke/nuke_path",
+ "{PYPE_STUDIO_PLUGINS}/nuke"
+ ],
+ "PATH": {
+ "windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}"
+ },
+ "PYPE_LOG_NO_COLORS": "True",
+ "PYTHONPATH": {
+ "windows": "{VIRTUAL_ENV}/Lib/site-packages;{PYTHONPATH}",
+ "linux": "{VIRTUAL_ENV}/lib/python3.6/site-packages:{PYTHONPATH}"
+ }
+}
diff --git a/pype/settings/defaults/system_settings/environments/nukestudio.json b/pype/settings/defaults/system_settings/environments/nukestudio.json
new file mode 100644
index 0000000000..b05e2411f0
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/nukestudio.json
@@ -0,0 +1,11 @@
+{
+ "HIERO_PLUGIN_PATH": [
+ "{PYPE_MODULE_ROOT}/setup/nukestudio/hiero_plugin_path"
+ ],
+ "PATH": {
+ "windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}"
+ },
+ "WORKFILES_STARTUP": "0",
+ "TAG_ASSETBUILD_STARTUP": "0",
+ "PYPE_LOG_NO_COLORS": "True"
+}
diff --git a/pype/settings/defaults/system_settings/environments/nukestudio_10.0.json b/pype/settings/defaults/system_settings/environments/nukestudio_10.0.json
new file mode 100644
index 0000000000..9bdcef53c9
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/nukestudio_10.0.json
@@ -0,0 +1,4 @@
+{
+ "PYPE_LOG_NO_COLORS": "Yes",
+ "QT_PREFERRED_BINDING": "PySide"
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/system_settings/environments/nukex.json b/pype/settings/defaults/system_settings/environments/nukex.json
new file mode 100644
index 0000000000..2b77f44076
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/nukex.json
@@ -0,0 +1,10 @@
+{
+ "NUKE_PATH": [
+ "{PYPE_SETUP_PATH}/repos/avalon-core/setup/nuke/nuke_path",
+ "{PYPE_MODULE_ROOT}/setup/nuke/nuke_path",
+ "{PYPE_STUDIO_PLUGINS}/nuke"
+ ],
+ "PATH": {
+ "windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}"
+ }
+}
diff --git a/pype/settings/defaults/system_settings/environments/nukex_10.0.json b/pype/settings/defaults/system_settings/environments/nukex_10.0.json
new file mode 100644
index 0000000000..9bdcef53c9
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/nukex_10.0.json
@@ -0,0 +1,4 @@
+{
+ "PYPE_LOG_NO_COLORS": "Yes",
+ "QT_PREFERRED_BINDING": "PySide"
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/system_settings/environments/photoshop.json b/pype/settings/defaults/system_settings/environments/photoshop.json
new file mode 100644
index 0000000000..2208a88665
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/photoshop.json
@@ -0,0 +1,4 @@
+{
+ "AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH": "1",
+ "PYTHONPATH": "{PYTHONPATH}"
+}
diff --git a/pype/settings/defaults/system_settings/environments/premiere.json b/pype/settings/defaults/system_settings/environments/premiere.json
new file mode 100644
index 0000000000..27dc5c564b
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/premiere.json
@@ -0,0 +1,11 @@
+{
+ "EXTENSIONS_PATH": {
+ "windows": "{USERPROFILE}/AppData/Roaming/Adobe/CEP/extensions",
+ "darvin": "{USER}/Library/Application Support/Adobe/CEP/extensions"
+ },
+ "EXTENSIONS_CACHE_PATH": {
+ "windows": "{USERPROFILE}/AppData/Local/Temp/cep_cache",
+ "darvin": "{USER}/Library/Application Support/Adobe/CEP/cep_cache"
+ },
+ "installed_zxp": ""
+}
diff --git a/pype/settings/defaults/system_settings/environments/resolve.json b/pype/settings/defaults/system_settings/environments/resolve.json
new file mode 100644
index 0000000000..1ff197dd5a
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/resolve.json
@@ -0,0 +1,40 @@
+{
+ "RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR": [
+ "{STUDIO_SOFT}/davinci_resolve/scripts/python"
+ ],
+ "RESOLVE_SCRIPT_API": {
+ "windows": "{PROGRAMDATA}/Blackmagic Design/DaVinci Resolve/Support/Developer/Scripting",
+ "darvin": "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting",
+ "linux": "/opt/resolve/Developer/Scripting"
+ },
+ "RESOLVE_SCRIPT_LIB": {
+ "windows": "C:/Program Files/Blackmagic Design/DaVinci Resolve/fusionscript.dll",
+ "darvin": "/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so",
+ "linux": "/opt/resolve/libs/Fusion/fusionscript.so"
+ },
+ "RESOLVE_UTILITY_SCRIPTS_DIR": {
+ "windows": "{PROGRAMDATA}/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Comp",
+ "darvin": "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Comp",
+ "linux": "/opt/resolve/Fusion/Scripts/Comp"
+ },
+ "PYTHON36_RESOLVE": {
+ "windows": "{LOCALAPPDATA}/Programs/Python/Python36",
+ "darvin": "~/Library/Python/3.6/bin",
+ "linux": "/opt/Python/3.6/bin"
+ },
+ "PYTHONPATH": [
+ "{PYTHON36_RESOLVE}/Lib/site-packages",
+ "{VIRTUAL_ENV}/Lib/site-packages",
+ "{PYTHONPATH}",
+ "{RESOLVE_SCRIPT_API}/Modules",
+ "{PYTHONPATH}"
+ ],
+ "PATH": [
+ "{PYTHON36_RESOLVE}",
+ "{PYTHON36_RESOLVE}/Scripts",
+ "{PATH}"
+ ],
+ "PRE_PYTHON_SCRIPT": "{PYPE_MODULE_ROOT}/pype/resolve/preload_console.py",
+ "PYPE_LOG_NO_COLORS": "True",
+ "RESOLVE_DEV": "True"
+}
diff --git a/pype/settings/defaults/system_settings/environments/storyboardpro.json b/pype/settings/defaults/system_settings/environments/storyboardpro.json
new file mode 100644
index 0000000000..581ad4db45
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/storyboardpro.json
@@ -0,0 +1,4 @@
+{
+ "AVALON_TOONBOOM_WORKFILES_ON_LAUNCH": "1",
+ "PYBLISH_LITE_ALWAYS_EXEC": "1"
+}
diff --git a/pype/settings/defaults/system_settings/environments/unreal_4.24.json b/pype/settings/defaults/system_settings/environments/unreal_4.24.json
new file mode 100644
index 0000000000..8feeb0230f
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/unreal_4.24.json
@@ -0,0 +1,5 @@
+{
+ "AVALON_UNREAL_PLUGIN": "{PYPE_SETUP_PATH}/repos/avalon-unreal-integration",
+ "PYPE_LOG_NO_COLORS": "True",
+ "QT_PREFERRED_BINDING": "PySide"
+}
diff --git a/pype/settings/defaults/system_settings/environments/vray_4300.json b/pype/settings/defaults/system_settings/environments/vray_4300.json
new file mode 100644
index 0000000000..3212188441
--- /dev/null
+++ b/pype/settings/defaults/system_settings/environments/vray_4300.json
@@ -0,0 +1,15 @@
+{
+ "VRAY_VERSION": "43001",
+ "VRAY_ROOT": "C:/vray/vray_{VRAY_VERSION}",
+ "MAYA_RENDER_DESC_PATH": "{VRAY_ROOT}/maya_root/bin/rendererDesc",
+ "VRAY_FOR_MAYA2019_MAIN": "{VRAY_ROOT}/maya_vray",
+ "VRAY_FOR_MAYA2019_PLUGINS": "{VRAY_ROOT}/maya_vray/vrayplugins",
+ "VRAY_PLUGINS": "{VRAY_ROOT}/maya_vray/vrayplugins",
+ "VRAY_OSL_PATH_MAYA2019": "{VRAY_ROOT}/vray/opensl",
+ "PATH": "{VRAY_ROOT}/maya_root/bin;{PATH}",
+ "MAYA_PLUG_IN_PATH": "{VRAY_ROOT}/maya_vray/plug-ins",
+ "MAYA_SCRIPT_PATH": "{VRAY_ROOT}/maya_vray/scripts",
+ "PYTHONPATH": "{VRAY_ROOT}/maya_vray/scripts;{PYTHONPATH}",
+ "XBMLANGPATH": "{VRAY_ROOT}/maya_vray/icons;{XBMLANGPATH}",
+ "VRAY_AUTH_CLIENT_FILE_PATH": "{VRAY_ROOT}"
+}
diff --git a/pype/settings/defaults/system_settings/global/applications.json b/pype/settings/defaults/system_settings/global/applications.json
new file mode 100644
index 0000000000..e85e5864d9
--- /dev/null
+++ b/pype/settings/defaults/system_settings/global/applications.json
@@ -0,0 +1,34 @@
+{
+ "blender_2.80": false,
+ "blender_2.81": false,
+ "blender_2.82": false,
+ "blender_2.83": true,
+ "celaction_local": true,
+ "celaction_remote": true,
+ "harmony_17": true,
+ "maya_2017": false,
+ "maya_2018": false,
+ "maya_2019": true,
+ "maya_2020": true,
+ "nuke_10.0": false,
+ "nuke_11.2": false,
+ "nuke_11.3": true,
+ "nuke_12.0": true,
+ "nukex_10.0": false,
+ "nukex_11.2": false,
+ "nukex_11.3": true,
+ "nukex_12.0": true,
+ "nukestudio_10.0": false,
+ "nukestudio_11.2": false,
+ "nukestudio_11.3": true,
+ "nukestudio_12.0": true,
+ "houdini_16": false,
+ "houdini_16.5": false,
+ "houdini_17": false,
+ "houdini_18": true,
+ "premiere_2019": false,
+ "premiere_2020": true,
+ "resolve_16": true,
+ "storyboardpro_7": true,
+ "unreal_4.24": true
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/system_settings/global/general.json b/pype/settings/defaults/system_settings/global/general.json
new file mode 100644
index 0000000000..bd501b06eb
--- /dev/null
+++ b/pype/settings/defaults/system_settings/global/general.json
@@ -0,0 +1,4 @@
+{
+ "studio_name": "",
+ "studio_code": ""
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/system_settings/global/intent.json b/pype/settings/defaults/system_settings/global/intent.json
new file mode 100644
index 0000000000..844bd1b518
--- /dev/null
+++ b/pype/settings/defaults/system_settings/global/intent.json
@@ -0,0 +1,8 @@
+{
+ "items": {
+ "wip": "WIP",
+ "test": "TEST",
+ "final": "FINAL"
+ },
+ "default": "wip"
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/system_settings/global/modules.json b/pype/settings/defaults/system_settings/global/modules.json
new file mode 100644
index 0000000000..9bd46602cf
--- /dev/null
+++ b/pype/settings/defaults/system_settings/global/modules.json
@@ -0,0 +1,88 @@
+{
+ "Avalon": {
+ "AVALON_MONGO": "mongodb://localhost:2707",
+ "AVALON_DB_DATA": "{PYPE_SETUP_PATH}/../mongo_db_data",
+ "AVALON_THUMBNAIL_ROOT": "{PYPE_SETUP_PATH}/../avalon_thumails"
+ },
+ "Ftrack": {
+ "enabled": true,
+ "ftrack_server": "https://pype.ftrackapp.com",
+ "ftrack_actions_path": [],
+ "ftrack_events_path": [],
+ "FTRACK_EVENTS_MONGO_DB": "pype",
+ "FTRACK_EVENTS_MONGO_COL": "ftrack_events",
+ "sync_to_avalon": {
+ "statuses_name_change": [
+ "ready",
+ "not ready"
+ ]
+ },
+ "status_version_to_task": {},
+ "status_update": {
+ "Ready": [
+ "Not Ready"
+ ],
+ "In Progress": [
+ "_any_"
+ ]
+ },
+ "intent": {
+ "items": {
+ "-": "-",
+ "wip": "WIP",
+ "final": "Final",
+ "test": "Test"
+ },
+ "default": "-"
+ }
+ },
+ "Rest Api": {
+ "default_port": 8021,
+ "exclude_ports": []
+ },
+ "Timers Manager": {
+ "enabled": true,
+ "full_time": 15.0,
+ "message_time": 0.5
+ },
+ "Clockify": {
+ "enabled": true,
+ "workspace_name": "studio name"
+ },
+ "Deadline": {
+ "enabled": true,
+ "DEADLINE_REST_URL": "http://localhost:8082"
+ },
+ "Muster": {
+ "enabled": false,
+ "MUSTER_REST_URL": "",
+ "templates_mapping": {
+ "file_layers": 7,
+ "mentalray": 2,
+ "mentalray_sf": 6,
+ "redshift": 55,
+ "renderman": 29,
+ "software": 1,
+ "software_sf": 5,
+ "turtle": 10,
+ "vector": 4,
+ "vray": 37,
+ "ffmpeg": 48
+ }
+ },
+ "Logging": {
+ "enabled": true
+ },
+ "Adobe Communicator": {
+ "enabled": true
+ },
+ "User setting": {
+ "enabled": true
+ },
+ "Standalone Publish": {
+ "enabled": true
+ },
+ "Idle Manager": {
+ "enabled": true
+ }
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/system_settings/global/tools.json b/pype/settings/defaults/system_settings/global/tools.json
new file mode 100644
index 0000000000..1f8c2ad1ea
--- /dev/null
+++ b/pype/settings/defaults/system_settings/global/tools.json
@@ -0,0 +1,6 @@
+{
+ "mtoa_3.0.1": false,
+ "mtoa_3.1.1": false,
+ "mtoa_3.2.0": true,
+ "yeti_2.1.2": true
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/system_settings/launchers/blender_2.80.toml b/pype/settings/defaults/system_settings/launchers/blender_2.80.toml
new file mode 100644
index 0000000000..5fea78b7b0
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/blender_2.80.toml
@@ -0,0 +1,7 @@
+application_dir = "blender"
+executable = "blender_2.80"
+schema = "avalon-core:application-1.0"
+label = "Blender 2.80"
+ftrack_label = "Blender"
+icon ="blender"
+ftrack_icon = '{}/app_icons/blender.png'
diff --git a/pype/settings/defaults/system_settings/launchers/blender_2.81.toml b/pype/settings/defaults/system_settings/launchers/blender_2.81.toml
new file mode 100644
index 0000000000..4f85ee5558
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/blender_2.81.toml
@@ -0,0 +1,7 @@
+application_dir = "blender"
+executable = "blender_2.81"
+schema = "avalon-core:application-1.0"
+label = "Blender 2.81"
+ftrack_label = "Blender"
+icon ="blender"
+ftrack_icon = '{}/app_icons/blender.png'
diff --git a/pype/settings/defaults/system_settings/launchers/blender_2.82.toml b/pype/settings/defaults/system_settings/launchers/blender_2.82.toml
new file mode 100644
index 0000000000..840001452e
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/blender_2.82.toml
@@ -0,0 +1,7 @@
+application_dir = "blender"
+executable = "blender_2.82"
+schema = "avalon-core:application-1.0"
+label = "Blender 2.82"
+ftrack_label = "Blender"
+icon ="blender"
+ftrack_icon = '{}/app_icons/blender.png'
diff --git a/pype/settings/defaults/system_settings/launchers/blender_2.83.toml b/pype/settings/defaults/system_settings/launchers/blender_2.83.toml
new file mode 100644
index 0000000000..7fc8bf87b9
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/blender_2.83.toml
@@ -0,0 +1,7 @@
+application_dir = "blender"
+executable = "blender_2.83"
+schema = "avalon-core:application-1.0"
+label = "Blender 2.83"
+ftrack_label = "Blender"
+icon ="blender"
+ftrack_icon = '{}/app_icons/blender.png'
diff --git a/pype/settings/defaults/system_settings/launchers/celaction_local.toml b/pype/settings/defaults/system_settings/launchers/celaction_local.toml
new file mode 100644
index 0000000000..aef3548e08
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/celaction_local.toml
@@ -0,0 +1,8 @@
+executable = "celaction_local"
+schema = "avalon-core:application-1.0"
+application_dir = "celaction"
+label = "CelAction2D"
+ftrack_label = "CelAction2D"
+icon ="celaction_local"
+launch_hook = "pype/hooks/celaction/prelaunch.py/CelactionPrelaunchHook"
+ftrack_icon = '{}/app_icons/celaction_local.png'
diff --git a/pype/settings/defaults/system_settings/launchers/celaction_publish.toml b/pype/settings/defaults/system_settings/launchers/celaction_publish.toml
new file mode 100644
index 0000000000..86f4ae39e7
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/celaction_publish.toml
@@ -0,0 +1,7 @@
+schema = "avalon-core:application-1.0"
+application_dir = "shell"
+executable = "celaction_publish"
+label = "Shell"
+
+[environment]
+CREATE_NEW_CONSOLE = "Yes"
diff --git a/pype/settings/defaults/system_settings/launchers/darwin/blender_2.82 b/pype/settings/defaults/system_settings/launchers/darwin/blender_2.82
new file mode 100644
index 0000000000..8254411ea2
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/darwin/blender_2.82
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+open -a blender $@
diff --git a/pype/settings/defaults/system_settings/launchers/darwin/harmony_17 b/pype/settings/defaults/system_settings/launchers/darwin/harmony_17
new file mode 100644
index 0000000000..b7eba2c2d0
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/darwin/harmony_17_launch b/pype/settings/defaults/system_settings/launchers/darwin/harmony_17_launch
new file mode 100644
index 0000000000..5dcf5db57e
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/darwin/python3 b/pype/settings/defaults/system_settings/launchers/darwin/python3
new file mode 100644
index 0000000000..c2b82c7638
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/darwin/python3
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+open /usr/bin/python3 --args $@
diff --git a/pype/settings/defaults/system_settings/launchers/harmony_17.toml b/pype/settings/defaults/system_settings/launchers/harmony_17.toml
new file mode 100644
index 0000000000..dbb76444a7
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/harmony_17.toml
@@ -0,0 +1,8 @@
+application_dir = "harmony"
+label = "Harmony 17"
+ftrack_label = "Harmony"
+schema = "avalon-core:application-1.0"
+executable = "harmony_17"
+description = ""
+icon ="harmony_icon"
+ftrack_icon = '{}/app_icons/harmony.png'
diff --git a/pype/settings/defaults/system_settings/launchers/houdini_16.toml b/pype/settings/defaults/system_settings/launchers/houdini_16.toml
new file mode 100644
index 0000000000..e29fa74cad
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/houdini_16.toml
@@ -0,0 +1,7 @@
+executable = "houdini_16"
+schema = "avalon-core:application-1.0"
+application_dir = "houdini"
+label = "Houdini 16"
+ftrack_label = "Houdini"
+icon = "houdini_icon"
+ftrack_icon = '{}/app_icons/houdini.png'
diff --git a/pype/settings/defaults/system_settings/launchers/houdini_17.toml b/pype/settings/defaults/system_settings/launchers/houdini_17.toml
new file mode 100644
index 0000000000..5d01364330
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/houdini_17.toml
@@ -0,0 +1,7 @@
+executable = "houdini_17"
+schema = "avalon-core:application-1.0"
+application_dir = "houdini"
+label = "Houdini 17.0"
+ftrack_label = "Houdini"
+icon = "houdini_icon"
+ftrack_icon = '{}/app_icons/houdini.png'
diff --git a/pype/settings/defaults/system_settings/launchers/houdini_18.toml b/pype/settings/defaults/system_settings/launchers/houdini_18.toml
new file mode 100644
index 0000000000..93b9a3334d
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/houdini_18.toml
@@ -0,0 +1,7 @@
+executable = "houdini_18"
+schema = "avalon-core:application-1.0"
+application_dir = "houdini"
+label = "Houdini 18"
+ftrack_label = "Houdini"
+icon = "houdini_icon"
+ftrack_icon = '{}/app_icons/houdini.png'
diff --git a/pype/settings/defaults/system_settings/launchers/linux/maya2016 b/pype/settings/defaults/system_settings/launchers/linux/maya2016
new file mode 100644
index 0000000000..98424304b1
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/linux/maya2017 b/pype/settings/defaults/system_settings/launchers/linux/maya2017
new file mode 100644
index 0000000000..7a2662a55e
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/linux/maya2018 b/pype/settings/defaults/system_settings/launchers/linux/maya2018
new file mode 100644
index 0000000000..db832b3fe7
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/linux/maya2019 b/pype/settings/defaults/system_settings/launchers/linux/maya2019
new file mode 100644
index 0000000000..8398734ab9
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/linux/maya2020 b/pype/settings/defaults/system_settings/launchers/linux/maya2020
new file mode 100644
index 0000000000..18a1edd598
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/linux/nuke11.3 b/pype/settings/defaults/system_settings/launchers/linux/nuke11.3
new file mode 100644
index 0000000000..b1c9a90d74
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/linux/nuke12.0 b/pype/settings/defaults/system_settings/launchers/linux/nuke12.0
new file mode 100644
index 0000000000..99ea1a6b0c
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/linux/nukestudio11.3 b/pype/settings/defaults/system_settings/launchers/linux/nukestudio11.3
new file mode 100644
index 0000000000..750d54a7d5
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/linux/nukestudio12.0 b/pype/settings/defaults/system_settings/launchers/linux/nukestudio12.0
new file mode 100644
index 0000000000..ba5cf654a8
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/linux/nukex11.3 b/pype/settings/defaults/system_settings/launchers/linux/nukex11.3
new file mode 100644
index 0000000000..d913e4b961
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/linux/nukex12.0 b/pype/settings/defaults/system_settings/launchers/linux/nukex12.0
new file mode 100644
index 0000000000..da2721c48b
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/maya_2016.toml b/pype/settings/defaults/system_settings/launchers/maya_2016.toml
new file mode 100644
index 0000000000..d69c4effaf
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/maya_2016.toml
@@ -0,0 +1,26 @@
+application_dir = "maya"
+default_dirs = [
+ "scenes",
+ "data",
+ "renderData/shaders",
+ "images"
+]
+label = "Autodesk Maya 2016x64"
+ftrack_label = "Maya"
+schema = "avalon-core:application-1.0"
+executable = "maya2016"
+description = ""
+icon ="maya_icon"
+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/system_settings/launchers/maya_2017.toml b/pype/settings/defaults/system_settings/launchers/maya_2017.toml
new file mode 100644
index 0000000000..2d1c35b530
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/maya_2017.toml
@@ -0,0 +1,28 @@
+application_dir = "maya"
+default_dirs = [
+ "scenes",
+ "data",
+ "renderData/shaders",
+ "images"
+]
+label = "Autodesk Maya 2017"
+ftrack_label = "Maya"
+schema = "avalon-core:application-1.0"
+executable = "maya2017"
+description = ""
+icon ="maya_icon"
+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/system_settings/launchers/maya_2018.toml b/pype/settings/defaults/system_settings/launchers/maya_2018.toml
new file mode 100644
index 0000000000..f180263fa2
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/maya_2018.toml
@@ -0,0 +1,14 @@
+application_dir = "maya"
+default_dirs = [
+ "renders"
+]
+label = "Autodesk Maya 2018"
+ftrack_label = "Maya"
+schema = "avalon-core:application-1.0"
+executable = "maya2018"
+description = ""
+icon ="maya_icon"
+ftrack_icon = '{}/app_icons/maya.png'
+
+[copy]
+"{PYPE_MODULE_ROOT}/pype/resources/maya/workspace.mel" = "workspace.mel"
diff --git a/pype/settings/defaults/system_settings/launchers/maya_2019.toml b/pype/settings/defaults/system_settings/launchers/maya_2019.toml
new file mode 100644
index 0000000000..7ec2cbcedd
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/maya_2019.toml
@@ -0,0 +1,14 @@
+application_dir = "maya"
+default_dirs = [
+ "renders"
+]
+label = "Autodesk Maya 2019"
+ftrack_label = "Maya"
+schema = "avalon-core:application-1.0"
+executable = "maya2019"
+description = ""
+icon ="maya_icon"
+ftrack_icon = '{}/app_icons/maya.png'
+
+[copy]
+"{PYPE_MODULE_ROOT}/pype/resources/maya/workspace.mel" = "workspace.mel"
diff --git a/pype/settings/defaults/system_settings/launchers/maya_2020.toml b/pype/settings/defaults/system_settings/launchers/maya_2020.toml
new file mode 100644
index 0000000000..49d84ef9a0
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/maya_2020.toml
@@ -0,0 +1,14 @@
+application_dir = "maya"
+default_dirs = [
+ "renders"
+]
+label = "Autodesk Maya 2020"
+ftrack_label = "Maya"
+schema = "avalon-core:application-1.0"
+executable = "maya2020"
+description = ""
+icon ="maya_icon"
+ftrack_icon = '{}/app_icons/maya.png'
+
+[copy]
+"{PYPE_MODULE_ROOT}/pype/resources/maya/workspace.mel" = "workspace.mel"
diff --git a/pype/settings/defaults/system_settings/launchers/mayabatch_2019.toml b/pype/settings/defaults/system_settings/launchers/mayabatch_2019.toml
new file mode 100644
index 0000000000..a928618d2b
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/mayabatch_2020.toml b/pype/settings/defaults/system_settings/launchers/mayabatch_2020.toml
new file mode 100644
index 0000000000..cd1e1e4474
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/mayapy2016.toml b/pype/settings/defaults/system_settings/launchers/mayapy2016.toml
new file mode 100644
index 0000000000..ad1e3dee86
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/mayapy2017.toml b/pype/settings/defaults/system_settings/launchers/mayapy2017.toml
new file mode 100644
index 0000000000..8d2095ff47
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/mayapy2018.toml b/pype/settings/defaults/system_settings/launchers/mayapy2018.toml
new file mode 100644
index 0000000000..597744fd85
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/mayapy2019.toml b/pype/settings/defaults/system_settings/launchers/mayapy2019.toml
new file mode 100644
index 0000000000..3c8a9860f9
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/mayapy2020.toml b/pype/settings/defaults/system_settings/launchers/mayapy2020.toml
new file mode 100644
index 0000000000..8f2d2e4a67
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/myapp.toml b/pype/settings/defaults/system_settings/launchers/myapp.toml
new file mode 100644
index 0000000000..21da0d52b2
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/nuke_10.0.toml b/pype/settings/defaults/system_settings/launchers/nuke_10.0.toml
new file mode 100644
index 0000000000..2195fd3e82
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/nuke_10.0.toml
@@ -0,0 +1,7 @@
+executable = "nuke10.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "Nuke 10.0v4"
+ftrack_label = "Nuke"
+icon ="nuke_icon"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/system_settings/launchers/nuke_11.0.toml b/pype/settings/defaults/system_settings/launchers/nuke_11.0.toml
new file mode 100644
index 0000000000..0c981b479a
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/nuke_11.0.toml
@@ -0,0 +1,7 @@
+executable = "nuke11.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "Nuke 11.0"
+ftrack_label = "Nuke"
+icon ="nuke_icon"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/system_settings/launchers/nuke_11.2.toml b/pype/settings/defaults/system_settings/launchers/nuke_11.2.toml
new file mode 100644
index 0000000000..57c962d126
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/nuke_11.2.toml
@@ -0,0 +1,7 @@
+executable = "nuke11.2"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "Nuke 11.2"
+ftrack_label = "Nuke"
+icon ="nuke_icon"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/system_settings/launchers/nuke_11.3.toml b/pype/settings/defaults/system_settings/launchers/nuke_11.3.toml
new file mode 100644
index 0000000000..87f769c23b
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/nuke_11.3.toml
@@ -0,0 +1,7 @@
+executable = "nuke11.3"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "Nuke 11.3"
+ftrack_label = "Nuke"
+icon ="nuke_icon"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/system_settings/launchers/nuke_12.0.toml b/pype/settings/defaults/system_settings/launchers/nuke_12.0.toml
new file mode 100644
index 0000000000..62936b4cdb
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/nuke_12.0.toml
@@ -0,0 +1,7 @@
+executable = "nuke12.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "Nuke 12.0"
+ftrack_label = "Nuke"
+icon ="nuke_icon"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/system_settings/launchers/nukestudio_10.0.toml b/pype/settings/defaults/system_settings/launchers/nukestudio_10.0.toml
new file mode 100644
index 0000000000..41601e4d40
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/nukestudio_10.0.toml
@@ -0,0 +1,7 @@
+executable = "nukestudio10.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nukestudio"
+label = "NukeStudio 10.0"
+ftrack_label = "NukeStudio"
+icon ="nuke_icon"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/system_settings/launchers/nukestudio_11.0.toml b/pype/settings/defaults/system_settings/launchers/nukestudio_11.0.toml
new file mode 100644
index 0000000000..7a9d84707a
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/nukestudio_11.0.toml
@@ -0,0 +1,7 @@
+executable = "nukestudio11.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nukestudio"
+label = "NukeStudio 11.0"
+ftrack_label = "NukeStudio"
+icon ="nuke_icon"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/system_settings/launchers/nukestudio_11.2.toml b/pype/settings/defaults/system_settings/launchers/nukestudio_11.2.toml
new file mode 100644
index 0000000000..21557033ca
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/nukestudio_11.2.toml
@@ -0,0 +1,7 @@
+executable = "nukestudio11.2"
+schema = "avalon-core:application-1.0"
+application_dir = "nukestudio"
+label = "NukeStudio 11.2"
+ftrack_label = "NukeStudio"
+icon ="nuke_icon"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/system_settings/launchers/nukestudio_11.3.toml b/pype/settings/defaults/system_settings/launchers/nukestudio_11.3.toml
new file mode 100644
index 0000000000..1946ad6c3b
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/nukestudio_11.3.toml
@@ -0,0 +1,7 @@
+executable = "nukestudio11.3"
+schema = "avalon-core:application-1.0"
+application_dir = "nukestudio"
+label = "NukeStudio 11.3"
+ftrack_label = "NukeStudio"
+icon ="nuke_icon"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/system_settings/launchers/nukestudio_12.0.toml b/pype/settings/defaults/system_settings/launchers/nukestudio_12.0.toml
new file mode 100644
index 0000000000..4ce7f9b538
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/nukestudio_12.0.toml
@@ -0,0 +1,7 @@
+executable = "nukestudio12.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nukestudio"
+label = "NukeStudio 12.0"
+ftrack_label = "NukeStudio"
+icon ="nuke_icon"
+ftrack_icon = '{}/app_icons/nuke.png'
diff --git a/pype/settings/defaults/system_settings/launchers/nukex_10.0.toml b/pype/settings/defaults/system_settings/launchers/nukex_10.0.toml
new file mode 100644
index 0000000000..7dee22996d
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/nukex_10.0.toml
@@ -0,0 +1,7 @@
+executable = "nukex10.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "NukeX 10.0"
+ftrack_label = "NukeX"
+icon ="nuke_icon"
+ftrack_icon = '{}/app_icons/nukex.png'
diff --git a/pype/settings/defaults/system_settings/launchers/nukex_11.0.toml b/pype/settings/defaults/system_settings/launchers/nukex_11.0.toml
new file mode 100644
index 0000000000..c2b4970a26
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/nukex_11.0.toml
@@ -0,0 +1,7 @@
+executable = "nukex11.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "NukeX 11.2"
+ftrack_label = "NukeX"
+icon ="nuke_icon"
+ftrack_icon = '{}/app_icons/nukex.png'
diff --git a/pype/settings/defaults/system_settings/launchers/nukex_11.2.toml b/pype/settings/defaults/system_settings/launchers/nukex_11.2.toml
new file mode 100644
index 0000000000..3857b9995c
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/nukex_11.2.toml
@@ -0,0 +1,7 @@
+executable = "nukex11.2"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "NukeX 11.2"
+ftrack_label = "NukeX"
+icon ="nuke_icon"
+ftrack_icon = '{}/app_icons/nukex.png'
diff --git a/pype/settings/defaults/system_settings/launchers/nukex_11.3.toml b/pype/settings/defaults/system_settings/launchers/nukex_11.3.toml
new file mode 100644
index 0000000000..56428470eb
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/nukex_11.3.toml
@@ -0,0 +1,7 @@
+executable = "nukex11.3"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "NukeX 11.3"
+ftrack_label = "NukeX"
+icon ="nuke_icon"
+ftrack_icon = '{}/app_icons/nukex.png'
diff --git a/pype/settings/defaults/system_settings/launchers/nukex_12.0.toml b/pype/settings/defaults/system_settings/launchers/nukex_12.0.toml
new file mode 100644
index 0000000000..33d7fddb88
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/nukex_12.0.toml
@@ -0,0 +1,7 @@
+executable = "nukex12.0"
+schema = "avalon-core:application-1.0"
+application_dir = "nuke"
+label = "NukeX 12.0"
+ftrack_label = "NukeX"
+icon ="nuke_icon"
+ftrack_icon = '{}/app_icons/nukex.png'
diff --git a/pype/settings/defaults/system_settings/launchers/photoshop_2020.toml b/pype/settings/defaults/system_settings/launchers/photoshop_2020.toml
new file mode 100644
index 0000000000..117b668232
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/photoshop_2020.toml
@@ -0,0 +1,8 @@
+executable = "photoshop_2020"
+schema = "avalon-core:application-1.0"
+application_dir = "photoshop"
+label = "Adobe Photoshop 2020"
+icon ="photoshop_icon"
+ftrack_label = "Photoshop"
+ftrack_icon = '{}/app_icons/photoshop.png'
+launch_hook = "pype/hooks/photoshop/prelaunch.py/PhotoshopPrelaunch"
diff --git a/pype/settings/defaults/system_settings/launchers/premiere_2019.toml b/pype/settings/defaults/system_settings/launchers/premiere_2019.toml
new file mode 100644
index 0000000000..f4c19c62cb
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/premiere_2019.toml
@@ -0,0 +1,8 @@
+executable = "premiere_pro_2019"
+schema = "avalon-core:application-1.0"
+application_dir = "premiere"
+label = "Adobe Premiere Pro CC 2019"
+icon ="premiere_icon"
+
+ftrack_label = "Premiere"
+ftrack_icon = '{}/app_icons/premiere.png'
diff --git a/pype/settings/defaults/system_settings/launchers/premiere_2020.toml b/pype/settings/defaults/system_settings/launchers/premiere_2020.toml
new file mode 100644
index 0000000000..4d721c898f
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/premiere_2020.toml
@@ -0,0 +1,9 @@
+executable = "premiere_pro_2020"
+schema = "avalon-core:application-1.0"
+application_dir = "premiere"
+label = "Adobe Premiere Pro CC 2020"
+launch_hook = "pype/hooks/premiere/prelaunch.py/PremierePrelaunch"
+icon ="premiere_icon"
+
+ftrack_label = "Premiere"
+ftrack_icon = '{}/app_icons/premiere.png'
diff --git a/pype/settings/defaults/system_settings/launchers/python_2.toml b/pype/settings/defaults/system_settings/launchers/python_2.toml
new file mode 100644
index 0000000000..e9e8dd7899
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/python_2.toml
@@ -0,0 +1,10 @@
+schema = "avalon-core:application-1.0"
+application_dir = "python"
+executable = "python"
+label = "Python 2"
+ftrack_label = "Python"
+icon ="python_icon"
+ftrack_icon = '{}/app_icons/python.png'
+
+[environment]
+CREATE_NEW_CONSOLE = "Yes"
diff --git a/pype/settings/defaults/system_settings/launchers/python_3.toml b/pype/settings/defaults/system_settings/launchers/python_3.toml
new file mode 100644
index 0000000000..5cbd8b2943
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/python_3.toml
@@ -0,0 +1,10 @@
+schema = "avalon-core:application-1.0"
+application_dir = "python"
+executable = "python3"
+label = "Python 3"
+ftrack_label = "Python"
+icon ="python_icon"
+ftrack_icon = '{}/app_icons/python.png'
+
+[environment]
+CREATE_NEW_CONSOLE = "Yes"
diff --git a/pype/settings/defaults/system_settings/launchers/resolve_16.toml b/pype/settings/defaults/system_settings/launchers/resolve_16.toml
new file mode 100644
index 0000000000..430fd1a638
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/resolve_16.toml
@@ -0,0 +1,9 @@
+executable = "resolve_16"
+schema = "avalon-core:application-1.0"
+application_dir = "resolve"
+label = "BM DaVinci Resolve 16"
+launch_hook = "pype/hooks/resolve/prelaunch.py/ResolvePrelaunch"
+icon ="resolve"
+
+ftrack_label = "BM DaVinci Resolve"
+ftrack_icon = '{}/app_icons/resolve.png'
diff --git a/pype/settings/defaults/system_settings/launchers/shell.toml b/pype/settings/defaults/system_settings/launchers/shell.toml
new file mode 100644
index 0000000000..959ad392ea
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/storyboardpro_7.toml b/pype/settings/defaults/system_settings/launchers/storyboardpro_7.toml
new file mode 100644
index 0000000000..ce8e96a49d
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/storyboardpro_7.toml
@@ -0,0 +1,8 @@
+application_dir = "storyboardpro"
+label = "Storyboard Pro 7"
+ftrack_label = "Storyboard Pro"
+schema = "avalon-core:application-1.0"
+executable = "storyboardpro_7"
+description = ""
+icon ="storyboardpro_icon"
+ftrack_icon = '{}/app_icons/storyboardpro.png'
diff --git a/pype/settings/defaults/system_settings/launchers/unreal_4.24.toml b/pype/settings/defaults/system_settings/launchers/unreal_4.24.toml
new file mode 100644
index 0000000000..0a799e5dcb
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/unreal_4.24.toml
@@ -0,0 +1,8 @@
+executable = "unreal"
+schema = "avalon-core:application-1.0"
+application_dir = "unreal"
+label = "Unreal Editor 4.24"
+ftrack_label = "UnrealEditor"
+icon ="ue4_icon"
+launch_hook = "pype/hooks/unreal/unreal_prelaunch.py/UnrealPrelaunch"
+ftrack_icon = '{}/app_icons/ue4.png'
diff --git a/pype/settings/defaults/system_settings/launchers/windows/blender_2.80.bat b/pype/settings/defaults/system_settings/launchers/windows/blender_2.80.bat
new file mode 100644
index 0000000000..5b8a37356b
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/blender_2.81.bat b/pype/settings/defaults/system_settings/launchers/windows/blender_2.81.bat
new file mode 100644
index 0000000000..a900b18eda
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/blender_2.82.bat b/pype/settings/defaults/system_settings/launchers/windows/blender_2.82.bat
new file mode 100644
index 0000000000..7105c1efe1
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/blender_2.83.bat b/pype/settings/defaults/system_settings/launchers/windows/blender_2.83.bat
new file mode 100644
index 0000000000..671952f0d7
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/celaction_local.bat b/pype/settings/defaults/system_settings/launchers/windows/celaction_local.bat
new file mode 100644
index 0000000000..8f2171617e
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/celaction_publish.bat b/pype/settings/defaults/system_settings/launchers/windows/celaction_publish.bat
new file mode 100644
index 0000000000..77ec2ac24e
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/harmony_17.bat b/pype/settings/defaults/system_settings/launchers/windows/harmony_17.bat
new file mode 100644
index 0000000000..0822650875
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/houdini_16.bat b/pype/settings/defaults/system_settings/launchers/windows/houdini_16.bat
new file mode 100644
index 0000000000..018ba08b4c
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/houdini_17.bat b/pype/settings/defaults/system_settings/launchers/windows/houdini_17.bat
new file mode 100644
index 0000000000..950a599623
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/houdini_18.bat b/pype/settings/defaults/system_settings/launchers/windows/houdini_18.bat
new file mode 100644
index 0000000000..3d6b1ae258
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/maya2016.bat b/pype/settings/defaults/system_settings/launchers/windows/maya2016.bat
new file mode 100644
index 0000000000..54f15cf269
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/maya2017.bat b/pype/settings/defaults/system_settings/launchers/windows/maya2017.bat
new file mode 100644
index 0000000000..5c2aeb495c
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/maya2018.bat b/pype/settings/defaults/system_settings/launchers/windows/maya2018.bat
new file mode 100644
index 0000000000..28cf776c77
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/maya2019.bat b/pype/settings/defaults/system_settings/launchers/windows/maya2019.bat
new file mode 100644
index 0000000000..7e80dd2557
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/maya2020.bat b/pype/settings/defaults/system_settings/launchers/windows/maya2020.bat
new file mode 100644
index 0000000000..b2acb5df5a
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/mayabatch2019.bat b/pype/settings/defaults/system_settings/launchers/windows/mayabatch2019.bat
new file mode 100644
index 0000000000..ddd9b9b956
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/mayabatch2020.bat b/pype/settings/defaults/system_settings/launchers/windows/mayabatch2020.bat
new file mode 100644
index 0000000000..b1cbc6dbb6
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/mayapy2016.bat b/pype/settings/defaults/system_settings/launchers/windows/mayapy2016.bat
new file mode 100644
index 0000000000..205991fd3d
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/mayapy2017.bat b/pype/settings/defaults/system_settings/launchers/windows/mayapy2017.bat
new file mode 100644
index 0000000000..14aacc5a7f
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/mayapy2018.bat b/pype/settings/defaults/system_settings/launchers/windows/mayapy2018.bat
new file mode 100644
index 0000000000..c47c472f46
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/mayapy2019.bat b/pype/settings/defaults/system_settings/launchers/windows/mayapy2019.bat
new file mode 100644
index 0000000000..73ca5b2d40
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/mayapy2020.bat b/pype/settings/defaults/system_settings/launchers/windows/mayapy2020.bat
new file mode 100644
index 0000000000..770a03dcf5
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/nuke10.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nuke10.0.bat
new file mode 100644
index 0000000000..a47cbdfb20
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/nuke11.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nuke11.0.bat
new file mode 100644
index 0000000000..a374c5cf5b
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/nuke11.2.bat b/pype/settings/defaults/system_settings/launchers/windows/nuke11.2.bat
new file mode 100644
index 0000000000..4c777ac28c
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/nuke11.3.bat b/pype/settings/defaults/system_settings/launchers/windows/nuke11.3.bat
new file mode 100644
index 0000000000..a023f5f46f
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/nuke12.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nuke12.0.bat
new file mode 100644
index 0000000000..d8fb5772bb
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/nukestudio10.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nukestudio10.0.bat
new file mode 100644
index 0000000000..82f833667c
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/nukestudio11.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nukestudio11.0.bat
new file mode 100644
index 0000000000..b66797727e
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/nukestudio11.2.bat b/pype/settings/defaults/system_settings/launchers/windows/nukestudio11.2.bat
new file mode 100644
index 0000000000..a653d816b4
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/nukestudio11.3.bat b/pype/settings/defaults/system_settings/launchers/windows/nukestudio11.3.bat
new file mode 100644
index 0000000000..62c8718873
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/nukestudio12.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nukestudio12.0.bat
new file mode 100644
index 0000000000..488232bcbf
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/nukex10.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nukex10.0.bat
new file mode 100644
index 0000000000..1759706a7b
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/nukex11.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nukex11.0.bat
new file mode 100644
index 0000000000..b554a7b6fa
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/nukex11.2.bat b/pype/settings/defaults/system_settings/launchers/windows/nukex11.2.bat
new file mode 100644
index 0000000000..a4cb5dec5c
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/nukex11.3.bat b/pype/settings/defaults/system_settings/launchers/windows/nukex11.3.bat
new file mode 100644
index 0000000000..490b55cf4c
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/nukex12.0.bat b/pype/settings/defaults/system_settings/launchers/windows/nukex12.0.bat
new file mode 100644
index 0000000000..26adf0d3f1
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/photoshop_2020.bat b/pype/settings/defaults/system_settings/launchers/windows/photoshop_2020.bat
new file mode 100644
index 0000000000..6b90922ef6
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/premiere_pro_2019.bat b/pype/settings/defaults/system_settings/launchers/windows/premiere_pro_2019.bat
new file mode 100644
index 0000000000..4886737d2f
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/premiere_pro_2020.bat b/pype/settings/defaults/system_settings/launchers/windows/premiere_pro_2020.bat
new file mode 100644
index 0000000000..14662d3be3
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/python3.bat b/pype/settings/defaults/system_settings/launchers/windows/python3.bat
new file mode 100644
index 0000000000..c7c116fe72
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/resolve_16.bat b/pype/settings/defaults/system_settings/launchers/windows/resolve_16.bat
new file mode 100644
index 0000000000..1a5d964e6b
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/shell.bat b/pype/settings/defaults/system_settings/launchers/windows/shell.bat
new file mode 100644
index 0000000000..eb0895364f
--- /dev/null
+++ b/pype/settings/defaults/system_settings/launchers/windows/shell.bat
@@ -0,0 +1,2 @@
+@echo off
+start cmd
diff --git a/pype/settings/defaults/system_settings/launchers/windows/storyboardpro_7.bat b/pype/settings/defaults/system_settings/launchers/windows/storyboardpro_7.bat
new file mode 100644
index 0000000000..122edac572
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/system_settings/launchers/windows/unreal.bat b/pype/settings/defaults/system_settings/launchers/windows/unreal.bat
new file mode 100644
index 0000000000..7771aaa5a5
--- /dev/null
+++ b/pype/settings/defaults/system_settings/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/muster/templates_mapping.json b/pype/settings/defaults/system_settings/muster/templates_mapping.json
new file mode 100644
index 0000000000..0c09113515
--- /dev/null
+++ b/pype/settings/defaults/system_settings/muster/templates_mapping.json
@@ -0,0 +1,19 @@
+{
+ "3delight": 41,
+ "arnold": 46,
+ "arnold_sf": 57,
+ "gelato": 30,
+ "harware": 3,
+ "krakatoa": 51,
+ "file_layers": 7,
+ "mentalray": 2,
+ "mentalray_sf": 6,
+ "redshift": 55,
+ "renderman": 29,
+ "software": 1,
+ "software_sf": 5,
+ "turtle": 10,
+ "vector": 4,
+ "vray": 37,
+ "ffmpeg": 48
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/system_settings/standalone_publish/families.json b/pype/settings/defaults/system_settings/standalone_publish/families.json
new file mode 100644
index 0000000000..d05941cc26
--- /dev/null
+++ b/pype/settings/defaults/system_settings/standalone_publish/families.json
@@ -0,0 +1,90 @@
+{
+ "create_look": {
+ "name": "look",
+ "label": "Look",
+ "family": "look",
+ "icon": "paint-brush",
+ "defaults": ["Main"],
+ "help": "Shader connections defining shape look"
+ },
+ "create_model": {
+ "name": "model",
+ "label": "Model",
+ "family": "model",
+ "icon": "cube",
+ "defaults": ["Main", "Proxy", "Sculpt"],
+ "help": "Polygonal static geometry"
+ },
+ "create_workfile": {
+ "name": "workfile",
+ "label": "Workfile",
+ "family": "workfile",
+ "icon": "cube",
+ "defaults": ["Main"],
+ "help": "Working scene backup"
+ },
+ "create_camera": {
+ "name": "camera",
+ "label": "Camera",
+ "family": "camera",
+ "icon": "video-camera",
+ "defaults": ["Main"],
+ "help": "Single baked camera"
+ },
+ "create_pointcache": {
+ "name": "pointcache",
+ "label": "Pointcache",
+ "family": "pointcache",
+ "icon": "gears",
+ "defaults": ["Main"],
+ "help": "Alembic pointcache for animated data"
+ },
+ "create_rig": {
+ "name": "rig",
+ "label": "Rig",
+ "family": "rig",
+ "icon": "wheelchair",
+ "defaults": ["Main"],
+ "help": "Artist-friendly rig with controls"
+ },
+ "create_layout": {
+ "name": "layout",
+ "label": "Layout",
+ "family": "layout",
+ "icon": "cubes",
+ "defaults": ["Main"],
+ "help": "Simple scene for animators with camera"
+ },
+ "create_plate": {
+ "name": "plate",
+ "label": "Plate",
+ "family": "plate",
+ "icon": "camera",
+ "defaults": ["Main", "BG", "Reference"],
+ "help": "Plates for compositors"
+ },
+ "create_matchmove": {
+ "name": "matchmove",
+ "label": "Matchmove script",
+ "family": "matchmove",
+ "icon": "empire",
+ "defaults": ["Camera", "Object", "Mocap"],
+ "help": "Script exported from matchmoving application"
+ },
+ "create_images": {
+ "name": "image",
+ "label": "Image file",
+ "family": "image",
+ "icon": "image",
+ "defaults": ["ConceptArt", "Reference", "Texture", "MattePaint"],
+ "help": "Holder for all kinds of image data"
+ },
+ "create_editorial": {
+ "name": "editorial",
+ "label": "Editorial",
+ "family": "editorial",
+ "icon": "image",
+ "defaults": ["Main"],
+ "help": "Editorial files to generate shots."
+ }
+}
diff --git a/pype/settings/lib.py b/pype/settings/lib.py
new file mode 100644
index 0000000000..388557ca9b
--- /dev/null
+++ b/pype/settings/lib.py
@@ -0,0 +1,258 @@
+import os
+import json
+import logging
+import copy
+
+log = logging.getLogger(__name__)
+
+# Metadata keys for work with studio and project overrides
+OVERRIDEN_KEY = "__overriden_keys__"
+# NOTE key popping not implemented yet
+POP_KEY = "__pop_key__"
+
+# Folder where studio overrides are stored
+STUDIO_OVERRIDES_PATH = os.environ["PYPE_PROJECT_CONFIGS"]
+
+# File where studio's system overrides are stored
+SYSTEM_SETTINGS_KEY = "system_settings"
+SYSTEM_SETTINGS_PATH = os.path.join(
+ STUDIO_OVERRIDES_PATH, SYSTEM_SETTINGS_KEY + ".json"
+)
+
+# File where studio's default project overrides are stored
+PROJECT_SETTINGS_KEY = "project_settings"
+PROJECT_SETTINGS_FILENAME = PROJECT_SETTINGS_KEY + ".json"
+PROJECT_SETTINGS_PATH = os.path.join(
+ STUDIO_OVERRIDES_PATH, PROJECT_SETTINGS_FILENAME
+)
+
+PROJECT_ANATOMY_KEY = "project_anatomy"
+PROJECT_ANATOMY_FILENAME = PROJECT_ANATOMY_KEY + ".json"
+PROJECT_ANATOMY_PATH = os.path.join(
+ STUDIO_OVERRIDES_PATH, PROJECT_ANATOMY_FILENAME
+)
+
+# Path to default settings
+DEFAULTS_DIR = os.path.join(os.path.dirname(__file__), "defaults")
+
+# Variable where cache of default settings are stored
+_DEFAULT_SETTINGS = None
+
+
+def reset_default_settings():
+ global _DEFAULT_SETTINGS
+ _DEFAULT_SETTINGS = None
+
+
+def default_settings():
+ global _DEFAULT_SETTINGS
+ if _DEFAULT_SETTINGS is None:
+ _DEFAULT_SETTINGS = load_jsons_from_dir(DEFAULTS_DIR)
+ return _DEFAULT_SETTINGS
+
+
+def load_json(fpath):
+ # Load json data
+ with open(fpath, "r") as opened_file:
+ lines = opened_file.read().splitlines()
+
+ # prepare json string
+ standard_json = ""
+ for line in lines:
+ # Remove all whitespace on both sides
+ line = line.strip()
+
+ # Skip blank lines
+ if len(line) == 0:
+ continue
+
+ standard_json += line
+
+ # Check if has extra commas
+ extra_comma = False
+ if ",]" in standard_json or ",}" in standard_json:
+ extra_comma = True
+ standard_json = standard_json.replace(",]", "]")
+ standard_json = standard_json.replace(",}", "}")
+
+ if extra_comma:
+ log.error("Extra comma in json file: \"{}\"".format(fpath))
+
+ # return empty dict if file is empty
+ if standard_json == "":
+ return {}
+
+ # Try to parse string
+ try:
+ return json.loads(standard_json)
+
+ except json.decoder.JSONDecodeError:
+ # Return empty dict if it is first time that decode error happened
+ return {}
+
+ # Repreduce the exact same exception but traceback contains better
+ # information about position of error in the loaded json
+ try:
+ with open(fpath, "r") as opened_file:
+ json.load(opened_file)
+
+ except json.decoder.JSONDecodeError:
+ log.warning(
+ "File has invalid json format \"{}\"".format(fpath),
+ exc_info=True
+ )
+
+ return {}
+
+
+def subkey_merge(_dict, value, keys):
+ key = keys.pop(0)
+ if not keys:
+ _dict[key] = value
+ return _dict
+
+ if key not in _dict:
+ _dict[key] = {}
+ _dict[key] = subkey_merge(_dict[key], value, keys)
+
+ return _dict
+
+
+def load_jsons_from_dir(path, *args, **kwargs):
+ output = {}
+
+ path = os.path.normpath(path)
+ if not os.path.exists(path):
+ # TODO warning
+ return output
+
+ sub_keys = list(kwargs.pop("subkeys", args))
+ for sub_key in tuple(sub_keys):
+ _path = os.path.join(path, sub_key)
+ if not os.path.exists(_path):
+ break
+
+ path = _path
+ sub_keys.pop(0)
+
+ base_len = len(path) + 1
+ for base, _directories, filenames in os.walk(path):
+ base_items_str = base[base_len:]
+ if not base_items_str:
+ base_items = []
+ else:
+ base_items = base_items_str.split(os.path.sep)
+
+ for filename in filenames:
+ basename, ext = os.path.splitext(filename)
+ if ext == ".json":
+ full_path = os.path.join(base, filename)
+ value = load_json(full_path)
+ dict_keys = base_items + [basename]
+ output = subkey_merge(output, value, dict_keys)
+
+ for sub_key in sub_keys:
+ output = output[sub_key]
+ return output
+
+
+def studio_system_settings():
+ if os.path.exists(SYSTEM_SETTINGS_PATH):
+ return load_json(SYSTEM_SETTINGS_PATH)
+ return {}
+
+
+def studio_project_settings():
+ if os.path.exists(PROJECT_SETTINGS_PATH):
+ return load_json(PROJECT_SETTINGS_PATH)
+ return {}
+
+
+def studio_project_anatomy():
+ if os.path.exists(PROJECT_ANATOMY_PATH):
+ return load_json(PROJECT_ANATOMY_PATH)
+ return {}
+
+
+def path_to_project_overrides(project_name):
+ return os.path.join(
+ STUDIO_OVERRIDES_PATH,
+ project_name,
+ PROJECT_SETTINGS_FILENAME
+ )
+
+
+def path_to_project_anatomy(project_name):
+ return os.path.join(
+ STUDIO_OVERRIDES_PATH,
+ project_name,
+ PROJECT_ANATOMY_FILENAME
+ )
+
+
+def project_settings_overrides(project_name):
+ if not project_name:
+ return {}
+
+ path_to_json = path_to_project_overrides(project_name)
+ if not os.path.exists(path_to_json):
+ return {}
+ return load_json(path_to_json)
+
+
+def project_anatomy_overrides(project_name):
+ if not project_name:
+ return {}
+
+ path_to_json = path_to_project_anatomy(project_name)
+ if not os.path.exists(path_to_json):
+ return {}
+ return load_json(path_to_json)
+
+
+def merge_overrides(global_dict, override_dict):
+ if OVERRIDEN_KEY in override_dict:
+ overriden_keys = set(override_dict.pop(OVERRIDEN_KEY))
+ else:
+ overriden_keys = set()
+
+ for key, value in override_dict.items():
+ if value == POP_KEY:
+ global_dict.pop(key)
+
+ elif (
+ key in overriden_keys
+ or key not in global_dict
+ ):
+ global_dict[key] = value
+
+ elif isinstance(value, dict) and isinstance(global_dict[key], dict):
+ global_dict[key] = merge_overrides(global_dict[key], value)
+
+ else:
+ global_dict[key] = value
+ return global_dict
+
+
+def apply_overrides(source_data, override_data):
+ if not override_data:
+ return source_data
+ _source_data = copy.deepcopy(source_data)
+ return merge_overrides(_source_data, override_data)
+
+
+def system_settings():
+ default_values = default_settings()[SYSTEM_SETTINGS_KEY]
+ studio_values = studio_system_settings()
+ return apply_overrides(default_values, studio_values)
+
+
+def project_settings(project_name):
+ default_values = default_settings()[PROJECT_SETTINGS_KEY]
+ studio_values = studio_project_settings()
+
+ studio_overrides = apply_overrides(default_values, studio_values)
+
+ project_overrides = project_settings_overrides(project_name)
+
+ return apply_overrides(studio_overrides, project_overrides)
diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py
index 3c9d4806ac..1482ff85b0 100644
--- a/pype/tools/pyblish_pype/model.py
+++ b/pype/tools/pyblish_pype/model.py
@@ -1147,48 +1147,52 @@ class TerminalModel(QtGui.QStandardItemModel):
return prepared_records
- def append(self, record_item):
- record_type = record_item["type"]
+ def append(self, record_items):
+ all_record_items = []
+ for record_item in record_items:
+ record_type = record_item["type"]
- terminal_item_type = None
- if record_type == "record":
- for level, _type in self.level_to_record:
- if level > record_item["levelno"]:
- break
- terminal_item_type = _type
+ terminal_item_type = None
+ if record_type == "record":
+ for level, _type in self.level_to_record:
+ if level > record_item["levelno"]:
+ break
+ terminal_item_type = _type
- else:
- terminal_item_type = record_type
+ else:
+ terminal_item_type = record_type
- icon_color = self.item_icon_colors.get(terminal_item_type)
- icon_name = self.item_icon_name.get(record_type)
+ icon_color = self.item_icon_colors.get(terminal_item_type)
+ icon_name = self.item_icon_name.get(record_type)
- top_item_icon = None
- if icon_color and icon_name:
- top_item_icon = QAwesomeIconFactory.icon(icon_name, icon_color)
+ top_item_icon = None
+ if icon_color and icon_name:
+ top_item_icon = QAwesomeIconFactory.icon(icon_name, icon_color)
- label = record_item["label"].split("\n")[0]
+ label = record_item["label"].split("\n")[0]
- top_item = QtGui.QStandardItem()
- top_item.setData(TerminalLabelType, Roles.TypeRole)
- top_item.setData(terminal_item_type, Roles.TerminalItemTypeRole)
- top_item.setData(label, QtCore.Qt.DisplayRole)
- top_item.setFlags(
- QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
- )
+ top_item = QtGui.QStandardItem()
+ all_record_items.append(top_item)
- if top_item_icon:
- top_item.setData(top_item_icon, QtCore.Qt.DecorationRole)
+ detail_item = TerminalDetailItem(record_item)
+ top_item.appendRow(detail_item)
- self.appendRow(top_item)
+ top_item.setData(TerminalLabelType, Roles.TypeRole)
+ top_item.setData(terminal_item_type, Roles.TerminalItemTypeRole)
+ top_item.setData(label, QtCore.Qt.DisplayRole)
+ top_item.setFlags(
+ QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
+ )
- detail_item = TerminalDetailItem(record_item)
- detail_item.setData(TerminalDetailType, Roles.TypeRole)
- top_item.appendRow(detail_item)
+ if top_item_icon:
+ top_item.setData(top_item_icon, QtCore.Qt.DecorationRole)
+
+ detail_item.setData(TerminalDetailType, Roles.TypeRole)
+
+ self.invisibleRootItem().appendRows(all_record_items)
def update_with_result(self, result):
- for record in result["records"]:
- self.append(record)
+ self.append(result["records"])
class TerminalProxy(QtCore.QSortFilterProxyModel):
diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py
index 76f31e2442..2a037ba4bc 100644
--- a/pype/tools/pyblish_pype/window.py
+++ b/pype/tools/pyblish_pype/window.py
@@ -1087,10 +1087,10 @@ class Window(QtWidgets.QDialog):
info.setText(message)
# Include message in terminal
- self.terminal_model.append({
+ self.terminal_model.append([{
"label": message,
"type": "info"
- })
+ }])
self.animation_info_msg.stop()
self.animation_info_msg.start()
diff --git a/pype/tools/settings/__init__.py b/pype/tools/settings/__init__.py
new file mode 100644
index 0000000000..7df121f06e
--- /dev/null
+++ b/pype/tools/settings/__init__.py
@@ -0,0 +1,7 @@
+from settings import style, MainWidget
+
+
+__all__ = (
+ "style",
+ "MainWidget"
+)
diff --git a/pype/tools/settings/__main__.py b/pype/tools/settings/__main__.py
new file mode 100644
index 0000000000..55a38b3604
--- /dev/null
+++ b/pype/tools/settings/__main__.py
@@ -0,0 +1,18 @@
+import sys
+
+import settings
+from Qt import QtWidgets, QtGui
+
+
+if __name__ == "__main__":
+ app = QtWidgets.QApplication(sys.argv)
+
+ stylesheet = settings.style.load_stylesheet()
+ app.setStyleSheet(stylesheet)
+ app.setWindowIcon(QtGui.QIcon(settings.style.app_icon_path()))
+
+ develop = "-d" in sys.argv or "--develop" in sys.argv
+ widget = settings.MainWidget(develop)
+ widget.show()
+
+ sys.exit(app.exec_())
diff --git a/pype/tools/settings/settings/README.md b/pype/tools/settings/settings/README.md
new file mode 100644
index 0000000000..47bbf28ba5
--- /dev/null
+++ b/pype/tools/settings/settings/README.md
@@ -0,0 +1,336 @@
+# Creating GUI schemas
+
+## Basic rules
+- configurations does not define GUI, but GUI defines configurations!
+- output is always json (yaml is not needed for anatomy templates anymore)
+- GUI schema has multiple input types, all inputs are represented by a dictionary
+- each input may have "input modifiers" (keys in dictionary) that are required or optional
+ - only required modifier for all input items is key `"type"` which says what type of item it is
+- there are special keys across all inputs
+ - `"is_file"` - this key is for storing pype defaults in `pype` repo
+ - reasons of existence: developing new schemas does not require to create defaults manually
+ - key is validated, must be once in hierarchy else it won't be possible to store pype defaults
+ - `"is_group"` - define that all values under key in hierarchy will be overriden if any value is modified, this information is also stored to overrides
+ - this keys is not allowed for all inputs as they may have not reason for that
+ - key is validated, can be only once in hierarchy but is not required
+- currently there are `system configurations` and `project configurations`
+
+## Inner schema
+- 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")
+
+### 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
+- will just paste schemas from other schema file in order of "children" list
+
+```
+{
+ "type": "schema",
+ "name": "my_schema_name"
+}
+```
+
+## Basic Dictionary inputs
+- these inputs wraps another inputs into {key: value} relation
+
+### dict-invisible
+- this input gives ability to wrap another inputs but keep them in same widget without visible divider
+ - this is for example used as first input widget
+- has required keys `"key"` and `"children"`
+ - "children" says what children inputs are underneath
+ - "key" is key under which will be stored value from it's children
+- output is dictionary `{the "key": children values}`
+- can't have `"is_group"` key set to True as it breaks visual override showing
+```
+{
+ "type": "dict-invisible",
+ "key": "global",
+ "children": [
+ ...ITEMS...
+ ]
+}
+```
+
+## dict
+- this is another dictionary input wrapping more inputs but visually makes them different
+- item may be used as widget (in `list` or `dict-modifiable`)
+ - in that case the only key modifier is `children` which is list of it's keys
+ - USAGE: e.g. List of dictionaries where each dictionary have same structure.
+- item options if is not used as widget
+ - required keys are `"key"` under which will be stored and `"label"` which will be shown in GUI
+ - this input can be expandable
+ - that can be set with key `"expandable"` as `True`/`False` (Default: `True`)
+ - with key `"expanded"` as `True`/`False` can be set that is expanded when GUI is opened (Default: `False`)
+ - it is possible to add darker background with `"highlight_content"` (Default: `False`)
+ - darker background has limits of maximum applies after 3-4 nested highlighted items there is not difference in the color
+```
+# Example
+{
+ "key": "applications",
+ "type": "dict",
+ "label": "Applications",
+ "expandable": true,
+ "highlight_content": true,
+ "is_group": true,
+ "is_file": true,
+ "children": [
+ ...ITEMS...
+ ]
+}
+
+# When used as widget
+{
+ "type": "list",
+ "key": "profiles",
+ "label": "Profiles",
+ "object_type": "dict-item",
+ "input_modifiers": {
+ "children": [
+ {
+ "key": "families",
+ "label": "Families",
+ "type": "list",
+ "object_type": "text"
+ }, {
+ "key": "hosts",
+ "label": "Hosts",
+ "type": "list",
+ "object_type": "text"
+ }
+ ...
+ ]
+ }
+}
+```
+
+## Inputs for setting any kind of value (`Pure` inputs)
+- all these input must have defined `"key"` under which will be stored and `"label"` which will be shown next to input
+ - unless they are used in different types of inputs (later) "as widgets" in that case `"key"` and `"label"` are not required as there is not place where to set them
+
+### boolean
+- simple checkbox, nothing more to set
+```
+{
+ "type": "boolean",
+ "key": "my_boolean_key",
+ "label": "Do you want to use Pype?"
+}
+```
+
+### number
+- number input, can be used for both integer and float
+ - key `"decimal"` defines how many decimal places will be used, 0 is for integer input (Default: `0`)
+ - key `"minimum"` as minimum allowed number to enter (Default: `-99999`)
+ - key `"maxium"` as maximum allowed number to enter (Default: `99999`)
+```
+{
+ "type": "number",
+ "key": "fps",
+ "label": "Frame rate (FPS)"
+ "decimal": 2,
+ "minimum": 1,
+ "maximum": 300000
+}
+```
+
+### text
+- simple text input
+ - key `"multiline"` allows to enter multiple lines of text (Default: `False`)
+ - key `"placeholder"` allows to show text inside input when is empty (Default: `None`)
+
+```
+{
+ "type": "text",
+ "key": "deadline_pool",
+ "label": "Deadline pool"
+}
+```
+
+### path-input
+- enhanced text input
+ - does not allow to enter backslash, is auto-converted to forward slash
+ - may be added another validations, like do not allow end path with slash
+- this input is implemented to add additional features to text input
+- this is meant to be used in proxy input `path-widget`
+ - DO NOT USE this input in schema please
+
+### raw-json
+- a little bit enhanced text input for raw json
+- has validations of json format
+ - empty value is invalid value, always must be at least `{}` of `[]`
+
+```
+{
+ "type": "raw-json",
+ "key": "profiles",
+ "label": "Extract Review profiles"
+}
+```
+
+## Inputs for setting value using Pure inputs
+- these inputs also have required `"key"` and `"label"`
+- they use Pure inputs "as widgets"
+
+### list
+- output is list
+- items can be added and removed
+- items in list must be the same type
+ - type of items is defined with key `"object_type"` where Pure input name is entered (e.g. `number`)
+ - because Pure inputs may have modifiers (`number` input has `minimum`, `maximum` and `decimals`) you can set these in key `"input_modifiers"`
+
+```
+{
+ "type": "list",
+ "object_type": "number",
+ "key": "exclude_ports",
+ "label": "Exclude ports",
+ "input_modifiers": {
+ "minimum": 1,
+ "maximum": 65535
+ }
+}
+```
+
+### dict-modifiable
+- one of dictionary inputs, this is only used as value input
+- items in this input can be removed and added same way as in `list` input
+- value items in dictionary must be the same type
+ - type of items is defined with key `"object_type"` where Pure input name is entered (e.g. `number`)
+ - because Pure inputs may have modifiers (`number` input has `minimum`, `maximum` and `decimals`) you can set these in key `"input_modifiers"`
+- this input can be expandable
+ - that can be set with key `"expandable"` as `True`/`False` (Default: `True`)
+ - with key `"expanded"` as `True`/`False` can be set that is expanded when GUI is opened (Default: `False`)
+
+```
+{
+ "type": "dict-modifiable",
+ "object_type": "number",
+ "input_modifiers": {
+ "minimum": 0,
+ "maximum": 300
+ },
+ "is_group": true,
+ "key": "templates_mapping",
+ "label": "Muster - Templates mapping",
+ "is_file": true
+}
+```
+
+### path-widget
+- input for paths, use `path-input` internally
+- has 2 input modifiers `"multiplatform"` and `"multipath"`
+ - `"multiplatform"` - adds `"windows"`, `"linux"` and `"darwin"` path inputs result is dictionary
+ - `"multipath"` - it is possible to enter multiple paths
+ - if both are enabled result is dictionary with lists
+
+```
+{
+ "type": "path-widget",
+ "key": "ffmpeg_path",
+ "label": "FFmpeg path",
+ "multiplatform": true,
+ "multipath": true
+}
+```
+
+### list-strict
+- input for strict number of items in list
+- each child item can be different type with different possible modifiers
+- it is possible to display them in horizontal or vertical layout
+ - key `"horizontal"` as `True`/`False` (Default: `True`)
+- each child may have defined `"label"` which is shown next to input
+ - label does not reflect modifications or overrides (TODO)
+- children item are defined under key `"object_types"` which is list of dictionaries
+ - key `"children"` is not used because is used for hierarchy validations in schema
+- USAGE: For colors, transformations, etc. Custom number and different modifiers
+ give ability to define if color is HUE or RGB, 0-255, 0-1, 0-100 etc.
+
+```
+{
+ "type": "list-strict",
+ "key": "color",
+ "label": "Color",
+ "object_types": [
+ {
+ "label": "Red",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255,
+ "decimal": 0
+ }, {
+ "label": "Green",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255,
+ "decimal": 0
+ }, {
+ "label": "Blue",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255,
+ "decimal": 0
+ }, {
+ "label": "Alpha",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1,
+ "decimal": 6
+ }
+ ]
+}
+```
+
+
+## Noninteractive widgets
+- have nothing to do with data
+
+### label
+- add label with note or explanations
+- it is possible to use html tags inside the label
+
+```
+{
+ "type": "label",
+ "label": "RED LABEL: Normal label"
+}
+```
+
+### splitter
+- visual splitter of items (more divider than splitter)
+
+```
+{
+ "type": "splitter"
+}
+```
+
+## Proxy wrappers
+- should wraps multiple inputs only visually
+- these does not have `"key"` key and do not allow to have `"is_file"` or `"is_group"` modifiers enabled
+
+### form
+- DEPRECATED
+ - may be used only in `dict` and `dict-invisible` where is currently used grid layout so form is not needed
+ - item is kept as still may be used in specific cases
+- wraps inputs into form look layout
+- should be used only for Pure inputs
+
+```
+{
+ "type": "dict-form",
+ "children": [
+ {
+ "type": "text",
+ "key": "deadline_department",
+ "label": "Deadline apartment"
+ }, {
+ "type": "number",
+ "key": "deadline_priority",
+ "label": "Deadline priority"
+ }, {
+ ...
+ }
+ ]
+}
+```
diff --git a/pype/tools/settings/settings/__init__.py b/pype/tools/settings/settings/__init__.py
new file mode 100644
index 0000000000..0c2fd6d4bb
--- /dev/null
+++ b/pype/tools/settings/settings/__init__.py
@@ -0,0 +1,8 @@
+from . import style
+from .widgets import MainWidget
+
+
+__all__ = (
+ "style",
+ "MainWidget"
+)
diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/0_project_gui_schema.json b/pype/tools/settings/settings/gui_schemas/projects_schema/0_project_gui_schema.json
new file mode 100644
index 0000000000..cf95bf4c45
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/projects_schema/0_project_gui_schema.json
@@ -0,0 +1,30 @@
+{
+ "key": "project",
+ "type": "dict-invisible",
+ "children": [
+ {
+ "type": "anatomy",
+ "key": "project_anatomy",
+ "children": [
+ {
+ "type": "anatomy_roots",
+ "key": "roots",
+ "is_file": true
+ }, {
+ "type": "anatomy_templates",
+ "key": "templates",
+ "is_file": true
+ }
+ ]
+ }, {
+ "type": "dict-invisible",
+ "key": "project_settings",
+ "children": [
+ {
+ "type": "schema",
+ "name": "1_plugins_gui_schema"
+ }
+ ]
+ }
+ ]
+}
diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json b/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json
new file mode 100644
index 0000000000..f357b51dc5
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json
@@ -0,0 +1,745 @@
+{
+ "type": "dict",
+ "collapsable": true,
+ "key": "plugins",
+ "label": "Plugins",
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "key": "celaction",
+ "label": "CelAction",
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "key": "publish",
+ "label": "Publish plugins",
+ "is_file": true,
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "key": "ExtractCelactionDeadline",
+ "label": "ExtractCelactionDeadline",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }, {
+ "type": "text",
+ "key": "deadline_department",
+ "label": "Deadline apartment"
+ }, {
+ "type": "number",
+ "key": "deadline_priority",
+ "label": "Deadline priority"
+ }, {
+ "type": "text",
+ "key": "deadline_pool",
+ "label": "Deadline pool"
+ }, {
+ "type": "text",
+ "key": "deadline_pool_secondary",
+ "label": "Deadline pool (secondary)"
+ }, {
+ "type": "text",
+ "key": "deadline_group",
+ "label": "Deadline Group"
+ }, {
+ "type": "number",
+ "key": "deadline_chunk_size",
+ "label": "Deadline Chunk size"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "ftrack",
+ "label": "Ftrack",
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "key": "publish",
+ "label": "Publish plugins",
+ "is_file": true,
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "key": "IntegrateFtrackNote",
+ "label": "IntegrateFtrackNote",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }, {
+ "type": "text",
+ "key": "note_with_intent_template",
+ "label": "Note with intent template"
+ }, {
+ "type": "list",
+ "object_type": "text",
+ "key": "note_labels",
+ "label": "Note labels"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "global",
+ "label": "Global",
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "key": "publish",
+ "label": "Publish plugins",
+ "is_file": true,
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "key": "IntegrateMasterVersion",
+ "label": "IntegrateMasterVersion",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "key": "ExtractJpegEXR",
+ "label": "ExtractJpegEXR",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }, {
+ "type": "dict-invisible",
+ "key": "ffmpeg_args",
+ "children": [
+ {
+ "type": "list",
+ "object_type": "text",
+ "key": "input",
+ "label": "FFmpeg input arguments"
+ }, {
+ "type": "list",
+ "object_type": "text",
+ "key": "output",
+ "label": "FFmpeg output arguments"
+ }
+ ]
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "ExtractReview",
+ "label": "ExtractReview",
+ "checkbox_key": "enabled",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }, {
+ "type": "list",
+ "key": "profiles",
+ "label": "Profiles",
+ "object_type": "dict",
+ "input_modifiers": {
+ "children": [
+ {
+ "key": "families",
+ "label": "Families",
+ "type": "list",
+ "object_type": "text"
+ }, {
+ "key": "hosts",
+ "label": "Hosts",
+ "type": "list",
+ "object_type": "text"
+ }, {
+ "type": "splitter"
+ }, {
+ "key": "outputs",
+ "label": "Output Definitions",
+ "type": "dict-modifiable",
+ "highlight_content": true,
+ "object_type": "dict",
+ "input_modifiers": {
+ "children": [
+ {
+ "key": "ext",
+ "label": "Output extension",
+ "type": "text"
+ }, {
+ "key": "tags",
+ "label": "Tags",
+ "type": "list",
+ "object_type": "text"
+ }, {
+ "key": "ffmpeg_args",
+ "label": "FFmpeg arguments",
+ "type": "dict",
+ "highlight_content": true,
+ "children": [
+ {
+ "key": "video_filters",
+ "label": "Video filters",
+ "type": "list",
+ "object_type": "text"
+ }, {
+ "type": "splitter"
+ }, {
+ "key": "audio_filters",
+ "label": "Audio filters",
+ "type": "list",
+ "object_type": "text"
+ }, {
+ "type": "splitter"
+ }, {
+ "key": "input",
+ "label": "Input arguments",
+ "type": "list",
+ "object_type": "text"
+ }, {
+ "type": "splitter"
+ }, {
+ "key": "output",
+ "label": "Output arguments",
+ "type": "list",
+ "object_type": "text"
+ }
+ ]
+ }, {
+ "key": "filter",
+ "label": "Additional output filtering",
+ "type": "dict",
+ "highlight_content": true,
+ "children": [
+ {
+ "key": "families",
+ "label": "Families",
+ "type": "list",
+ "object_type": "text"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "ExtractBurnin",
+ "label": "ExtractBurnin",
+ "checkbox_key": "enabled",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "options",
+ "label": "Burnin formating options",
+ "children": [
+ {
+ "type": "number",
+ "key": "font_size",
+ "label": "Font size"
+ }, {
+ "type": "number",
+ "key": "opacity",
+ "label": "Font opacity"
+ }, {
+ "type": "number",
+ "key": "bg_opacity",
+ "label": "Background opacity"
+ }, {
+ "type": "number",
+ "key": "x_offset",
+ "label": "X Offset"
+ }, {
+ "type": "number",
+ "key": "y_offset",
+ "label": "Y Offset"
+ }, {
+ "type": "number",
+ "key": "bg_padding",
+ "label": "Padding aroung text"
+ }
+ ]
+ }, {
+ "type": "raw-json",
+ "key": "profiles",
+ "label": "Burnin profiles"
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "IntegrateAssetNew",
+ "label": "IntegrateAssetNew",
+ "is_group": true,
+ "children": [
+ {
+ "type": "raw-json",
+ "key": "template_name_profiles",
+ "label": "template_name_profiles"
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "ProcessSubmittedJobOnFarm",
+ "label": "ProcessSubmittedJobOnFarm",
+ "checkbox_key": "enabled",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }, {
+ "type": "text",
+ "key": "deadline_department",
+ "label": "Deadline department"
+ }, {
+ "type": "text",
+ "key": "deadline_pool",
+ "label": "Deadline Pool"
+ }, {
+ "type": "text",
+ "key": "deadline_group",
+ "label": "Deadline Group"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "maya",
+ "label": "Maya",
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "key": "publish",
+ "label": "Publish plugins",
+ "is_file": true,
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "key": "ValidateModelName",
+ "label": "Validate Model Name",
+ "checkbox_key": "enabled",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }, {
+ "type": "text",
+ "key": "material_file",
+ "label": "Material File"
+ }, {
+ "type": "text",
+ "key": "regex",
+ "label": "Validation regex"
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "ValidateAssemblyName",
+ "label": "Validate Assembly Name",
+ "checkbox_key": "enabled",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "ValidateShaderName",
+ "label": "ValidateShaderName",
+ "checkbox_key": "enabled",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }, {
+ "type": "text",
+ "key": "regex",
+ "label": "Validation regex"
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "ValidateMeshHasOverlappingUVs",
+ "label": "ValidateMeshHasOverlappingUVs",
+ "checkbox_key": "enabled",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }
+ ]
+ }
+ ]
+ }, {
+ "type": "raw-json",
+ "key": "workfile_build",
+ "label": "Workfile Build logic",
+ "is_file": true
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "nuke",
+ "label": "Nuke",
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "key": "create",
+ "label": "Create plugins",
+ "is_file": true,
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": false,
+ "key": "CreateWriteRender",
+ "label": "CreateWriteRender",
+ "is_group": true,
+ "children": [
+ {
+ "type": "text",
+ "key": "fpath_template",
+ "label": "Path template"
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": false,
+ "key": "CreateWritePrerender",
+ "label": "CreateWritePrerender",
+ "is_group": true,
+ "children": [
+ {
+ "type": "text",
+ "key": "fpath_template",
+ "label": "Path template"
+ }
+ ]
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "publish",
+ "label": "Publish plugins",
+ "is_file": true,
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "key": "ExtractThumbnail",
+ "label": "ExtractThumbnail",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }, {
+ "type": "raw-json",
+ "key": "nodes",
+ "label": "Nodes"
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "key": "ValidateNukeWriteKnobs",
+ "label": "ValidateNukeWriteKnobs",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }, {
+ "type": "raw-json",
+ "key": "knobs",
+ "label": "Knobs"
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "key": "ExtractReviewDataLut",
+ "label": "ExtractReviewDataLut",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "key": "ExtractReviewDataMov",
+ "label": "ExtractReviewDataMov",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }, {
+ "type": "boolean",
+ "key": "viewer_lut_raw",
+ "label": "Viewer LUT raw"
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "ExtractSlateFrame",
+ "label": "ExtractSlateFrame",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "viewer_lut_raw",
+ "label": "Viewer LUT raw"
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "NukeSubmitDeadline",
+ "label": "NukeSubmitDeadline",
+ "is_group": true,
+ "children": [
+ {
+ "type": "number",
+ "key": "deadline_priority",
+ "label": "deadline_priority"
+ }, {
+ "type": "text",
+ "key": "deadline_pool",
+ "label": "deadline_pool"
+ }, {
+ "type": "text",
+ "key": "deadline_pool_secondary",
+ "label": "deadline_pool_secondary"
+ }, {
+ "type": "number",
+ "key": "deadline_chunk_size",
+ "label": "deadline_chunk_size"
+ }
+ ]
+ }
+ ]
+ }, {
+ "type": "raw-json",
+ "key": "workfile_build",
+ "label": "Workfile Build logic",
+ "is_file": true
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "nukestudio",
+ "label": "NukeStudio",
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "key": "publish",
+ "label": "Publish plugins",
+ "is_file": true,
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "key": "CollectInstanceVersion",
+ "label": "Collect Instance Version",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "key": "ExtractReviewCutUpVideo",
+ "label": "Extract Review Cut Up Video",
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }, {
+ "type": "list",
+ "object_type": "text",
+ "key": "tags_addition",
+ "label": "Tags addition"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }, {
+ "type": "dict",
+ "collapsable": true,
+ "key": "resolve",
+ "label": "DaVinci Resolve",
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "key": "create",
+ "label": "Creator plugins",
+ "is_file": true,
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "key": "CreateShotClip",
+ "label": "Create Shot Clip",
+ "is_group": true,
+ "children": [
+ {
+ "type": "text",
+ "key": "clipName",
+ "label": "Clip name template"
+ }, {
+ "type": "text",
+ "key": "folder",
+ "label": "Folder"
+ }, {
+ "type": "number",
+ "key": "steps",
+ "label": "Steps"
+ }
+ ]
+ }
+
+ ]
+ }
+ ]
+ },
+ {
+ "type": "dict",
+ "collapsable": true,
+ "key": "standalonepublisher",
+ "label": "Standalone Publisher",
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "key": "publish",
+ "label": "Publish plugins",
+ "is_file": true,
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": true,
+ "key": "ExtractThumbnailSP",
+ "label": "ExtractThumbnailSP",
+ "is_group": true,
+ "children": [
+ {
+ "type": "dict",
+ "collapsable": false,
+ "key": "ffmpeg_args",
+ "label": "ffmpeg_args",
+ "children": [
+ {
+ "type": "list",
+ "object_type": "text",
+ "key": "input",
+ "label": "input"
+ },
+ {
+ "type": "list",
+ "object_type": "text",
+ "key": "output",
+ "label": "output"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json
new file mode 100644
index 0000000000..c5f229fc2f
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json
@@ -0,0 +1,23 @@
+{
+ "key": "system",
+ "type": "dict-invisible",
+ "children": [
+ {
+ "type": "dict-invisible",
+ "key": "global",
+ "children": [{
+ "type": "schema",
+ "name": "1_intents_gui_schema"
+ },{
+ "type": "schema",
+ "name": "1_modules_gui_schema"
+ }, {
+ "type": "schema",
+ "name": "1_applications_gui_schema"
+ }, {
+ "type": "schema",
+ "name": "1_tools_gui_schema"
+ }]
+ }
+ ]
+}
diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_applications_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_applications_gui_schema.json
new file mode 100644
index 0000000000..3427f98253
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_applications_gui_schema.json
@@ -0,0 +1,139 @@
+{
+ "key": "applications",
+ "type": "dict",
+ "label": "Applications",
+ "collapsable": true,
+ "is_group": true,
+ "is_file": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "blender_2.80",
+ "label": "Blender 2.80"
+ }, {
+ "type": "boolean",
+ "key": "blender_2.81",
+ "label": "Blender 2.81"
+ }, {
+ "type": "boolean",
+ "key": "blender_2.82",
+ "label": "Blender 2.82"
+ }, {
+ "type": "boolean",
+ "key": "blender_2.83",
+ "label": "Blender 2.83"
+ }, {
+ "type": "boolean",
+ "key": "celaction_local",
+ "label": "Celaction Local"
+ }, {
+ "type": "boolean",
+ "key": "celaction_remote",
+ "label": "Celaction Remote"
+ }, {
+ "type": "boolean",
+ "key": "harmony_17",
+ "label": "Harmony 17"
+ }, {
+ "type": "boolean",
+ "key": "maya_2017",
+ "label": "Autodest Maya 2017"
+ }, {
+ "type": "boolean",
+ "key": "maya_2018",
+ "label": "Autodest Maya 2018"
+ }, {
+ "type": "boolean",
+ "key": "maya_2019",
+ "label": "Autodest Maya 2019"
+ }, {
+ "type": "boolean",
+ "key": "maya_2020",
+ "label": "Autodest Maya 2020"
+ }, {
+ "key": "nuke_10.0",
+ "type": "boolean",
+ "label": "Nuke 10.0"
+ }, {
+ "type": "boolean",
+ "key": "nuke_11.2",
+ "label": "Nuke 11.2"
+ }, {
+ "type": "boolean",
+ "key": "nuke_11.3",
+ "label": "Nuke 11.3"
+ }, {
+ "type": "boolean",
+ "key": "nuke_12.0",
+ "label": "Nuke 12.0"
+ }, {
+ "type": "boolean",
+ "key": "nukex_10.0",
+ "label": "NukeX 10.0"
+ }, {
+ "type": "boolean",
+ "key": "nukex_11.2",
+ "label": "NukeX 11.2"
+ }, {
+ "type": "boolean",
+ "key": "nukex_11.3",
+ "label": "NukeX 11.3"
+ }, {
+ "type": "boolean",
+ "key": "nukex_12.0",
+ "label": "NukeX 12.0"
+ }, {
+ "type": "boolean",
+ "key": "nukestudio_10.0",
+ "label": "NukeStudio 10.0"
+ }, {
+ "type": "boolean",
+ "key": "nukestudio_11.2",
+ "label": "NukeStudio 11.2"
+ }, {
+ "type": "boolean",
+ "key": "nukestudio_11.3",
+ "label": "NukeStudio 11.3"
+ }, {
+ "type": "boolean",
+ "key": "nukestudio_12.0",
+ "label": "NukeStudio 12.0"
+ }, {
+ "type": "boolean",
+ "key": "houdini_16",
+ "label": "Houdini 16"
+ }, {
+ "type": "boolean",
+ "key": "houdini_16.5",
+ "label": "Houdini 16.5"
+ }, {
+ "type": "boolean",
+ "key": "houdini_17",
+ "label": "Houdini 17"
+ }, {
+ "type": "boolean",
+ "key": "houdini_18",
+ "label": "Houdini 18"
+ }, {
+ "type": "boolean",
+ "key": "premiere_2019",
+ "label": "Premiere 2019"
+ }, {
+ "type": "boolean",
+ "key": "premiere_2020",
+ "label": "Premiere 2020"
+ }, {
+ "type": "boolean",
+ "key": "resolve_16",
+ "label": "BM DaVinci Resolve 16"
+ }, {
+ "type": "boolean",
+ "key": "storyboardpro_7",
+ "label": "Storyboard Pro 7"
+ }, {
+ "type": "boolean",
+ "key": "unreal_4.24",
+ "label": "Unreal Editor 4.24"
+ }
+ ]
+}
diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json
new file mode 100644
index 0000000000..0578968508
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json
@@ -0,0 +1,372 @@
+{
+ "key": "example_dict",
+ "label": "Examples",
+ "type": "dict",
+ "is_file": true,
+ "children": [
+ {
+ "key": "dict_wrapper",
+ "type": "dict-invisible",
+ "children": [
+ {
+ "type": "boolean",
+ "key": "bool",
+ "label": "Boolean checkbox"
+ }, {
+ "type": "label",
+ "label": "NOTE: This is label"
+ }, {
+ "type": "splitter"
+ }, {
+ "type": "number",
+ "key": "integer",
+ "label": "Integer",
+ "decimal": 0,
+ "minimum": 0,
+ "maximum": 10
+ }, {
+ "type": "number",
+ "key": "float",
+ "label": "Float (2 decimals)",
+ "decimal": 2,
+ "minimum": -10,
+ "maximum": -5
+ }, {
+ "type": "text",
+ "key": "singleline_text",
+ "label": "Singleline text"
+ }, {
+ "type": "text",
+ "key": "multiline_text",
+ "label": "Multiline text",
+ "multiline": true
+ }, {
+ "type": "raw-json",
+ "key": "raw_json",
+ "label": "Raw json input"
+ }, {
+ "type": "list",
+ "key": "list_item_of_multiline_texts",
+ "label": "List of multiline texts",
+ "object_type": "text",
+ "input_modifiers": {
+ "multiline": true
+ }
+ }, {
+ "type": "list",
+ "key": "list_item_of_floats",
+ "label": "List of floats",
+ "object_type": "number",
+ "input_modifiers": {
+ "decimal": 3,
+ "minimum": 1000,
+ "maximum": 2000
+ }
+ }, {
+ "type": "dict-modifiable",
+ "key": "modifiable_dict_of_integers",
+ "label": "Modifiable dict of integers",
+ "object_type": "number",
+ "input_modifiers": {
+ "decimal": 0,
+ "minimum": 10,
+ "maximum": 100
+ }
+ }, {
+ "type": "list-strict",
+ "key": "strict_list_labels_horizontal",
+ "label": "StrictList-labels-horizontal (color)",
+ "object_types": [
+ {
+ "label": "Red",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255,
+ "decimal": 0
+ }, {
+ "label": "Green",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255,
+ "decimal": 0
+ }, {
+ "label": "Blue",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255,
+ "decimal": 0
+ }, {
+ "label": "Alpha",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1,
+ "decimal": 6
+ }
+ ]
+ }, {
+ "type": "list-strict",
+ "key": "strict_list_labels_vertical",
+ "label": "StrictList-labels-vertical (color)",
+ "horizontal": false,
+ "object_types": [
+ {
+ "label": "Red",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255,
+ "decimal": 0
+ }, {
+ "label": "Green",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255,
+ "decimal": 0
+ }, {
+ "label": "Blue",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255,
+ "decimal": 0
+ }, {
+ "label": "Alpha",
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1,
+ "decimal": 6
+ }
+ ]
+ }, {
+ "type": "list-strict",
+ "key": "strict_list_nolabels_horizontal",
+ "label": "StrictList-nolabels-horizontal (color)",
+ "object_types": [
+ {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255,
+ "decimal": 0
+ }, {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255,
+ "decimal": 0
+ }, {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255,
+ "decimal": 0
+ }, {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1,
+ "decimal": 6
+ }
+ ]
+ }, {
+ "type": "list-strict",
+ "key": "strict_list_nolabels_vertical",
+ "label": "StrictList-nolabels-vertical (color)",
+ "horizontal": false,
+ "object_types": [
+ {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255,
+ "decimal": 0
+ }, {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255,
+ "decimal": 0
+ }, {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 255,
+ "decimal": 0
+ }, {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1,
+ "decimal": 6
+ }
+ ]
+ }, {
+ "type": "list",
+ "key": "dict_item",
+ "label": "DictItem in List",
+ "object_type": "dict-item",
+ "input_modifiers": {
+ "children": [
+ {
+ "key": "families",
+ "label": "Families",
+ "type": "list",
+ "object_type": "text"
+ }, {
+ "key": "hosts",
+ "label": "Hosts",
+ "type": "list",
+ "object_type": "text"
+ }
+ ]
+ }
+ }, {
+ "type": "path-widget",
+ "key": "single_path_input",
+ "label": "Single path input",
+ "multiplatform": false,
+ "multipath": false
+ }, {
+ "type": "path-widget",
+ "key": "multi_path_input",
+ "label": "Multi path input",
+ "multiplatform": false,
+ "multipath": true
+ }, {
+ "type": "path-widget",
+ "key": "single_os_specific_path_input",
+ "label": "Single OS specific path input",
+ "multiplatform": true,
+ "multipath": false
+ }, {
+ "type": "path-widget",
+ "key": "multi_os_specific_path_input",
+ "label": "Multi OS specific path input",
+ "multiplatform": true,
+ "multipath": true
+ }, {
+ "key": "collapsable",
+ "type": "dict",
+ "label": "collapsable dictionary",
+ "collapsable": true,
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "_nothing",
+ "label": "Exmaple input"
+ }
+ ]
+ }, {
+ "key": "collapsable_expanded",
+ "type": "dict",
+ "label": "collapsable dictionary, expanded on creation",
+ "collapsable": true,
+ "collapsed": false,
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "_nothing",
+ "label": "Exmaple input"
+ }
+ ]
+ }, {
+ "key": "not_collapsable",
+ "type": "dict",
+ "label": "Not collapsable",
+ "collapsable": false,
+ "is_group": true,
+ "children": [
+ {
+ "type": "boolean",
+ "key": "_nothing",
+ "label": "Exmaple input"
+ }
+ ]
+ }, {
+ "key": "nested_dict_lvl1",
+ "type": "dict",
+ "label": "Nested dictionary (level 1)",
+ "children": [
+ {
+ "key": "nested_dict_lvl2",
+ "type": "dict",
+ "label": "Nested dictionary (level 2)",
+ "is_group": true,
+ "children": [
+ {
+ "key": "nested_dict_lvl3",
+ "type": "dict",
+ "label": "Nested dictionary (level 3)",
+ "children": [
+ {
+ "type": "boolean",
+ "key": "_nothing",
+ "label": "Exmaple input"
+ }
+ ]
+ }, {
+ "key": "nested_dict_lvl3_2",
+ "type": "dict",
+ "label": "Nested dictionary (level 3) (2)",
+ "children": [
+ {
+ "type": "text",
+ "key": "_nothing",
+ "label": "Exmaple input"
+ }, {
+ "type": "text",
+ "key": "_nothing2",
+ "label": "Exmaple input 2"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }, {
+ "key": "form_examples",
+ "type": "dict",
+ "label": "Form examples",
+ "children": [
+ {
+ "key": "inputs_without_form_example",
+ "type": "dict",
+ "label": "Inputs without form",
+ "children": [
+ {
+ "type": "text",
+ "key": "_nothing_1",
+ "label": "Example label"
+ }, {
+ "type": "text",
+ "key": "_nothing_2",
+ "label": "Example label ####"
+ }, {
+ "type": "text",
+ "key": "_nothing_3",
+ "label": "Example label ########"
+ }
+ ]
+ }, {
+ "key": "inputs_with_form_example",
+ "type": "dict",
+ "label": "Inputs with form",
+ "children": [
+ {
+ "type": "form",
+ "children": [
+ {
+ "type": "text",
+ "key": "_nothing_1",
+ "label": "Example label"
+ }, {
+ "type": "text",
+ "key": "_nothing_2",
+ "label": "Example label ####"
+ }, {
+ "type": "text",
+ "key": "_nothing_3",
+ "label": "Example label ########"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_intents_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_intents_gui_schema.json
new file mode 100644
index 0000000000..7f71da26cd
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_intents_gui_schema.json
@@ -0,0 +1,16 @@
+{
+ "key": "general",
+ "type": "dict",
+ "label": "General",
+ "collapsable": true,
+ "is_file": true,
+ "children": [{
+ "key": "studio_name",
+ "type": "text",
+ "label": "Studio Name"
+ },{
+ "key": "studio_code",
+ "type": "text",
+ "label": "Studio Short Code"
+ }
+]}
diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_modules_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_modules_gui_schema.json
new file mode 100644
index 0000000000..7b3d8cdd13
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_modules_gui_schema.json
@@ -0,0 +1,283 @@
+{
+ "key": "modules",
+ "type": "dict",
+ "label": "Modules",
+ "collapsable": true,
+ "is_file": true,
+ "children": [{
+ "type": "dict",
+ "key": "Avalon",
+ "label": "Avalon",
+ "collapsable": true,
+ "children": [
+ {
+ "type": "text",
+ "key": "AVALON_MONGO",
+ "label": "Avalon Mongo URL"
+ },
+ {
+ "type": "text",
+ "key": "AVALON_DB_DATA",
+ "label": "Avalon Mongo Data Location"
+ },
+ {
+ "type": "text",
+ "key": "AVALON_THUMBNAIL_ROOT",
+ "label": "Thumbnail Storage Location"
+ }
+ ]
+ },{
+ "type": "dict",
+ "key": "Ftrack",
+ "label": "Ftrack",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "children": [{
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ },
+ {
+ "type": "text",
+ "key": "ftrack_server",
+ "label": "Server"
+ },
+ {
+ "type": "label",
+ "label": "Additional Ftrack paths"
+ },
+ {
+ "type": "list",
+ "key": "ftrack_actions_path",
+ "label": "Action paths",
+ "object_type": "text"
+ },
+ {
+ "type": "list",
+ "key": "ftrack_events_path",
+ "label": "Event paths",
+ "object_type": "text"
+ },
+ {
+ "type": "label",
+ "label": "Ftrack event server advanced settings"
+ },
+ {
+ "type": "text",
+ "key": "FTRACK_EVENTS_MONGO_DB",
+ "label": "Event Mongo DB"
+ },
+ {
+ "type": "text",
+ "key": "FTRACK_EVENTS_MONGO_COL",
+ "label": "Events Mongo Collection"
+ },
+ {
+ "type": "dict",
+ "key": "sync_to_avalon",
+ "label": "Sync to avalon",
+ "children": [{
+ "type": "list",
+ "key": "statuses_name_change",
+ "label": "Status name change",
+ "object_type": "text",
+ "input_modifiers": {
+ "multiline": false
+ }
+ }]
+ },
+ {
+ "type": "dict-modifiable",
+ "key": "status_version_to_task",
+ "label": "Version to Task status mapping",
+ "object_type": "text"
+ },
+ {
+ "type": "dict-modifiable",
+ "key": "status_update",
+ "label": "Status Updates",
+ "object_type": "list",
+ "input_modifiers": {
+ "object_type": "text"
+ }
+ },
+ {
+ "key": "intent",
+ "type": "dict-invisible",
+ "children": [
+ {
+ "type": "dict-modifiable",
+ "object_type": "text",
+ "key": "items",
+ "label": "Intent Key/Label"
+ },
+ {
+ "key": "default",
+ "type": "text",
+ "label": "Defautl Intent"
+ }
+ ]
+ }
+ ]
+ }, {
+ "type": "dict",
+ "key": "Rest Api",
+ "label": "Rest Api",
+ "collapsable": true,
+ "children": [{
+ "type": "number",
+ "key": "default_port",
+ "label": "Default Port",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ {
+ "type": "list",
+ "object_type": "number",
+ "key": "exclude_ports",
+ "label": "Exclude ports",
+ "input_modifiers": {
+ "minimum": 1,
+ "maximum": 65535
+ }
+ }
+ ]
+ }, {
+ "type": "dict",
+ "key": "Timers Manager",
+ "label": "Timers Manager",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "children": [{
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ },
+ {
+ "type": "number",
+ "decimal": 2,
+ "key": "full_time",
+ "label": "Max idle time"
+ }, {
+ "type": "number",
+ "decimal": 2,
+ "key": "message_time",
+ "label": "When dialog will show"
+ }
+ ]
+ }, {
+ "type": "dict",
+ "key": "Clockify",
+ "label": "Clockify",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "children": [{
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ },
+ {
+ "type": "text",
+ "key": "workspace_name",
+ "label": "Workspace name"
+ }
+ ]
+ }, {
+ "type": "dict",
+ "key": "Deadline",
+ "label": "Deadline",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "children": [{
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ },{
+ "type": "text",
+ "key": "DEADLINE_REST_URL",
+ "label": "Deadline Resl URL"
+ }]
+ }, {
+ "type": "dict",
+ "key": "Muster",
+ "label": "Muster",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "children": [{
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ },{
+ "type": "text",
+ "key": "MUSTER_REST_URL",
+ "label": "Muster Resl URL"
+ },{
+ "type": "dict-modifiable",
+ "object_type": "number",
+ "input_modifiers": {
+ "minimum": 0,
+ "maximum": 300
+ },
+ "is_group": true,
+ "key": "templates_mapping",
+ "label": "Templates mapping",
+ "is_file": true
+ }]
+ }, {
+ "type": "dict",
+ "key": "Logging",
+ "label": "Logging",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "children": [{
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }]
+ }, {
+ "type": "dict",
+ "key": "Adobe Communicator",
+ "label": "Adobe Communicator",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "children": [{
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }]
+ }, {
+ "type": "dict",
+ "key": "User setting",
+ "label": "User setting",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "children": [{
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }]
+ }, {
+ "type": "dict",
+ "key": "Standalone Publish",
+ "label": "Standalone Publish",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "children": [{
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }]
+ }, {
+ "type": "dict",
+ "key": "Idle Manager",
+ "label": "Idle Manager",
+ "collapsable": true,
+ "checkbox_key": "enabled",
+ "children": [{
+ "type": "boolean",
+ "key": "enabled",
+ "label": "Enabled"
+ }]
+ }
+ ]
+}
diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_tools_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_tools_gui_schema.json
new file mode 100644
index 0000000000..08b8d13d89
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_tools_gui_schema.json
@@ -0,0 +1,27 @@
+{
+ "key": "tools",
+ "type": "dict",
+ "label": "Tools",
+ "collapsable": true,
+ "is_group": true,
+ "is_file": true,
+ "children": [
+ {
+ "key": "mtoa_3.0.1",
+ "type": "boolean",
+ "label": "Arnold Maya 3.0.1"
+ }, {
+ "key": "mtoa_3.1.1",
+ "type": "boolean",
+ "label": "Arnold Maya 3.1.1"
+ }, {
+ "key": "mtoa_3.2.0",
+ "type": "boolean",
+ "label": "Arnold Maya 3.2.0"
+ }, {
+ "key": "yeti_2.1.2",
+ "type": "boolean",
+ "label": "Yeti 2.1.2"
+ }
+ ]
+}
diff --git a/pype/tools/settings/settings/style/__init__.py b/pype/tools/settings/settings/style/__init__.py
new file mode 100644
index 0000000000..a8f202d97b
--- /dev/null
+++ b/pype/tools/settings/settings/style/__init__.py
@@ -0,0 +1,12 @@
+import os
+
+
+def load_stylesheet():
+ style_path = os.path.join(os.path.dirname(__file__), "style.css")
+ with open(style_path, "r") as style_file:
+ stylesheet = style_file.read()
+ return stylesheet
+
+
+def app_icon_path():
+ return os.path.join(os.path.dirname(__file__), "pype_icon.png")
diff --git a/pype/tools/settings/settings/style/pype_icon.png b/pype/tools/settings/settings/style/pype_icon.png
new file mode 100644
index 0000000000..bfacf6eeed
Binary files /dev/null and b/pype/tools/settings/settings/style/pype_icon.png differ
diff --git a/pype/tools/settings/settings/style/style.css b/pype/tools/settings/settings/style/style.css
new file mode 100644
index 0000000000..3f648abef8
--- /dev/null
+++ b/pype/tools/settings/settings/style/style.css
@@ -0,0 +1,321 @@
+QWidget {
+ color: #bfccd6;
+ background-color: #293742;
+ font-size: 12px;
+ border-radius: 0px;
+}
+
+QMenu {
+ border: 1px solid #555555;
+ background-color: #1d272f;
+}
+
+QMenu::item {
+ padding: 5px 10px 5px 10px;
+ border-left: 5px solid #313131;
+}
+
+QMenu::item:selected {
+ border-left-color: #61839e;
+ background-color: #222d37;
+}
+QCheckBox {
+ spacing: 0px;
+}
+QCheckBox::indicator {}
+QCheckBox::indicator:focus {}
+
+QLineEdit, QSpinBox, QDoubleSpinBox, QPlainTextEdit, QTextEdit {
+ border: 1px solid #aaaaaa;
+ border-radius: 3px;
+ background-color: #1d272f;
+}
+
+QLineEdit:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled, QPlainTextEdit:disabled, QTextEdit:disabled, QPushButton:disabled {
+ background-color: #4e6474;
+}
+
+QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QPlainTextEdit:focus, QTextEdit:focus {
+ border: 1px solid #ffffff;
+}
+QToolButton {
+ background: transparent;
+}
+
+QLabel {
+ background: transparent;
+ color: #7390a5;
+}
+QLabel:hover {color: #839caf;}
+
+QLabel[state="studio"] {color: #bfccd6;}
+QLabel[state="studio"]:hover {color: #ffffff;}
+QLabel[state="modified"] {color: #137cbd;}
+QLabel[state="modified"]:hover {color: #1798e8;}
+QLabel[state="overriden-modified"] {color: #137cbd;}
+QLabel[state="overriden-modified"]:hover {color: #1798e8;}
+QLabel[state="overriden"] {color: #ff8c1a;}
+QLabel[state="overriden"]:hover {color: #ffa64d;}
+QLabel[state="invalid"] {color: #ad2e2e;}
+QLabel[state="invalid"]:hover {color: #ad2e2e;}
+
+
+QWidget[input-state="studio"] {border-color: #bfccd6;}
+QWidget[input-state="modified"] {border-color: #137cbd;}
+QWidget[input-state="overriden-modified"] {border-color: #137cbd;}
+QWidget[input-state="overriden"] {border-color: #ff8c1a;}
+QWidget[input-state="invalid"] {border-color: #ad2e2e;}
+
+QPushButton {
+ border: 1px solid #aaaaaa;
+ border-radius: 3px;
+ padding: 5px;
+}
+QPushButton:hover {
+ background-color: #31424e;
+}
+QPushButton[btn-type="tool-item"] {
+ border: 1px solid #bfccd6;
+ border-radius: 10px;
+}
+
+QPushButton[btn-type="tool-item"]:hover {
+ border-color: #137cbd;
+ color: #137cbd;
+ background-color: transparent;
+}
+
+QPushButton[btn-type="expand-toggle"] {
+ background: #1d272f;
+}
+
+#GroupWidget {
+ border-bottom: 1px solid #1d272f;
+}
+
+#ProjectListWidget QListView {
+ border: 1px solid #aaaaaa;
+ background: #1d272f;
+}
+#ProjectListWidget QLabel {
+ background: transparent;
+ font-weight: bold;
+}
+
+#DictKey[state="studio"] {border-color: #bfccd6;}
+#DictKey[state="modified"] {border-color: #137cbd;}
+#DictKey[state="overriden"] {border-color: #00f;}
+#DictKey[state="overriden-modified"] {border-color: #0f0;}
+#DictKey[state="invalid"] {border-color: #ad2e2e;}
+
+#DictLabel {
+ font-weight: bold;
+}
+
+#ContentWidget {
+ background-color: transparent;
+}
+#ContentWidget[content_state="hightlighted"] {
+ background-color: rgba(19, 26, 32, 15%);
+}
+
+#SideLineWidget {
+ background-color: #31424e;
+ border-style: solid;
+ border-color: #3b4f5e;
+ border-left-width: 3px;
+ border-bottom-width: 0px;
+ border-right-width: 0px;
+ border-top-width: 0px;
+}
+
+#SideLineWidget:hover {
+ border-color: #58768d;
+}
+
+#SideLineWidget[state="child-studio"] {border-color: #455c6e;}
+#SideLineWidget[state="child-studio"]:hover {border-color: #62839d;}
+
+#SideLineWidget[state="child-modified"] {border-color: #106aa2;}
+#SideLineWidget[state="child-modified"]:hover {border-color: #137cbd;}
+
+#SideLineWidget[state="child-invalid"] {border-color: #ad2e2e;}
+#SideLineWidget[state="child-invalid"]:hover {border-color: #c93636;}
+
+#SideLineWidget[state="child-overriden"] {border-color: #e67300;}
+#SideLineWidget[state="child-overriden"]:hover {border-color: #ff8c1a;}
+
+#SideLineWidget[state="child-overriden-modified"] {border-color: #106aa2;}
+#SideLineWidget[state="child-overriden-modified"]:hover {border-color: #137cbd;}
+
+#MainWidget {
+ background: #141a1f;
+}
+
+#DictAsWidgetBody{
+ background: transparent;
+ border: 2px solid #cccccc;
+ border-radius: 5px;
+}
+
+#SplitterItem {
+ background-color: #1d272f;
+}
+
+QTabWidget::pane {
+ border-top-style: none;
+}
+
+QTabBar {
+ background: transparent;
+}
+
+QTabBar::tab {
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ padding: 5px;
+}
+
+QTabBar::tab:selected {
+ background: #293742;
+ border-color: #9B9B9B;
+ border-bottom-color: #C2C7CB;
+}
+
+QTabBar::tab:!selected {
+ margin-top: 2px;
+ background: #1d272f;
+}
+
+QTabBar::tab:!selected:hover {
+ background: #3b4f5e;
+}
+
+
+
+QTabBar::tab:first:selected {
+ margin-left: 0;
+}
+
+QTabBar::tab:last:selected {
+ margin-right: 0;
+}
+
+QTabBar::tab:only-one {
+ margin: 0;
+}
+
+QScrollBar:horizontal {
+ height: 15px;
+ margin: 3px 15px 3px 15px;
+ border: 1px transparent #1d272f;
+ border-radius: 4px;
+ background-color: #1d272f;
+}
+
+QScrollBar::handle:horizontal {
+ background-color: #61839e;
+ min-width: 5px;
+ border-radius: 4px;
+}
+
+QScrollBar::add-line:horizontal {
+ margin: 0px 3px 0px 3px;
+ border-image: url(:/qss_icons/rc/right_arrow_disabled.png);
+ width: 10px;
+ height: 10px;
+ subcontrol-position: right;
+ subcontrol-origin: margin;
+}
+
+QScrollBar::sub-line:horizontal {
+ margin: 0px 3px 0px 3px;
+ border-image: url(:/qss_icons/rc/left_arrow_disabled.png);
+ height: 10px;
+ width: 10px;
+ subcontrol-position: left;
+ subcontrol-origin: margin;
+}
+
+QScrollBar::add-line:horizontal:hover,QScrollBar::add-line:horizontal:on {
+ border-image: url(:/qss_icons/rc/right_arrow.png);
+ height: 10px;
+ width: 10px;
+ subcontrol-position: right;
+ subcontrol-origin: margin;
+}
+
+QScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on {
+ border-image: url(:/qss_icons/rc/left_arrow.png);
+ height: 10px;
+ width: 10px;
+ subcontrol-position: left;
+ subcontrol-origin: margin;
+}
+
+QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal {
+ background: none;
+}
+
+QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
+ background: none;
+}
+
+QScrollBar:vertical {
+ background-color: #1d272f;
+ width: 15px;
+ margin: 15px 3px 15px 3px;
+ border: 1px transparent #1d272f;
+ border-radius: 4px;
+}
+
+QScrollBar::handle:vertical {
+ background-color: #61839e;
+ min-height: 5px;
+ border-radius: 4px;
+}
+
+QScrollBar::sub-line:vertical {
+ margin: 3px 0px 3px 0px;
+ border-image: url(:/qss_icons/rc/up_arrow_disabled.png);
+ height: 10px;
+ width: 10px;
+ subcontrol-position: top;
+ subcontrol-origin: margin;
+}
+
+QScrollBar::add-line:vertical {
+ margin: 3px 0px 3px 0px;
+ border-image: url(:/qss_icons/rc/down_arrow_disabled.png);
+ height: 10px;
+ width: 10px;
+ subcontrol-position: bottom;
+ subcontrol-origin: margin;
+}
+
+QScrollBar::sub-line:vertical:hover,QScrollBar::sub-line:vertical:on {
+
+ border-image: url(:/qss_icons/rc/up_arrow.png);
+ height: 10px;
+ width: 10px;
+ subcontrol-position: top;
+ subcontrol-origin: margin;
+}
+
+
+QScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on {
+ border-image: url(:/qss_icons/rc/down_arrow.png);
+ height: 10px;
+ width: 10px;
+ subcontrol-position: bottom;
+ subcontrol-origin: margin;
+}
+
+QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {
+ background: none;
+}
+
+
+QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
+ background: none;
+}
diff --git a/pype/tools/settings/settings/widgets/__init__.py b/pype/tools/settings/settings/widgets/__init__.py
new file mode 100644
index 0000000000..361fd9d23d
--- /dev/null
+++ b/pype/tools/settings/settings/widgets/__init__.py
@@ -0,0 +1,9 @@
+from .window import MainWidget
+from . import item_types
+from . import anatomy_types
+
+__all__ = [
+ "MainWidget",
+ "item_types",
+ "anatomy_types"
+]
diff --git a/pype/tools/settings/settings/widgets/anatomy_types.py b/pype/tools/settings/settings/widgets/anatomy_types.py
new file mode 100644
index 0000000000..6d7b3292ce
--- /dev/null
+++ b/pype/tools/settings/settings/widgets/anatomy_types.py
@@ -0,0 +1,758 @@
+from Qt import QtWidgets, QtCore
+from .widgets import ExpandingWidget
+from .item_types import (
+ SettingObject, ModifiableDict, PathWidget, RawJsonWidget
+)
+from .lib import NOT_SET, TypeToKlass, CHILD_OFFSET, METADATA_KEY
+
+
+class AnatomyWidget(QtWidgets.QWidget, SettingObject):
+ value_changed = QtCore.Signal(object)
+ template_keys = (
+ "project[name]",
+ "project[code]",
+ "asset",
+ "task",
+ "subset",
+ "family",
+ "version",
+ "ext",
+ "representation"
+ )
+ default_exmaple_data = {
+ "project": {
+ "name": "ProjectPype",
+ "code": "pp",
+ },
+ "asset": "sq01sh0010",
+ "task": "compositing",
+ "subset": "renderMain",
+ "family": "render",
+ "version": 1,
+ "ext": ".png",
+ "representation": "png"
+ }
+
+ def __init__(
+ self, input_data, parent, as_widget=False, label_widget=None
+ ):
+ if as_widget:
+ raise TypeError(
+ "`AnatomyWidget` does not allow to be used as widget."
+ )
+ super(AnatomyWidget, self).__init__(parent)
+ self.setObjectName("AnatomyWidget")
+
+ self.initial_attributes(input_data, parent, as_widget)
+
+ self.key = input_data["key"]
+
+ children_data = input_data["children"]
+ roots_input_data = {}
+ templates_input_data = {}
+ for child in children_data:
+ if child["type"] == "anatomy_roots":
+ roots_input_data = child
+ elif child["type"] == "anatomy_templates":
+ templates_input_data = child
+
+ self.root_widget = RootsWidget(roots_input_data, self)
+ self.templates_widget = TemplatesWidget(templates_input_data, self)
+
+ self.setAttribute(QtCore.Qt.WA_StyledBackground)
+
+ body_widget = ExpandingWidget("Anatomy", self)
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+ layout.addWidget(body_widget)
+
+ content_widget = QtWidgets.QWidget(body_widget)
+ content_layout = QtWidgets.QVBoxLayout(content_widget)
+ content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0)
+ content_layout.setSpacing(5)
+
+ content_layout.addWidget(self.root_widget)
+ content_layout.addWidget(self.templates_widget)
+
+ body_widget.set_content_widget(content_widget)
+
+ self.body_widget = body_widget
+ self.label_widget = body_widget.label_widget
+
+ self.root_widget.value_changed.connect(self._on_value_change)
+ self.templates_widget.value_changed.connect(self._on_value_change)
+
+ def update_default_values(self, parent_values):
+ self._state = None
+ self._child_state = None
+
+ if isinstance(parent_values, dict):
+ value = parent_values.get(self.key, NOT_SET)
+ else:
+ value = NOT_SET
+
+ self.root_widget.update_default_values(value)
+ self.templates_widget.update_default_values(value)
+
+ def update_studio_values(self, parent_values):
+ self._state = None
+ self._child_state = None
+
+ if isinstance(parent_values, dict):
+ value = parent_values.get(self.key, NOT_SET)
+ else:
+ value = NOT_SET
+
+ self.root_widget.update_studio_values(value)
+ self.templates_widget.update_studio_values(value)
+
+ def apply_overrides(self, parent_values):
+ # Make sure this is set to False
+ self._state = None
+ self._child_state = None
+
+ value = NOT_SET
+ if parent_values is not NOT_SET:
+ value = parent_values.get(self.key, value)
+
+ self.root_widget.apply_overrides(value)
+ self.templates_widget.apply_overrides(value)
+
+ def set_value(self, value):
+ raise TypeError("AnatomyWidget does not allow to use `set_value`")
+
+ def _on_value_change(self, item=None):
+ if self.ignore_value_changes:
+ return
+
+ self.hierarchical_style_update()
+
+ self.value_changed.emit(self)
+
+ def update_style(self, is_overriden=None):
+ child_has_studio_override = self.child_has_studio_override
+ child_modified = self.child_modified
+ child_invalid = self.child_invalid
+ child_state = self.style_state(
+ child_has_studio_override,
+ child_invalid,
+ self.child_overriden,
+ child_modified
+ )
+ if child_state:
+ child_state = "child-{}".format(child_state)
+
+ if child_state != self._child_state:
+ self.body_widget.side_line_widget.setProperty("state", child_state)
+ self.body_widget.side_line_widget.style().polish(
+ self.body_widget.side_line_widget
+ )
+ self._child_state = child_state
+
+ def hierarchical_style_update(self):
+ self.root_widget.hierarchical_style_update()
+ self.templates_widget.hierarchical_style_update()
+ self.update_style()
+
+ @property
+ def child_has_studio_override(self):
+ return (
+ self.root_widget.child_has_studio_override
+ or self.templates_widget.child_has_studio_override
+ )
+
+ @property
+ def child_modified(self):
+ return (
+ self.root_widget.child_modified
+ or self.templates_widget.child_modified
+ )
+
+ @property
+ def child_overriden(self):
+ return (
+ self.root_widget.child_overriden
+ or self.templates_widget.child_overriden
+ )
+
+ @property
+ def child_invalid(self):
+ return (
+ self.root_widget.child_invalid
+ or self.templates_widget.child_invalid
+ )
+
+ def set_as_overriden(self):
+ self.root_widget.set_as_overriden()
+ self.templates_widget.set_as_overriden()
+
+ def remove_overrides(self):
+ self.root_widget.remove_overrides()
+ self.templates_widget.remove_overrides()
+
+ def reset_to_pype_default(self):
+ self.root_widget.reset_to_pype_default()
+ self.templates_widget.reset_to_pype_default()
+
+ def set_studio_default(self):
+ self.root_widget.set_studio_default()
+ self.templates_widget.set_studio_default()
+
+ def discard_changes(self):
+ self.root_widget.discard_changes()
+ self.templates_widget.discard_changes()
+
+ def overrides(self):
+ if self.child_overriden:
+ return self.config_value(), True
+ return NOT_SET, False
+
+ def item_value(self):
+ output = {}
+ output.update(self.root_widget.config_value())
+ output.update(self.templates_widget.config_value())
+ return output
+
+ def studio_overrides(self):
+ if (
+ self.root_widget.child_has_studio_override
+ or self.templates_widget.child_has_studio_override
+ ):
+ groups = [self.root_widget.key, self.templates_widget.key]
+ value = self.config_value()
+ value[self.key][METADATA_KEY] = {"groups": groups}
+ return value, True
+ return NOT_SET, False
+
+ def config_value(self):
+ return {self.key: self.item_value()}
+
+
+class RootsWidget(QtWidgets.QWidget, SettingObject):
+ value_changed = QtCore.Signal(object)
+
+ def __init__(self, input_data, parent):
+ super(RootsWidget, self).__init__(parent)
+ self.setObjectName("RootsWidget")
+
+ input_data["is_group"] = True
+ self.initial_attributes(input_data, parent, False)
+
+ self.key = input_data["key"]
+
+ self._multiroot_state = None
+ self.default_is_multiroot = False
+ self.studio_is_multiroot = False
+ self.was_multiroot = NOT_SET
+
+ checkbox_widget = QtWidgets.QWidget(self)
+ multiroot_label = QtWidgets.QLabel(
+ "Use multiple roots", checkbox_widget
+ )
+ multiroot_checkbox = QtWidgets.QCheckBox(checkbox_widget)
+
+ checkbox_layout = QtWidgets.QHBoxLayout(checkbox_widget)
+ checkbox_layout.addWidget(multiroot_label, 0)
+ checkbox_layout.addWidget(multiroot_checkbox, 1)
+
+ body_widget = ExpandingWidget("Roots", self)
+ content_widget = QtWidgets.QWidget(body_widget)
+
+ path_widget_data = {
+ "key": self.key,
+ "multipath": False,
+ "multiplatform": True
+ }
+ singleroot_widget = PathWidget(
+ path_widget_data, self,
+ as_widget=True, parent_widget=content_widget
+ )
+ multiroot_data = {
+ "key": self.key,
+ "object_type": "path-widget",
+ "expandable": False,
+ "input_modifiers": {
+ "multiplatform": True
+ }
+ }
+ multiroot_widget = ModifiableDict(
+ multiroot_data, self,
+ as_widget=True, parent_widget=content_widget
+ )
+
+ content_layout = QtWidgets.QVBoxLayout(content_widget)
+ content_layout.setContentsMargins(0, 0, 0, 0)
+ content_layout.addWidget(checkbox_widget)
+ content_layout.addWidget(singleroot_widget)
+ content_layout.addWidget(multiroot_widget)
+
+ body_widget.set_content_widget(content_widget)
+ self.label_widget = body_widget.label_widget
+
+ main_layout = QtWidgets.QVBoxLayout(self)
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.addWidget(body_widget)
+
+ self.body_widget = body_widget
+ self.multiroot_label = multiroot_label
+ self.multiroot_checkbox = multiroot_checkbox
+ self.singleroot_widget = singleroot_widget
+ self.multiroot_widget = multiroot_widget
+
+ multiroot_checkbox.stateChanged.connect(self._on_multiroot_checkbox)
+ singleroot_widget.value_changed.connect(self._on_value_change)
+ multiroot_widget.value_changed.connect(self._on_value_change)
+
+ self._on_multiroot_checkbox()
+
+ @property
+ def is_multiroot(self):
+ return self.multiroot_checkbox.isChecked()
+
+ def update_default_values(self, parent_values):
+ self._state = None
+ self._multiroot_state = None
+ self._is_modified = False
+
+ if isinstance(parent_values, dict):
+ value = parent_values.get(self.key, NOT_SET)
+ else:
+ value = NOT_SET
+
+ is_multiroot = False
+ if isinstance(value, dict):
+ for _value in value.values():
+ if isinstance(_value, dict):
+ is_multiroot = True
+ break
+
+ self.default_is_multiroot = is_multiroot
+ self.was_multiroot = is_multiroot
+ self.set_multiroot(is_multiroot)
+
+ self._has_studio_override = False
+ self._had_studio_override = False
+ if is_multiroot:
+ for _value in value.values():
+ singleroot_value = _value
+ break
+
+ multiroot_value = value
+ else:
+ singleroot_value = value
+ multiroot_value = {"": value}
+
+ self.singleroot_widget.update_default_values(singleroot_value)
+ self.multiroot_widget.update_default_values(multiroot_value)
+
+ def update_studio_values(self, parent_values):
+ self._state = None
+ self._multiroot_state = None
+ self._is_modified = False
+
+ if isinstance(parent_values, dict):
+ value = parent_values.get(self.key, NOT_SET)
+ else:
+ value = NOT_SET
+
+ if value is NOT_SET:
+ is_multiroot = self.default_is_multiroot
+ self.studio_is_multiroot = NOT_SET
+ self._has_studio_override = False
+ self._had_studio_override = False
+ else:
+ is_multiroot = False
+ if isinstance(value, dict):
+ for _value in value.values():
+ if isinstance(_value, dict):
+ is_multiroot = True
+ break
+ self.studio_is_multiroot = is_multiroot
+ self._has_studio_override = True
+ self._had_studio_override = True
+
+ self.was_multiroot = is_multiroot
+ self.set_multiroot(is_multiroot)
+
+ if is_multiroot:
+ self.multiroot_widget.update_studio_values(value)
+ else:
+ self.singleroot_widget.update_studio_values(value)
+
+ def apply_overrides(self, parent_values):
+ # Make sure this is set to False
+ self._state = None
+ self._multiroot_state = None
+ self._is_modified = False
+
+ value = NOT_SET
+ if parent_values is not NOT_SET:
+ value = parent_values.get(self.key, value)
+
+ if value is NOT_SET:
+ is_multiroot = self.studio_is_multiroot
+ if is_multiroot is NOT_SET:
+ is_multiroot = self.default_is_multiroot
+ else:
+ is_multiroot = False
+ if isinstance(value, dict):
+ for _value in value.values():
+ if isinstance(_value, dict):
+ is_multiroot = True
+ break
+
+ self.was_multiroot = is_multiroot
+ self.set_multiroot(is_multiroot)
+
+ if is_multiroot:
+ self._is_overriden = value is not NOT_SET
+ self._was_overriden = bool(self._is_overriden)
+ self.multiroot_widget.apply_overrides(value)
+ else:
+ self._is_overriden = value is not NOT_SET
+ self._was_overriden = bool(self._is_overriden)
+ self.singleroot_widget.apply_overrides(value)
+
+ def hierarchical_style_update(self):
+ self.singleroot_widget.hierarchical_style_update()
+ self.multiroot_widget.hierarchical_style_update()
+ self.update_style()
+
+ def update_style(self):
+ multiroot_state = self.style_state(
+ self.has_studio_override,
+ False,
+ False,
+ self.was_multiroot != self.is_multiroot
+ )
+ if multiroot_state != self._multiroot_state:
+ self.multiroot_label.setProperty("state", multiroot_state)
+ self.multiroot_label.style().polish(self.multiroot_label)
+ self._multiroot_state = multiroot_state
+
+ state = self.style_state(
+ self.has_studio_override,
+ self.child_invalid,
+ self.is_overriden,
+ self.is_modified
+ )
+ if self._state == state:
+ return
+
+ if state:
+ child_state = "child-{}".format(state)
+ else:
+ child_state = ""
+
+ self.body_widget.side_line_widget.setProperty("state", child_state)
+ self.body_widget.side_line_widget.style().polish(
+ self.body_widget.side_line_widget
+ )
+
+ self.label_widget.setProperty("state", state)
+ self.label_widget.style().polish(self.label_widget)
+
+ self._state = state
+
+ def _on_multiroot_checkbox(self):
+ self.set_multiroot()
+
+ def _on_value_change(self, item=None):
+ if self.ignore_value_changes:
+ return
+
+ if item is not None and (
+ (self.is_multiroot and item != self.multiroot_widget)
+ or (not self.is_multiroot and item != self.singleroot_widget)
+ ):
+ return
+
+ if self.is_group and self.is_overidable:
+ self._is_overriden = True
+
+ self._is_modified = (
+ self.was_multiroot != self.is_multiroot
+ or self.child_modified
+ )
+
+ self.update_style()
+
+ self.value_changed.emit(self)
+
+ def _from_single_to_multi(self):
+ single_value = self.singleroot_widget.item_value()
+ mutli_value = self.multiroot_widget.item_value()
+ first_key = None
+ for key in mutli_value.keys():
+ first_key = key
+ break
+
+ if first_key is None:
+ first_key = ""
+
+ mutli_value[first_key] = single_value
+
+ self.multiroot_widget.set_value(mutli_value)
+
+ def _from_multi_to_single(self):
+ mutli_value = self.multiroot_widget.all_item_values()
+ for value in mutli_value.values():
+ single_value = value
+ break
+
+ self.singleroot_widget.set_value(single_value)
+
+ def set_multiroot(self, is_multiroot=None):
+ if is_multiroot is None:
+ is_multiroot = self.is_multiroot
+ if is_multiroot:
+ self._from_single_to_multi()
+ else:
+ self._from_multi_to_single()
+
+ if is_multiroot != self.is_multiroot:
+ self.multiroot_checkbox.setChecked(is_multiroot)
+
+ self.singleroot_widget.setVisible(not is_multiroot)
+ self.multiroot_widget.setVisible(is_multiroot)
+
+ self._on_value_change()
+
+ @property
+ def child_has_studio_override(self):
+ if self.is_multiroot:
+ return self.multiroot_widget.has_studio_override
+ else:
+ return self.singleroot_widget.has_studio_override
+
+ @property
+ def child_modified(self):
+ if self.is_multiroot:
+ return self.multiroot_widget.child_modified
+ else:
+ return self.singleroot_widget.child_modified
+
+ @property
+ def child_overriden(self):
+ if self.is_multiroot:
+ return (
+ self.multiroot_widget.is_overriden
+ or self.multiroot_widget.child_overriden
+ )
+ else:
+ return (
+ self.singleroot_widget.is_overriden
+ or self.singleroot_widget.child_overriden
+ )
+
+ @property
+ def child_invalid(self):
+ if self.is_multiroot:
+ return self.multiroot_widget.child_invalid
+ else:
+ return self.singleroot_widget.child_invalid
+
+ def remove_overrides(self):
+ self._is_overriden = False
+ self._is_modified = False
+
+ if self.studio_is_multiroot is NOT_SET:
+ self.set_multiroot(self.default_is_multiroot)
+ else:
+ self.set_multiroot(self.studio_is_multiroot)
+
+ if self.is_multiroot:
+ self.multiroot_widget.remove_overrides()
+ else:
+ self.singleroot_widget.remove_overrides()
+
+ def reset_to_pype_default(self):
+ self.set_multiroot(self.default_is_multiroot)
+ if self.is_multiroot:
+ self.multiroot_widget.reset_to_pype_default()
+ else:
+ self.singleroot_widget.reset_to_pype_default()
+ self._has_studio_override = False
+
+ def set_studio_default(self):
+ if self.is_multiroot:
+ self.multiroot_widget.reset_to_pype_default()
+ else:
+ self.singleroot_widget.reset_to_pype_default()
+ self._has_studio_override = True
+
+ def discard_changes(self):
+ self._is_overriden = self._was_overriden
+ self._is_modified = False
+ if self._is_overriden:
+ self.set_multiroot(self.was_multiroot)
+ else:
+ if self.studio_is_multiroot is NOT_SET:
+ self.set_multiroot(self.default_is_multiroot)
+ else:
+ self.set_multiroot(self.studio_is_multiroot)
+
+ if self.is_multiroot:
+ self.multiroot_widget.discard_changes()
+ else:
+ self.singleroot_widget.discard_changes()
+
+ self._is_modified = self.child_modified
+ self._has_studio_override = self._had_studio_override
+
+ def set_as_overriden(self):
+ self._is_overriden = True
+ self.singleroot_widget.set_as_overriden()
+ self.multiroot_widget.set_as_overriden()
+
+ def item_value(self):
+ if self.is_multiroot:
+ return self.multiroot_widget.item_value()
+ else:
+ return self.singleroot_widget.item_value()
+
+ def config_value(self):
+ return {self.key: self.item_value()}
+
+
+class TemplatesWidget(QtWidgets.QWidget, SettingObject):
+ value_changed = QtCore.Signal(object)
+
+ def __init__(self, input_data, parent):
+ super(TemplatesWidget, self).__init__(parent)
+
+ input_data["is_group"] = True
+ self.initial_attributes(input_data, parent, False)
+
+ self.key = input_data["key"]
+
+ body_widget = ExpandingWidget("Templates", self)
+ content_widget = QtWidgets.QWidget(body_widget)
+ body_widget.set_content_widget(content_widget)
+ content_layout = QtWidgets.QVBoxLayout(content_widget)
+
+ template_input_data = {
+ "key": self.key
+ }
+ self.body_widget = body_widget
+ self.label_widget = body_widget.label_widget
+ self.value_input = RawJsonWidget(
+ template_input_data, self,
+ label_widget=self.label_widget
+ )
+ content_layout.addWidget(self.value_input)
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+
+ layout.addWidget(body_widget)
+
+ self.value_input.value_changed.connect(self._on_value_change)
+
+ def _on_value_change(self, item):
+ self.update_style()
+
+ self.value_changed.emit(self)
+
+ def update_default_values(self, values):
+ self._state = None
+ self.value_input.update_default_values(values)
+
+ def update_studio_values(self, values):
+ self._state = None
+ self.value_input.update_studio_values(values)
+
+ def apply_overrides(self, parent_values):
+ self._state = None
+ self.value_input.apply_overrides(parent_values)
+
+ def hierarchical_style_update(self):
+ self.value_input.hierarchical_style_update()
+ self.update_style()
+
+ def update_style(self):
+ state = self.style_state(
+ self.has_studio_override,
+ self.child_invalid,
+ self.child_overriden,
+ self.child_modified
+ )
+ if self._state == state:
+ return
+
+ if state:
+ child_state = "child-{}".format(state)
+ else:
+ child_state = ""
+
+ self.body_widget.side_line_widget.setProperty("state", child_state)
+ self.body_widget.side_line_widget.style().polish(
+ self.body_widget.side_line_widget
+ )
+
+ self.label_widget.setProperty("state", state)
+ self.label_widget.style().polish(self.label_widget)
+
+ self._state = state
+
+ @property
+ def is_modified(self):
+ return self.value_input.is_modified
+
+ @property
+ def is_overriden(self):
+ return self._is_overriden
+
+ @property
+ def has_studio_override(self):
+ return self.value_input._has_studio_override
+
+ @property
+ def child_has_studio_override(self):
+ return self.value_input.child_has_studio_override
+
+ @property
+ def child_modified(self):
+ return self.value_input.child_modified
+
+ @property
+ def child_overriden(self):
+ return self.value_input.child_overriden
+
+ @property
+ def child_invalid(self):
+ return self.value_input.child_invalid
+
+ def remove_overrides(self):
+ self.value_input.remove_overrides()
+
+ def reset_to_pype_default(self):
+ self.value_input.reset_to_pype_default()
+
+ def set_studio_default(self):
+ self.value_input.set_studio_default()
+
+ def discard_changes(self):
+ self.value_input.discard_changes()
+
+ def set_as_overriden(self):
+ self.value_input.set_as_overriden()
+
+ def overrides(self):
+ if not self.child_overriden:
+ return NOT_SET, False
+ return self.config_value(), True
+
+ def item_value(self):
+ return self.value_input.item_value()
+
+ def config_value(self):
+ return self.value_input.config_value()
+
+
+TypeToKlass.types["anatomy"] = AnatomyWidget
+TypeToKlass.types["anatomy_roots"] = AnatomyWidget
+TypeToKlass.types["anatomy_templates"] = AnatomyWidget
diff --git a/pype/tools/settings/settings/widgets/base.py b/pype/tools/settings/settings/widgets/base.py
new file mode 100644
index 0000000000..423380d54c
--- /dev/null
+++ b/pype/tools/settings/settings/widgets/base.py
@@ -0,0 +1,739 @@
+import os
+import json
+from Qt import QtWidgets, QtCore, QtGui
+from pype.settings.lib import (
+ SYSTEM_SETTINGS_KEY,
+ SYSTEM_SETTINGS_PATH,
+ PROJECT_SETTINGS_KEY,
+ PROJECT_SETTINGS_PATH,
+ PROJECT_ANATOMY_KEY,
+ PROJECT_ANATOMY_PATH,
+
+ DEFAULTS_DIR,
+
+ reset_default_settings,
+ default_settings,
+
+ studio_system_settings,
+ studio_project_settings,
+ studio_project_anatomy,
+
+ project_settings_overrides,
+ project_anatomy_overrides,
+
+ path_to_project_overrides,
+ path_to_project_anatomy
+)
+from .widgets import UnsavedChangesDialog
+from . import lib
+from avalon import io
+from avalon.vendor import qtawesome
+
+
+class SystemWidget(QtWidgets.QWidget):
+ is_overidable = False
+ has_studio_override = _has_studio_override = False
+ is_overriden = _is_overriden = False
+ as_widget = _as_widget = False
+ any_parent_as_widget = _any_parent_as_widget = False
+ is_group = _is_group = False
+ any_parent_is_group = _any_parent_is_group = False
+
+ def __init__(self, develop_mode, parent=None):
+ super(SystemWidget, self).__init__(parent)
+
+ self.develop_mode = develop_mode
+ self._hide_studio_overrides = False
+ self._ignore_value_changes = False
+
+ self.input_fields = []
+
+ scroll_widget = QtWidgets.QScrollArea(self)
+ scroll_widget.setObjectName("GroupWidget")
+ content_widget = QtWidgets.QWidget(scroll_widget)
+ content_layout = QtWidgets.QVBoxLayout(content_widget)
+ content_layout.setContentsMargins(3, 3, 3, 3)
+ content_layout.setSpacing(0)
+ content_layout.setAlignment(QtCore.Qt.AlignTop)
+ content_widget.setLayout(content_layout)
+
+ scroll_widget.setWidgetResizable(True)
+ scroll_widget.setWidget(content_widget)
+
+ self.scroll_widget = scroll_widget
+ self.content_layout = content_layout
+ self.content_widget = content_widget
+
+ footer_widget = QtWidgets.QWidget()
+ footer_layout = QtWidgets.QHBoxLayout(footer_widget)
+
+ if self.develop_mode:
+ save_as_default_btn = QtWidgets.QPushButton("Save as Default")
+ save_as_default_btn.clicked.connect(self._save_as_defaults)
+
+ refresh_icon = qtawesome.icon("fa.refresh", color="white")
+ refresh_button = QtWidgets.QPushButton()
+ refresh_button.setIcon(refresh_icon)
+ refresh_button.clicked.connect(self._on_refresh)
+
+ hide_studio_overrides = QtWidgets.QCheckBox()
+ hide_studio_overrides.setChecked(self._hide_studio_overrides)
+ hide_studio_overrides.stateChanged.connect(
+ self._on_hide_studio_overrides
+ )
+
+ hide_studio_overrides_widget = QtWidgets.QWidget()
+ hide_studio_overrides_layout = QtWidgets.QHBoxLayout(
+ hide_studio_overrides_widget
+ )
+ _label_widget = QtWidgets.QLabel(
+ "Hide studio overrides", hide_studio_overrides_widget
+ )
+ hide_studio_overrides_layout.addWidget(_label_widget)
+ hide_studio_overrides_layout.addWidget(hide_studio_overrides)
+
+ footer_layout.addWidget(save_as_default_btn, 0)
+ footer_layout.addWidget(refresh_button, 0)
+ footer_layout.addWidget(hide_studio_overrides_widget, 0)
+
+ save_btn = QtWidgets.QPushButton("Save")
+ spacer_widget = QtWidgets.QWidget()
+ footer_layout.addWidget(spacer_widget, 1)
+ footer_layout.addWidget(save_btn, 0)
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+ self.setLayout(layout)
+
+ layout.addWidget(scroll_widget, 1)
+ layout.addWidget(footer_widget, 0)
+
+ save_btn.clicked.connect(self._save)
+
+ self.reset()
+
+ def any_parent_overriden(self):
+ return False
+
+ @property
+ def ignore_value_changes(self):
+ return self._ignore_value_changes
+
+ @ignore_value_changes.setter
+ def ignore_value_changes(self, value):
+ self._ignore_value_changes = value
+ if value is False:
+ self.hierarchical_style_update()
+
+ def hierarchical_style_update(self):
+ for input_field in self.input_fields:
+ input_field.hierarchical_style_update()
+
+ 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.schema = lib.gui_schema("system_schema", "0_system_gui_schema")
+ self.keys = self.schema.get("keys", [])
+ self.add_children_gui(self.schema)
+ self._update_values()
+ self.hierarchical_style_update()
+
+ def _save(self):
+ has_invalid = False
+ for item in self.input_fields:
+ if item.child_invalid:
+ has_invalid = True
+
+ if has_invalid:
+ invalid_items = []
+ for item in self.input_fields:
+ invalid_items.extend(item.get_invalid())
+ msg_box = QtWidgets.QMessageBox(
+ QtWidgets.QMessageBox.Warning,
+ "Invalid input",
+ "There is invalid value in one of inputs."
+ " Please lead red color and fix them."
+ )
+ msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok)
+ msg_box.exec_()
+
+ first_invalid_item = invalid_items[0]
+ self.scroll_widget.ensureWidgetVisible(first_invalid_item)
+ if first_invalid_item.isVisible():
+ first_invalid_item.setFocus(True)
+ return
+
+ _data = {}
+ for input_field in self.input_fields:
+ value, is_group = input_field.studio_overrides()
+ if value is not lib.NOT_SET:
+ _data.update(value)
+
+ values = lib.convert_gui_data_to_overrides(_data.get("system", {}))
+
+ dirpath = os.path.dirname(SYSTEM_SETTINGS_PATH)
+ if not os.path.exists(dirpath):
+ os.makedirs(dirpath)
+
+ print("Saving data to:", SYSTEM_SETTINGS_PATH)
+ with open(SYSTEM_SETTINGS_PATH, "w") as file_stream:
+ json.dump(values, file_stream, indent=4)
+
+ self._update_values()
+
+ def _on_refresh(self):
+ self.reset()
+
+ def _on_hide_studio_overrides(self, state):
+ self._hide_studio_overrides = (state == QtCore.Qt.Checked)
+ self._update_values()
+ self.hierarchical_style_update()
+
+ def _save_as_defaults(self):
+ output = {}
+ for item in self.input_fields:
+ output.update(item.config_value())
+
+ for key in reversed(self.keys):
+ _output = {key: output}
+ output = _output
+
+ all_values = {}
+ for item in self.input_fields:
+ all_values.update(item.config_value())
+
+ for key in reversed(self.keys):
+ _all_values = {key: all_values}
+ all_values = _all_values
+
+ # Skip first key
+ all_values = all_values["system"]
+
+ prject_defaults_dir = os.path.join(
+ DEFAULTS_DIR, SYSTEM_SETTINGS_KEY
+ )
+ keys_to_file = lib.file_keys_from_schema(self.schema)
+ for key_sequence in keys_to_file:
+ # Skip first key
+ key_sequence = key_sequence[1:]
+ subpath = "/".join(key_sequence) + ".json"
+
+ new_values = all_values
+ for key in key_sequence:
+ new_values = new_values[key]
+
+ output_path = os.path.join(prject_defaults_dir, subpath)
+ dirpath = os.path.dirname(output_path)
+ if not os.path.exists(dirpath):
+ os.makedirs(dirpath)
+
+ print("Saving data to: ", subpath)
+ with open(output_path, "w") as file_stream:
+ json.dump(new_values, file_stream, indent=4)
+
+ reset_default_settings()
+
+ self._update_values()
+ self.hierarchical_style_update()
+
+ def _update_values(self):
+ self.ignore_value_changes = True
+
+ default_values = {
+ "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()}
+ for input_field in self.input_fields:
+ input_field.update_studio_values(system_values)
+
+ self.ignore_value_changes = False
+
+ def add_children_gui(self, child_configuration):
+ item_type = child_configuration["type"]
+ klass = lib.TypeToKlass.types.get(item_type)
+ item = klass(child_configuration, self)
+ self.input_fields.append(item)
+ self.content_layout.addWidget(item)
+
+
+class ProjectListView(QtWidgets.QListView):
+ left_mouse_released_at = QtCore.Signal(QtCore.QModelIndex)
+
+ def mouseReleaseEvent(self, event):
+ if event.button() == QtCore.Qt.LeftButton:
+ index = self.indexAt(event.pos())
+ self.left_mouse_released_at.emit(index)
+ super(ProjectListView, self).mouseReleaseEvent(event)
+
+
+class ProjectListWidget(QtWidgets.QWidget):
+ default = "< Default >"
+ project_changed = QtCore.Signal()
+
+ def __init__(self, parent):
+ self._parent = parent
+
+ self.current_project = None
+
+ super(ProjectListWidget, self).__init__(parent)
+ self.setObjectName("ProjectListWidget")
+
+ label_widget = QtWidgets.QLabel("Projects")
+ label_widget.setProperty("state", "studio")
+ project_list = ProjectListView(self)
+ project_list.setModel(QtGui.QStandardItemModel())
+
+ # Do not allow editing
+ project_list.setEditTriggers(
+ QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers
+ )
+ # Do not automatically handle selection
+ project_list.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.setSpacing(3)
+ layout.addWidget(label_widget, 0)
+ layout.addWidget(project_list, 1)
+
+ project_list.left_mouse_released_at.connect(self.on_item_clicked)
+
+ self.project_list = project_list
+
+ self.refresh()
+
+ def on_item_clicked(self, new_index):
+ new_project_name = new_index.data(QtCore.Qt.DisplayRole)
+ if new_project_name is None:
+ return
+
+ if self.current_project == new_project_name:
+ return
+
+ save_changes = False
+ change_project = False
+ if self.validate_context_change():
+ change_project = True
+
+ else:
+ dialog = UnsavedChangesDialog(self)
+ result = dialog.exec_()
+ if result == 1:
+ save_changes = True
+ change_project = True
+
+ elif result == 2:
+ change_project = True
+
+ if save_changes:
+ self._parent._save()
+
+ if change_project:
+ self.select_project(new_project_name)
+ self.current_project = new_project_name
+ self.project_changed.emit()
+ else:
+ self.select_project(self.current_project)
+
+ def validate_context_change(self):
+ # TODO add check if project can be changed (is modified)
+ for item in self._parent.input_fields:
+ is_modified = item.child_modified
+ if is_modified:
+ return False
+ return True
+
+ def project_name(self):
+ if self.current_project == self.default:
+ return None
+ return self.current_project
+
+ def select_project(self, project_name):
+ model = self.project_list.model()
+ found_items = model.findItems(project_name)
+ if not found_items:
+ found_items = model.findItems(self.default)
+
+ index = model.indexFromItem(found_items[0])
+ self.project_list.selectionModel().clear()
+ self.project_list.selectionModel().setCurrentIndex(
+ index, QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent
+ )
+
+ def refresh(self):
+ selected_project = None
+ for index in self.project_list.selectedIndexes():
+ selected_project = index.data(QtCore.Qt.DisplayRole)
+ break
+
+ model = self.project_list.model()
+ model.clear()
+ items = [self.default]
+ io.install()
+ for project_doc in tuple(io.projects()):
+ items.append(project_doc["name"])
+
+ for item in items:
+ model.appendRow(QtGui.QStandardItem(item))
+
+ self.select_project(selected_project)
+
+ self.current_project = self.project_list.currentIndex().data(
+ QtCore.Qt.DisplayRole
+ )
+
+
+class ProjectWidget(QtWidgets.QWidget):
+ has_studio_override = _has_studio_override = False
+ is_overriden = _is_overriden = False
+ as_widget = _as_widget = False
+ any_parent_as_widget = _any_parent_as_widget = False
+ is_group = _is_group = False
+ any_parent_is_group = _any_parent_is_group = False
+
+ def __init__(self, develop_mode, parent=None):
+ super(ProjectWidget, self).__init__(parent)
+
+ self.develop_mode = develop_mode
+ self._hide_studio_overrides = False
+
+ self.is_overidable = False
+ self._ignore_value_changes = False
+ self.project_name = None
+
+ self.input_fields = []
+
+ scroll_widget = QtWidgets.QScrollArea(self)
+ scroll_widget.setObjectName("GroupWidget")
+ content_widget = QtWidgets.QWidget(scroll_widget)
+ content_layout = QtWidgets.QVBoxLayout(content_widget)
+ content_layout.setContentsMargins(3, 3, 3, 3)
+ content_layout.setSpacing(0)
+ content_layout.setAlignment(QtCore.Qt.AlignTop)
+ content_widget.setLayout(content_layout)
+
+ scroll_widget.setWidgetResizable(True)
+ scroll_widget.setWidget(content_widget)
+
+ project_list_widget = ProjectListWidget(self)
+ content_layout.addWidget(project_list_widget)
+
+ footer_widget = QtWidgets.QWidget()
+ footer_layout = QtWidgets.QHBoxLayout(footer_widget)
+
+ if self.develop_mode:
+ save_as_default_btn = QtWidgets.QPushButton("Save as Default")
+ save_as_default_btn.clicked.connect(self._save_as_defaults)
+
+ refresh_icon = qtawesome.icon("fa.refresh", color="white")
+ refresh_button = QtWidgets.QPushButton()
+ refresh_button.setIcon(refresh_icon)
+ refresh_button.clicked.connect(self._on_refresh)
+
+ hide_studio_overrides = QtWidgets.QCheckBox()
+ hide_studio_overrides.setChecked(self._hide_studio_overrides)
+ hide_studio_overrides.stateChanged.connect(
+ self._on_hide_studio_overrides
+ )
+
+ hide_studio_overrides_widget = QtWidgets.QWidget()
+ hide_studio_overrides_layout = QtWidgets.QHBoxLayout(
+ hide_studio_overrides_widget
+ )
+ _label_widget = QtWidgets.QLabel(
+ "Hide studio overrides", hide_studio_overrides_widget
+ )
+ hide_studio_overrides_layout.addWidget(_label_widget)
+ hide_studio_overrides_layout.addWidget(hide_studio_overrides)
+
+ footer_layout.addWidget(save_as_default_btn, 0)
+ footer_layout.addWidget(refresh_button, 0)
+ footer_layout.addWidget(hide_studio_overrides_widget, 0)
+
+ save_btn = QtWidgets.QPushButton("Save")
+ spacer_widget = QtWidgets.QWidget()
+ footer_layout.addWidget(spacer_widget, 1)
+ footer_layout.addWidget(save_btn, 0)
+
+ configurations_widget = QtWidgets.QWidget()
+ configurations_layout = QtWidgets.QVBoxLayout(configurations_widget)
+ configurations_layout.setContentsMargins(0, 0, 0, 0)
+ configurations_layout.setSpacing(0)
+
+ configurations_layout.addWidget(scroll_widget, 1)
+ configurations_layout.addWidget(footer_widget, 0)
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+ self.setLayout(layout)
+
+ layout.addWidget(project_list_widget, 0)
+ layout.addWidget(configurations_widget, 1)
+
+ save_btn.clicked.connect(self._save)
+ project_list_widget.project_changed.connect(self._on_project_change)
+
+ self.project_list_widget = project_list_widget
+ self.scroll_widget = scroll_widget
+ self.content_layout = content_layout
+ self.content_widget = content_widget
+
+ self.reset()
+
+ def any_parent_overriden(self):
+ return False
+
+ @property
+ def ignore_value_changes(self):
+ return self._ignore_value_changes
+
+ @ignore_value_changes.setter
+ def ignore_value_changes(self, value):
+ self._ignore_value_changes = value
+ if value is False:
+ self.hierarchical_style_update()
+
+ def hierarchical_style_update(self):
+ for input_field in self.input_fields:
+ input_field.hierarchical_style_update()
+
+ def reset(self):
+ if self.content_layout.count() != 0:
+ for widget in self.input_fields:
+ self.content_layout.removeWidget(widget)
+ widget.deleteLater()
+ self.input_fields.clear()
+
+ self.schema = lib.gui_schema("projects_schema", "0_project_gui_schema")
+ self.keys = self.schema.get("keys", [])
+ self.add_children_gui(self.schema)
+ self._update_values()
+ self.hierarchical_style_update()
+
+ def add_children_gui(self, child_configuration):
+ item_type = child_configuration["type"]
+ klass = lib.TypeToKlass.types.get(item_type)
+ item = klass(child_configuration, self)
+ self.input_fields.append(item)
+ self.content_layout.addWidget(item)
+
+ def _on_project_change(self):
+ project_name = self.project_list_widget.project_name()
+ if project_name is None:
+ _project_overrides = lib.NOT_SET
+ _project_anatomy = lib.NOT_SET
+ self.is_overidable = False
+ else:
+ _project_overrides = project_settings_overrides(project_name)
+ _project_anatomy = project_anatomy_overrides(project_name)
+ self.is_overidable = True
+
+ overrides = {"project": {
+ PROJECT_SETTINGS_KEY: lib.convert_overrides_to_gui_data(
+ _project_overrides
+ ),
+ PROJECT_ANATOMY_KEY: lib.convert_overrides_to_gui_data(
+ _project_anatomy
+ )
+ }}
+ self.project_name = project_name
+ self.ignore_value_changes = True
+ for item in self.input_fields:
+ item.apply_overrides(overrides)
+ self.ignore_value_changes = False
+
+ def _save_as_defaults(self):
+ output = {}
+ for item in self.input_fields:
+ output.update(item.config_value())
+
+ for key in reversed(self.keys):
+ _output = {key: output}
+ output = _output
+
+ all_values = {}
+ for item in self.input_fields:
+ all_values.update(item.config_value())
+
+ for key in reversed(self.keys):
+ _all_values = {key: all_values}
+ all_values = _all_values
+
+ # Skip first key
+ all_values = all_values["project"]
+
+ keys_to_file = lib.file_keys_from_schema(self.schema)
+ for key_sequence in keys_to_file:
+ # Skip first key
+ key_sequence = key_sequence[1:]
+ subpath = "/".join(key_sequence) + ".json"
+
+ new_values = all_values
+ for key in key_sequence:
+ new_values = new_values[key]
+
+ output_path = os.path.join(DEFAULTS_DIR, subpath)
+ dirpath = os.path.dirname(output_path)
+ if not os.path.exists(dirpath):
+ os.makedirs(dirpath)
+
+ print("Saving data to: ", subpath)
+ with open(output_path, "w") as file_stream:
+ json.dump(new_values, file_stream, indent=4)
+
+ reset_default_settings()
+
+ self._update_values()
+ self.hierarchical_style_update()
+
+ def _save(self):
+ has_invalid = False
+ for item in self.input_fields:
+ if item.child_invalid:
+ has_invalid = True
+
+ if has_invalid:
+ invalid_items = []
+ for item in self.input_fields:
+ invalid_items.extend(item.get_invalid())
+ msg_box = QtWidgets.QMessageBox(
+ QtWidgets.QMessageBox.Warning,
+ "Invalid input",
+ "There is invalid value in one of inputs."
+ " Please lead red color and fix them."
+ )
+ msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok)
+ msg_box.exec_()
+
+ first_invalid_item = invalid_items[0]
+ self.scroll_widget.ensureWidgetVisible(first_invalid_item)
+ if first_invalid_item.isVisible():
+ first_invalid_item.setFocus(True)
+ return
+
+ if self.project_name is None:
+ self._save_studio_overrides()
+ else:
+ self._save_overrides()
+
+ def _on_refresh(self):
+ self.reset()
+
+ def _on_hide_studio_overrides(self, state):
+ self._hide_studio_overrides = (state == QtCore.Qt.Checked)
+ self._update_values()
+ self.hierarchical_style_update()
+
+ def _save_overrides(self):
+ data = {}
+ for item in self.input_fields:
+ value, is_group = item.overrides()
+ if value is not lib.NOT_SET:
+ data.update(value)
+
+ output_data = lib.convert_gui_data_to_overrides(
+ data.get("project") or {}
+ )
+
+ # Saving overrides data
+ project_overrides_data = output_data.get(
+ PROJECT_SETTINGS_KEY, {}
+ )
+ project_overrides_json_path = path_to_project_overrides(
+ self.project_name
+ )
+ dirpath = os.path.dirname(project_overrides_json_path)
+ if not os.path.exists(dirpath):
+ os.makedirs(dirpath)
+
+ print("Saving data to:", project_overrides_json_path)
+ with open(project_overrides_json_path, "w") as file_stream:
+ json.dump(project_overrides_data, file_stream, indent=4)
+
+ # Saving anatomy data
+ project_anatomy_data = output_data.get(
+ PROJECT_ANATOMY_KEY, {}
+ )
+ project_anatomy_json_path = path_to_project_anatomy(
+ self.project_name
+ )
+ dirpath = os.path.dirname(project_anatomy_json_path)
+ if not os.path.exists(dirpath):
+ os.makedirs(dirpath)
+
+ print("Saving data to:", project_anatomy_json_path)
+ with open(project_anatomy_json_path, "w") as file_stream:
+ json.dump(project_anatomy_data, file_stream, indent=4)
+
+ # Refill values with overrides
+ self._on_project_change()
+
+ def _save_studio_overrides(self):
+ data = {}
+ for input_field in self.input_fields:
+ value, is_group = input_field.studio_overrides()
+ if value is not lib.NOT_SET:
+ data.update(value)
+
+ output_data = lib.convert_gui_data_to_overrides(
+ data.get("project", {})
+ )
+
+ # Project overrides data
+ project_overrides_data = output_data.get(
+ PROJECT_SETTINGS_KEY, {}
+ )
+ dirpath = os.path.dirname(PROJECT_SETTINGS_PATH)
+ if not os.path.exists(dirpath):
+ os.makedirs(dirpath)
+
+ print("Saving data to:", PROJECT_SETTINGS_PATH)
+ with open(PROJECT_SETTINGS_PATH, "w") as file_stream:
+ json.dump(project_overrides_data, file_stream, indent=4)
+
+ # Project Anatomy data
+ project_anatomy_data = output_data.get(
+ PROJECT_ANATOMY_KEY, {}
+ )
+ dirpath = os.path.dirname(PROJECT_ANATOMY_PATH)
+ if not os.path.exists(dirpath):
+ os.makedirs(dirpath)
+
+ print("Saving data to:", PROJECT_ANATOMY_PATH)
+ with open(PROJECT_ANATOMY_PATH, "w") as file_stream:
+ json.dump(project_anatomy_data, file_stream, indent=4)
+
+ # Update saved values
+ self._update_values()
+
+ def _update_values(self):
+ self.ignore_value_changes = True
+
+ default_values = {"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": {
+ 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)
+
+ self.ignore_value_changes = False
diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py
new file mode 100644
index 0000000000..e589b89c0f
--- /dev/null
+++ b/pype/tools/settings/settings/widgets/item_types.py
@@ -0,0 +1,3506 @@
+import json
+import logging
+import collections
+from Qt import QtWidgets, QtCore, QtGui
+from .widgets import (
+ ExpandingWidget,
+ NumberSpinBox,
+ PathInput,
+ GridLabelWidget
+)
+from .lib import NOT_SET, METADATA_KEY, TypeToKlass, CHILD_OFFSET
+from avalon.vendor import qtawesome
+
+
+class SettingObject:
+ """Partially abstract class for Setting's item type workflow."""
+ # `is_input_type` attribute says if has implemented item type methods
+ is_input_type = True
+ # Each input must have implemented default value for development
+ # when defaults are not filled yet.
+ default_input_value = NOT_SET
+ # 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
+ # All item types must have implemented Qt signal which is emitted when
+ # it's or it's children value has changed,
+ value_changed = None
+ # Item will expand to full width in grid layout
+ expand_in_grid = False
+
+ def _set_default_attributes(self):
+ """Create and reset attributes required for all item types.
+
+ They may not be used in the item but are required to be set.
+ """
+ # Default input attributes
+ self._has_studio_override = False
+ self._had_studio_override = False
+
+ self._is_overriden = False
+ self._was_overriden = False
+
+ self._is_modified = False
+ self._is_invalid = False
+
+ self._is_nullable = False
+ self._as_widget = False
+ self._is_group = False
+
+ self._any_parent_as_widget = None
+ self._any_parent_is_group = None
+
+ # Parent input
+ self._parent = None
+
+ # States of inputs
+ self._state = None
+ self._child_state = None
+
+ # Attributes where values are stored
+ self.default_value = NOT_SET
+ self.studio_value = NOT_SET
+ self.override_value = NOT_SET
+
+ # Log object
+ self._log = None
+
+ # Only for develop mode
+ self.defaults_not_set = False
+
+ def initial_attributes(self, input_data, parent, as_widget):
+ """Prepare attributes based on entered arguments.
+
+ This method should be same for each item type. Few item types
+ may require to extend with specific attributes for their case.
+ """
+ self._set_default_attributes()
+
+ self._parent = parent
+ self._as_widget = as_widget
+
+ self._is_group = input_data.get("is_group", False)
+ # TODO not implemented yet
+ self._is_nullable = input_data.get("is_nullable", False)
+
+ any_parent_as_widget = parent.as_widget
+ if not any_parent_as_widget:
+ any_parent_as_widget = parent.any_parent_as_widget
+
+ self._any_parent_as_widget = any_parent_as_widget
+
+ any_parent_is_group = parent.is_group
+ if not any_parent_is_group:
+ any_parent_is_group = parent.any_parent_is_group
+
+ self._any_parent_is_group = any_parent_is_group
+
+ @property
+ def develop_mode(self):
+ """Tool is in develop mode or not.
+
+ Returns:
+ bool
+
+ """
+ return self._parent.develop_mode
+
+ @property
+ def log(self):
+ """Auto created logger for debugging."""
+ if self._log is None:
+ self._log = logging.getLogger(self.__class__.__name__)
+ return self._log
+
+ @property
+ def had_studio_override(self):
+ """Item had studio overrides on refresh.
+
+ Use attribute `_had_studio_override` which should be changed only
+ during methods `update_studio_values` and `update_default_values`.
+
+ Returns:
+ bool
+
+ """
+ return self._had_studio_override
+
+ @property
+ def has_studio_override(self):
+ """Item has studio override at the moment.
+
+ With combination of `had_studio_override` is possible to know if item
+ is modified (not value change).
+
+ Returns:
+ bool
+
+ """
+ return self._has_studio_override or self._parent.has_studio_override
+
+ @property
+ def as_widget(self):
+ """Item is used as widget in parent item.
+
+ Returns:
+ bool
+
+ """
+ return self._as_widget
+
+ @property
+ def any_parent_as_widget(self):
+ """Any parent of item is used as widget.
+
+ Attribute holding this information is set during creation and
+ stored to `_any_parent_as_widget`.
+
+ Why is this information useful: If any parent is used as widget then
+ modifications and override are not important for whole part.
+
+ Returns:
+ bool
+
+ """
+ if self._any_parent_as_widget is None:
+ return super(SettingObject, self).any_parent_as_widget
+ return self._any_parent_as_widget
+
+ @property
+ def is_group(self):
+ """Item represents key that can be overriden.
+
+ Attribute `is_group` can be set to True only once in item hierarchy.
+
+ Returns:
+ bool
+
+ """
+ return self._is_group
+
+ @property
+ def any_parent_is_group(self):
+ """Any parent of item is group.
+
+ Attribute holding this information is set during creation and
+ stored to `_any_parent_is_group`.
+
+ Why is this information useful: If any parent is group and
+ the parent is set as overriden, this item is overriden too.
+
+ Returns:
+ bool
+
+ """
+ if self._any_parent_is_group is None:
+ return super(SettingObject, self).any_parent_is_group
+ return self._any_parent_is_group
+
+ @property
+ def is_modified(self):
+ """Has object any changes that require saving."""
+ if self.any_parent_as_widget:
+ return self._is_modified
+
+ if self._is_modified or self.defaults_not_set:
+ return True
+
+ if self.is_overidable:
+ return self.was_overriden != self.is_overriden
+ else:
+ return self.has_studio_override != self.had_studio_override
+
+ @property
+ def is_overriden(self):
+ """Is object overriden so should be saved to overrides."""
+ return self._is_overriden or self._parent.is_overriden
+
+ @property
+ def was_overriden(self):
+ """Item had set value of project overrides on project change."""
+ if self._as_widget:
+ return self._parent.was_overriden
+ return self._was_overriden
+
+ @property
+ def is_invalid(self):
+ """Value set in is not valid."""
+ return self._is_invalid
+
+ @property
+ def is_nullable(self):
+ """Value of item can be set to None.
+
+ NOT IMPLEMENTED!
+ """
+ return self._is_nullable
+
+ @property
+ def is_overidable(self):
+ """ care about overrides."""
+
+ return self._parent.is_overidable
+
+ def any_parent_overriden(self):
+ """Any of parent objects up to top hiearchy item is overriden.
+
+ Returns:
+ bool
+
+ """
+
+ if self._parent._is_overriden:
+ return True
+ return self._parent.any_parent_overriden()
+
+ @property
+ def ignore_value_changes(self):
+ """Most of attribute changes are ignored on value change when True."""
+ return self._parent.ignore_value_changes
+
+ @ignore_value_changes.setter
+ def ignore_value_changes(self, value):
+ """Setter for global parent item to apply changes for all inputs."""
+ self._parent.ignore_value_changes = value
+
+ def config_value(self):
+ """Output for saving changes or overrides."""
+ return {self.key: self.item_value()}
+
+ @classmethod
+ def style_state(
+ cls, has_studio_override, is_invalid, is_overriden, is_modified
+ ):
+ """Return stylesheet state by intered booleans."""
+ items = []
+ if is_invalid:
+ items.append("invalid")
+ else:
+ if is_overriden:
+ items.append("overriden")
+ if is_modified:
+ items.append("modified")
+
+ if not items and has_studio_override:
+ items.append("studio")
+
+ return "-".join(items) or ""
+
+ def show_actions_menu(self, event=None):
+ if event and event.button() != QtCore.Qt.RightButton:
+ return
+
+ if not self.allow_actions:
+ if event:
+ return self.mouseReleaseEvent(event)
+ return
+
+ menu = QtWidgets.QMenu()
+
+ actions_mapping = {}
+ if self.child_modified:
+ action = QtWidgets.QAction("Discard changes")
+ actions_mapping[action] = self._discard_changes
+ menu.addAction(action)
+
+ if (
+ self.is_overidable
+ and not self.is_overriden
+ and not self.any_parent_is_group
+ ):
+ action = QtWidgets.QAction("Set project override")
+ actions_mapping[action] = self._set_as_overriden
+ menu.addAction(action)
+
+ if (
+ not self.is_overidable
+ and (
+ self.has_studio_override
+ )
+ ):
+ action = QtWidgets.QAction("Reset to pype default")
+ actions_mapping[action] = self._reset_to_pype_default
+ menu.addAction(action)
+
+ if (
+ not self.is_overidable
+ and not self.is_overriden
+ and not self.any_parent_is_group
+ and not self._had_studio_override
+ ):
+ action = QtWidgets.QAction("Set studio default")
+ actions_mapping[action] = self._set_studio_default
+ menu.addAction(action)
+
+ if (
+ not self.any_parent_overriden()
+ and (self.is_overriden or self.child_overriden)
+ ):
+ # TODO better label
+ action = QtWidgets.QAction("Remove project override")
+ actions_mapping[action] = self._remove_overrides
+ menu.addAction(action)
+
+ if not actions_mapping:
+ action = QtWidgets.QAction("< No action >")
+ actions_mapping[action] = None
+ menu.addAction(action)
+
+ result = menu.exec_(QtGui.QCursor.pos())
+ if result:
+ to_run = actions_mapping[result]
+ if to_run:
+ to_run()
+
+ def mouseReleaseEvent(self, event):
+ if self.allow_actions and event.button() == QtCore.Qt.RightButton:
+ return self.show_actions_menu()
+
+ mro = type(self).mro()
+ index = mro.index(self.__class__)
+ item = None
+ for idx in range(index + 1, len(mro)):
+ _item = mro[idx]
+ if hasattr(_item, "mouseReleaseEvent"):
+ item = _item
+ break
+
+ if item:
+ return item.mouseReleaseEvent(self, event)
+
+ def _discard_changes(self):
+ self.ignore_value_changes = True
+ self.discard_changes()
+ self.ignore_value_changes = False
+
+ def discard_changes(self):
+ """Item's implementation to discard all changes made by user.
+
+ Reset all values to same values as had when opened GUI
+ or when changed project.
+
+ Must not affect `had_studio_override` value or `was_overriden`
+ value. It must be marked that there are keys/values which are not in
+ defaults or overrides.
+ """
+ raise NotImplementedError(
+ "{} Method `discard_changes` not implemented!".format(
+ repr(self)
+ )
+ )
+
+ def _set_studio_default(self):
+ self.ignore_value_changes = True
+ self.set_studio_default()
+ self.ignore_value_changes = False
+
+ def set_studio_default(self):
+ """Item's implementation to set current values as studio's overrides.
+
+ Mark item and it's children as they have studio overrides.
+ """
+ raise NotImplementedError(
+ "{} Method `set_studio_default` not implemented!".format(
+ repr(self)
+ )
+ )
+
+ def _reset_to_pype_default(self):
+ self.ignore_value_changes = True
+ self.reset_to_pype_default()
+ self.ignore_value_changes = False
+
+ def reset_to_pype_default(self):
+ """Item's implementation to remove studio overrides.
+
+ Mark item as it does not have studio overrides unset studio
+ override values.
+ """
+ raise NotImplementedError(
+ "{} Method `reset_to_pype_default` not implemented!".format(
+ repr(self)
+ )
+ )
+
+ def _remove_overrides(self):
+ self.ignore_value_changes = True
+ self.remove_overrides()
+ self.ignore_value_changes = False
+
+ def remove_overrides(self):
+ """Item's implementation to remove project overrides.
+
+ Mark item as does not have project overrides. Must not change
+ `was_overriden` attribute value.
+ """
+ raise NotImplementedError(
+ "{} Method `remove_overrides` not implemented!".format(
+ repr(self)
+ )
+ )
+
+ def _set_as_overriden(self):
+ self.ignore_value_changes = True
+ self.set_as_overriden()
+ self.ignore_value_changes = False
+
+ def set_as_overriden(self):
+ """Item's implementation to set values as overriden for project.
+
+ Mark item and all it's children as they're overriden. Must skip
+ items with children items that has attributes `is_group`
+ and `any_parent_is_group` set to False. In that case those items
+ are not meant to be overridable and should trigger the method on it's
+ children.
+
+ """
+ raise NotImplementedError(
+ "{} Method `set_as_overriden` not implemented!".format(repr(self))
+ )
+
+ def hierarchical_style_update(self):
+ """Trigger update style method down the hierarchy."""
+ raise NotImplementedError(
+ "{} Method `hierarchical_style_update` not implemented!".format(
+ repr(self)
+ )
+ )
+
+ def update_default_values(self, parent_values):
+ """Fill default values on startup or on refresh.
+
+ Default values stored in `pype` repository should update all items in
+ schema. Each item should take values for his key and set it's value or
+ pass values down to children items.
+
+ Args:
+ parent_values (dict): Values of parent's item. But in case item is
+ used as widget, `parent_values` contain value for item.
+ """
+ raise NotImplementedError(
+ "{} does not have implemented `update_default_values`".format(self)
+ )
+
+ def update_studio_values(self, parent_values):
+ """Fill studio override values on startup or on refresh.
+
+ Set studio value if is not set to NOT_SET, in that case studio
+ overrides are not set yet.
+
+ Args:
+ parent_values (dict): Values of parent's item. But in case item is
+ used as widget, `parent_values` contain value for item.
+ """
+ raise NotImplementedError(
+ "{} does not have implemented `update_studio_values`".format(self)
+ )
+
+ def apply_overrides(self, parent_values):
+ """Fill project override values on startup, refresh or project change.
+
+ Set project value if is not set to NOT_SET, in that case project
+ overrides are not set yet.
+
+ Args:
+ parent_values (dict): Values of parent's item. But in case item is
+ used as widget, `parent_values` contain value for item.
+ """
+ raise NotImplementedError(
+ "{} does not have implemented `apply_overrides`".format(self)
+ )
+
+ @property
+ def child_has_studio_override(self):
+ """Any children item has studio overrides."""
+ raise NotImplementedError(
+ "{} does not have implemented `child_has_studio_override`".format(
+ self
+ )
+ )
+
+ @property
+ def child_modified(self):
+ """Any children item is modified."""
+ raise NotImplementedError(
+ "{} does not have implemented `child_modified`".format(self)
+ )
+
+ @property
+ def child_overriden(self):
+ """Any children item has project overrides."""
+ raise NotImplementedError(
+ "{} does not have implemented `child_overriden`".format(self)
+ )
+
+ @property
+ def child_invalid(self):
+ """Any children item does not have valid value."""
+ raise NotImplementedError(
+ "{} does not have implemented `child_invalid`".format(self)
+ )
+
+ def get_invalid(self):
+ """Return invalid item types all down the hierarchy."""
+ raise NotImplementedError(
+ "{} does not have implemented `get_invalid`".format(self)
+ )
+
+ def item_value(self):
+ """Value of an item without key."""
+ raise NotImplementedError(
+ "Method `item_value` not implemented!"
+ )
+
+
+class InputObject(SettingObject):
+ """Class for inputs with pre-implemented methods.
+
+ Class is for item types not creating or using other item types, most
+ of methods has same code in that case.
+ """
+
+ def update_default_values(self, parent_values):
+ self._state = None
+ self._is_modified = False
+
+ value = NOT_SET
+ if self._as_widget:
+ value = parent_values
+ elif parent_values is not NOT_SET:
+ value = parent_values.get(self.key, NOT_SET)
+
+ if value is NOT_SET:
+ if self.develop_mode:
+ self.defaults_not_set = True
+ value = self.default_input_value
+ if value is NOT_SET:
+ raise NotImplementedError((
+ "{} Does not have implemented"
+ " attribute `default_input_value`"
+ ).format(self))
+
+ else:
+ raise ValueError(
+ "Default value is not set. This is implementation BUG."
+ )
+ else:
+ self.defaults_not_set = False
+
+ self.default_value = value
+ self._has_studio_override = False
+ self._had_studio_override = False
+ self.set_value(value)
+
+ def update_studio_values(self, parent_values):
+ self._state = None
+ self._is_modified = False
+
+ value = NOT_SET
+ if self._as_widget:
+ value = parent_values
+ elif parent_values is not NOT_SET:
+ value = parent_values.get(self.key, NOT_SET)
+
+ self.studio_value = value
+ if value is not NOT_SET:
+ self._has_studio_override = True
+ self._had_studio_override = True
+ self.set_value(value)
+
+ else:
+ self._has_studio_override = False
+ self._had_studio_override = False
+ self.set_value(self.default_value)
+
+ def apply_overrides(self, parent_values):
+ self._is_modified = False
+ self._state = None
+ self._had_studio_override = bool(self._has_studio_override)
+ if self._as_widget:
+ override_value = parent_values
+ elif parent_values is NOT_SET or self.key not in parent_values:
+ override_value = NOT_SET
+ else:
+ override_value = parent_values[self.key]
+
+ self.override_value = override_value
+
+ if override_value is NOT_SET:
+ self._is_overriden = False
+ self._was_overriden = False
+ if self.has_studio_override:
+ value = self.studio_value
+ else:
+ value = self.default_value
+ else:
+ self._is_overriden = True
+ self._was_overriden = True
+ value = override_value
+
+ self.set_value(value)
+
+ def _on_value_change(self, item=None):
+ if self.ignore_value_changes:
+ return
+
+ if not self.any_parent_as_widget:
+ if self.is_overidable:
+ self._is_overriden = True
+ else:
+ self._has_studio_override = True
+
+ if self._is_invalid:
+ self._is_modified = True
+ elif self._is_overriden:
+ self._is_modified = self.item_value() != self.override_value
+ elif self._has_studio_override:
+ self._is_modified = self.item_value() != self.studio_value
+ else:
+ self._is_modified = self.item_value() != self.default_value
+
+ self.update_style()
+
+ self.value_changed.emit(self)
+
+ def studio_overrides(self):
+ if (
+ not (self.as_widget or self.any_parent_as_widget)
+ and not self.has_studio_override
+ ):
+ return NOT_SET, False
+ return self.config_value(), self.is_group
+
+ def overrides(self):
+ if (
+ not (self.as_widget or self.any_parent_as_widget)
+ and not self.is_overriden
+ ):
+ return NOT_SET, False
+ return self.config_value(), self.is_group
+
+ def hierarchical_style_update(self):
+ self.update_style()
+
+ def remove_overrides(self):
+ self._is_overriden = False
+ self._is_modified = False
+ if self.has_studio_override:
+ self.set_value(self.studio_value)
+ else:
+ self.set_value(self.default_value)
+ self._is_overriden = False
+ self._is_modified = False
+
+ def reset_to_pype_default(self):
+ self.set_value(self.default_value)
+ self._has_studio_override = False
+
+ def set_studio_default(self):
+ self._has_studio_override = True
+
+ def discard_changes(self):
+ self._is_overriden = self._was_overriden
+ self._has_studio_override = self._had_studio_override
+ if self.is_overidable:
+ if self._was_overriden and self.override_value is not NOT_SET:
+ self.set_value(self.override_value)
+ else:
+ if self._had_studio_override:
+ self.set_value(self.studio_value)
+ else:
+ self.set_value(self.default_value)
+
+ if not self.is_overidable:
+ if self.has_studio_override:
+ self._is_modified = self.studio_value != self.item_value()
+ else:
+ self._is_modified = self.default_value != self.item_value()
+ self._is_overriden = False
+ return
+
+ self._is_modified = False
+ self._is_overriden = self._was_overriden
+
+ def set_as_overriden(self):
+ self._is_overriden = True
+
+ @property
+ def child_has_studio_override(self):
+ return self._has_studio_override
+
+ @property
+ def child_modified(self):
+ return self.is_modified
+
+ @property
+ def child_overriden(self):
+ return self._is_overriden
+
+ @property
+ def child_invalid(self):
+ return self.is_invalid
+
+ def get_invalid(self):
+ output = []
+ if self.is_invalid:
+ output.append(self)
+ return output
+
+ def reset_children_attributes(self):
+ return
+
+
+class BooleanWidget(QtWidgets.QWidget, InputObject):
+ default_input_value = True
+ value_changed = QtCore.Signal(object)
+
+ def __init__(
+ self, input_data, parent,
+ as_widget=False, label_widget=None, parent_widget=None
+ ):
+ if parent_widget is None:
+ parent_widget = parent
+ super(BooleanWidget, self).__init__(parent_widget)
+
+ self.initial_attributes(input_data, parent, as_widget)
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(5)
+
+ if not self._as_widget:
+ self.key = input_data["key"]
+ if not label_widget:
+ label = input_data["label"]
+ label_widget = QtWidgets.QLabel(label)
+ label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+ layout.addWidget(label_widget, 0)
+ self.label_widget = label_widget
+
+ self.checkbox = QtWidgets.QCheckBox(self)
+ spacer = QtWidgets.QWidget(self)
+ layout.addWidget(self.checkbox, 0)
+ layout.addWidget(spacer, 1)
+
+ spacer.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+
+ self.setFocusProxy(self.checkbox)
+
+ self.checkbox.stateChanged.connect(self._on_value_change)
+
+ def set_value(self, value):
+ # Ignore value change because if `self.isChecked()` has same
+ # value as `value` the `_on_value_change` is not triggered
+ self.checkbox.setChecked(value)
+
+ def update_style(self):
+ if self._as_widget:
+ if not self.isEnabled():
+ state = self.style_state(False, False, False, False)
+ else:
+ state = self.style_state(
+ False,
+ self._is_invalid,
+ False,
+ self._is_modified
+ )
+ else:
+ state = self.style_state(
+ self.has_studio_override,
+ self.is_invalid,
+ self.is_overriden,
+ self.is_modified
+ )
+ if self._state == state:
+ return
+
+ if self._as_widget:
+ property_name = "input-state"
+ else:
+ property_name = "state"
+
+ self.label_widget.setProperty(property_name, state)
+ self.label_widget.style().polish(self.label_widget)
+ self._state = state
+
+ def item_value(self):
+ return self.checkbox.isChecked()
+
+
+class NumberWidget(QtWidgets.QWidget, InputObject):
+ default_input_value = 0
+ value_changed = QtCore.Signal(object)
+ input_modifiers = ("minimum", "maximum", "decimal")
+
+ def __init__(
+ self, input_data, parent,
+ as_widget=False, label_widget=None, parent_widget=None
+ ):
+ if parent_widget is None:
+ parent_widget = parent
+ super(NumberWidget, self).__init__(parent_widget)
+
+ self.initial_attributes(input_data, parent, as_widget)
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(5)
+
+ kwargs = {
+ modifier: input_data.get(modifier)
+ for modifier in self.input_modifiers
+ if input_data.get(modifier)
+ }
+ self.input_field = NumberSpinBox(self, **kwargs)
+
+ self.setFocusProxy(self.input_field)
+
+ if not self._as_widget:
+ self.key = input_data["key"]
+ if not label_widget:
+ label = input_data["label"]
+ label_widget = QtWidgets.QLabel(label)
+ layout.addWidget(label_widget, 0)
+ self.label_widget = label_widget
+
+ layout.addWidget(self.input_field, 1)
+
+ self.input_field.valueChanged.connect(self._on_value_change)
+
+ def set_value(self, value):
+ self.input_field.setValue(value)
+
+ def update_style(self):
+ if self._as_widget:
+ if not self.isEnabled():
+ state = self.style_state(False, False, False, False)
+ else:
+ state = self.style_state(
+ False,
+ self._is_invalid,
+ False,
+ self._is_modified
+ )
+ else:
+ state = self.style_state(
+ self.has_studio_override,
+ self.is_invalid,
+ self.is_overriden,
+ self.is_modified
+ )
+ if self._state == state:
+ return
+
+ if self._as_widget:
+ property_name = "input-state"
+ widget = self.input_field
+ else:
+ property_name = "state"
+ widget = self.label_widget
+
+ widget.setProperty(property_name, state)
+ widget.style().polish(widget)
+
+ def item_value(self):
+ return self.input_field.value()
+
+
+class TextWidget(QtWidgets.QWidget, InputObject):
+ default_input_value = ""
+ value_changed = QtCore.Signal(object)
+
+ def __init__(
+ self, input_data, parent,
+ as_widget=False, label_widget=None, parent_widget=None
+ ):
+ if parent_widget is None:
+ parent_widget = parent
+ super(TextWidget, self).__init__(parent_widget)
+
+ self.initial_attributes(input_data, parent, as_widget)
+
+ self.multiline = input_data.get("multiline", False)
+ placeholder = input_data.get("placeholder")
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(5)
+
+ if self.multiline:
+ self.text_input = QtWidgets.QPlainTextEdit(self)
+ else:
+ self.text_input = QtWidgets.QLineEdit(self)
+
+ if placeholder:
+ self.text_input.setPlaceholderText(placeholder)
+
+ self.setFocusProxy(self.text_input)
+
+ layout_kwargs = {}
+ if self.multiline:
+ layout_kwargs["alignment"] = QtCore.Qt.AlignTop
+
+ if not self._as_widget:
+ self.key = input_data["key"]
+ if not label_widget:
+ label = input_data["label"]
+ label_widget = QtWidgets.QLabel(label)
+ layout.addWidget(label_widget, 0, **layout_kwargs)
+ self.label_widget = label_widget
+
+ layout.addWidget(self.text_input, 1, **layout_kwargs)
+
+ self.text_input.textChanged.connect(self._on_value_change)
+
+ def set_value(self, value):
+ if self.multiline:
+ self.text_input.setPlainText(value)
+ else:
+ self.text_input.setText(value)
+
+ def update_style(self):
+ if self._as_widget:
+ if not self.isEnabled():
+ state = self.style_state(False, False, False, False)
+ else:
+ state = self.style_state(
+ False,
+ self._is_invalid,
+ False,
+ self._is_modified
+ )
+ else:
+ state = self.style_state(
+ self.has_studio_override,
+ self.is_invalid,
+ self.is_overriden,
+ self.is_modified
+ )
+
+ if self._state == state:
+ return
+
+ if self._as_widget:
+ property_name = "input-state"
+ widget = self.text_input
+ else:
+ property_name = "state"
+ widget = self.label_widget
+
+ widget.setProperty(property_name, state)
+ widget.style().polish(widget)
+
+ def item_value(self):
+ if self.multiline:
+ return self.text_input.toPlainText()
+ else:
+ return self.text_input.text()
+
+
+class PathInputWidget(QtWidgets.QWidget, InputObject):
+ default_input_value = ""
+ value_changed = QtCore.Signal(object)
+
+ def __init__(
+ self, input_data, parent,
+ as_widget=False, label_widget=None, parent_widget=None
+ ):
+ if parent_widget is None:
+ parent_widget = parent
+ super(PathInputWidget, self).__init__(parent_widget)
+
+ self.initial_attributes(input_data, parent, as_widget)
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(5)
+
+ if not self._as_widget:
+ self.key = input_data["key"]
+ if not label_widget:
+ label = input_data["label"]
+ label_widget = QtWidgets.QLabel(label)
+ layout.addWidget(label_widget, 0)
+ self.label_widget = label_widget
+
+ self.path_input = PathInput(self)
+ self.setFocusProxy(self.path_input)
+ layout.addWidget(self.path_input, 1)
+
+ self.path_input.textChanged.connect(self._on_value_change)
+
+ def set_value(self, value):
+ self.path_input.setText(value)
+
+ def focusOutEvent(self, event):
+ self.path_input.clear_end_path()
+ super(PathInput, self).focusOutEvent(event)
+
+ def update_style(self):
+ if self._as_widget:
+ if not self.isEnabled():
+ state = self.style_state(False, False, False, False)
+ else:
+ state = self.style_state(
+ False,
+ self._is_invalid,
+ False,
+ self._is_modified
+ )
+ else:
+ state = self.style_state(
+ self.has_studio_override,
+ self.is_invalid,
+ self.is_overriden,
+ self.is_modified
+ )
+
+ if self._state == state:
+ return
+
+ if self._as_widget:
+ property_name = "input-state"
+ widget = self.path_input
+ else:
+ property_name = "state"
+ widget = self.label_widget
+
+ widget.setProperty(property_name, state)
+ widget.style().polish(widget)
+
+ def item_value(self):
+ return self.path_input.text()
+
+
+class RawJsonInput(QtWidgets.QPlainTextEdit):
+ tab_length = 4
+
+ def __init__(self, *args, **kwargs):
+ super(RawJsonInput, self).__init__(*args, **kwargs)
+ self.setObjectName("RawJsonInput")
+ self.setTabStopDistance(
+ QtGui.QFontMetricsF(
+ self.font()
+ ).horizontalAdvance(" ") * self.tab_length
+ )
+
+ def sizeHint(self):
+ document = self.document()
+ layout = document.documentLayout()
+
+ height = document.documentMargin() + 2 * self.frameWidth() + 1
+ block = document.begin()
+ while block != document.end():
+ height += layout.blockBoundingRect(block).height()
+ block = block.next()
+
+ hint = super(RawJsonInput, self).sizeHint()
+ hint.setHeight(height)
+
+ return hint
+
+ def set_value(self, value):
+ if value is NOT_SET:
+ value = ""
+ elif not isinstance(value, str):
+ try:
+ value = json.dumps(value, indent=4)
+ except Exception:
+ value = ""
+ self.setPlainText(value)
+
+ def json_value(self):
+ return json.loads(self.toPlainText())
+
+ def has_invalid_value(self):
+ try:
+ self.json_value()
+ return False
+ except Exception:
+ return True
+
+ def resizeEvent(self, event):
+ self.updateGeometry()
+ super(RawJsonInput, self).resizeEvent(event)
+
+
+class RawJsonWidget(QtWidgets.QWidget, InputObject):
+ default_input_value = "{}"
+ value_changed = QtCore.Signal(object)
+
+ def __init__(
+ self, input_data, parent,
+ as_widget=False, label_widget=None, parent_widget=None
+ ):
+ if parent_widget is None:
+ parent_widget = parent
+ super(RawJsonWidget, self).__init__(parent_widget)
+
+ self.initial_attributes(input_data, parent, as_widget)
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(5)
+
+ self.text_input = RawJsonInput(self)
+ self.text_input.setSizePolicy(
+ QtWidgets.QSizePolicy.Minimum,
+ QtWidgets.QSizePolicy.MinimumExpanding
+ )
+
+ self.setFocusProxy(self.text_input)
+
+ if not self._as_widget:
+ self.key = input_data["key"]
+ if not label_widget:
+ label = input_data["label"]
+ label_widget = QtWidgets.QLabel(label)
+ layout.addWidget(label_widget, 0, alignment=QtCore.Qt.AlignTop)
+ self.label_widget = label_widget
+ layout.addWidget(self.text_input, 1, alignment=QtCore.Qt.AlignTop)
+
+ self.text_input.textChanged.connect(self._on_value_change)
+
+ def update_studio_values(self, parent_values):
+ self._is_invalid = self.text_input.has_invalid_value()
+ return super(RawJsonWidget, self).update_studio_values(parent_values)
+
+ def set_value(self, value):
+ self.text_input.set_value(value)
+
+ def _on_value_change(self, *args, **kwargs):
+ self._is_invalid = self.text_input.has_invalid_value()
+ return super(RawJsonWidget, self)._on_value_change(*args, **kwargs)
+
+ def update_style(self):
+ if self._as_widget:
+ if not self.isEnabled():
+ state = self.style_state(False, False, False, False)
+ else:
+ state = self.style_state(
+ False,
+ self._is_invalid,
+ False,
+ self._is_modified
+ )
+ else:
+ state = self.style_state(
+ self.has_studio_override,
+ self.is_invalid,
+ self.is_overriden,
+ self.is_modified
+ )
+
+ if self._state == state:
+ return
+
+ if self._as_widget:
+ property_name = "input-state"
+ widget = self.text_input
+ else:
+ property_name = "state"
+ widget = self.label_widget
+
+ widget.setProperty(property_name, state)
+ widget.style().polish(widget)
+
+ def item_value(self):
+ if self.is_invalid:
+ return NOT_SET
+ return self.text_input.json_value()
+
+
+class ListItem(QtWidgets.QWidget, SettingObject):
+ _btn_size = 20
+ value_changed = QtCore.Signal(object)
+
+ def __init__(
+ self, object_type, input_modifiers, config_parent, parent,
+ is_strict=False
+ ):
+ super(ListItem, self).__init__(parent)
+
+ self._set_default_attributes()
+
+ self._is_strict = is_strict
+
+ self._parent = config_parent
+ self._any_parent_is_group = True
+ self._is_empty = False
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(3)
+
+ char_up = qtawesome.charmap("fa.angle-up")
+ char_down = qtawesome.charmap("fa.angle-down")
+
+ if not self._is_strict:
+ self.add_btn = QtWidgets.QPushButton("+")
+ self.remove_btn = QtWidgets.QPushButton("-")
+ self.up_btn = QtWidgets.QPushButton(char_up)
+ self.down_btn = QtWidgets.QPushButton(char_down)
+
+ font_up_down = qtawesome.font("fa", 13)
+ self.up_btn.setFont(font_up_down)
+ self.down_btn.setFont(font_up_down)
+
+ self.add_btn.setFocusPolicy(QtCore.Qt.ClickFocus)
+ self.remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus)
+ self.up_btn.setFocusPolicy(QtCore.Qt.ClickFocus)
+ self.down_btn.setFocusPolicy(QtCore.Qt.ClickFocus)
+
+ self.add_btn.setFixedSize(self._btn_size, self._btn_size)
+ self.remove_btn.setFixedSize(self._btn_size, self._btn_size)
+ self.up_btn.setFixedSize(self._btn_size, self._btn_size)
+ self.down_btn.setFixedSize(self._btn_size, self._btn_size)
+
+ self.add_btn.setProperty("btn-type", "tool-item")
+ self.remove_btn.setProperty("btn-type", "tool-item")
+ self.up_btn.setProperty("btn-type", "tool-item")
+ self.down_btn.setProperty("btn-type", "tool-item")
+
+ self.add_btn.clicked.connect(self._on_add_clicked)
+ self.remove_btn.clicked.connect(self._on_remove_clicked)
+ self.up_btn.clicked.connect(self._on_up_clicked)
+ self.down_btn.clicked.connect(self._on_down_clicked)
+
+ layout.addWidget(self.add_btn, 0)
+ layout.addWidget(self.remove_btn, 0)
+
+ ItemKlass = TypeToKlass.types[object_type]
+ self.value_input = ItemKlass(
+ input_modifiers,
+ self,
+ as_widget=True,
+ label_widget=None
+ )
+
+ layout.addWidget(self.value_input, 1)
+
+ if not self._is_strict:
+ self.spacer_widget = QtWidgets.QWidget(self)
+ self.spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+ self.spacer_widget.setVisible(False)
+
+ layout.addWidget(self.spacer_widget, 1)
+
+ layout.addWidget(self.up_btn, 0)
+ layout.addWidget(self.down_btn, 0)
+
+ self.value_input.value_changed.connect(self._on_value_change)
+
+ @property
+ def as_widget(self):
+ return self._parent.as_widget
+
+ @property
+ def any_parent_as_widget(self):
+ return self.as_widget or self._parent.any_parent_as_widget
+
+ def set_as_empty(self, is_empty=True):
+ self._is_empty = is_empty
+
+ self.spacer_widget.setVisible(is_empty)
+ self.value_input.setVisible(not is_empty)
+ self.remove_btn.setEnabled(not is_empty)
+ self.up_btn.setVisible(not is_empty)
+ self.down_btn.setVisible(not is_empty)
+ self.order_changed()
+ self._on_value_change()
+
+ def order_changed(self):
+ row = self.row()
+ parent_row_count = self.parent_rows_count()
+ if parent_row_count == 1:
+ self.up_btn.setVisible(False)
+ self.down_btn.setVisible(False)
+ return
+
+ if not self.up_btn.isVisible():
+ self.up_btn.setVisible(True)
+ self.down_btn.setVisible(True)
+
+ if row == 0:
+ self.up_btn.setEnabled(False)
+ self.down_btn.setEnabled(True)
+
+ elif row == parent_row_count - 1:
+ self.up_btn.setEnabled(True)
+ self.down_btn.setEnabled(False)
+
+ else:
+ self.up_btn.setEnabled(True)
+ self.down_btn.setEnabled(True)
+
+ def _on_value_change(self, item=None):
+ self.value_changed.emit(self)
+
+ def row(self):
+ return self._parent.input_fields.index(self)
+
+ def parent_rows_count(self):
+ return len(self._parent.input_fields)
+
+ def _on_add_clicked(self):
+ if self._is_empty:
+ self.set_as_empty(False)
+ else:
+ self._parent.add_row(row=self.row() + 1)
+
+ def _on_remove_clicked(self):
+ self._parent.remove_row(self)
+
+ def _on_up_clicked(self):
+ row = self.row()
+ self._parent.swap_rows(row - 1, row)
+
+ def _on_down_clicked(self):
+ row = self.row()
+ self._parent.swap_rows(row, row + 1)
+
+ def config_value(self):
+ if not self._is_empty:
+ return self.value_input.item_value()
+ return NOT_SET
+
+ @property
+ def child_has_studio_override(self):
+ return self.value_input.child_has_studio_override
+
+ @property
+ def child_modified(self):
+ return self.value_input.child_modified
+
+ @property
+ def child_overriden(self):
+ return self.value_input.child_overriden
+
+ def hierarchical_style_update(self):
+ self.value_input.hierarchical_style_update()
+
+ def mouseReleaseEvent(self, event):
+ return QtWidgets.QWidget.mouseReleaseEvent(self, event)
+
+ def update_default_values(self, value):
+ self.value_input.update_default_values(value)
+
+ def update_studio_values(self, value):
+ self.value_input.update_studio_values(value)
+
+ def apply_overrides(self, value):
+ self.value_input.apply_overrides(value)
+
+
+class ListWidget(QtWidgets.QWidget, InputObject):
+ default_input_value = []
+ value_changed = QtCore.Signal(object)
+
+ def __init__(
+ self, input_data, parent,
+ as_widget=False, label_widget=None, parent_widget=None
+ ):
+ if parent_widget is None:
+ parent_widget = parent
+ super(ListWidget, self).__init__(parent_widget)
+ self.setObjectName("ListWidget")
+
+ self.initial_attributes(input_data, parent, as_widget)
+
+ self.object_type = input_data["object_type"]
+ self.input_modifiers = input_data.get("input_modifiers") or {}
+
+ self.input_fields = []
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 5)
+ layout.setSpacing(5)
+
+ if not self.as_widget:
+ self.key = input_data["key"]
+ if not label_widget:
+ label_widget = QtWidgets.QLabel(input_data["label"], self)
+ layout.addWidget(label_widget, alignment=QtCore.Qt.AlignTop)
+
+ self.label_widget = label_widget
+
+ inputs_widget = QtWidgets.QWidget(self)
+ inputs_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+ layout.addWidget(inputs_widget)
+
+ inputs_layout = QtWidgets.QVBoxLayout(inputs_widget)
+ inputs_layout.setContentsMargins(0, 0, 0, 0)
+ inputs_layout.setSpacing(3)
+
+ self.inputs_widget = inputs_widget
+ self.inputs_layout = inputs_layout
+
+ self.add_row(is_empty=True)
+
+ def count(self):
+ return len(self.input_fields)
+
+ def update_studio_values(self, parent_values):
+ super(ListWidget, self).update_studio_values(parent_values)
+
+ self.hierarchical_style_update()
+
+ def set_value(self, value):
+ previous_inputs = tuple(self.input_fields)
+ for item_value in value:
+ self.add_row(value=item_value)
+
+ for input_field in previous_inputs:
+ self.remove_row(input_field)
+
+ if self.count() == 0:
+ self.add_row(is_empty=True)
+
+ def swap_rows(self, row_1, row_2):
+ if row_1 == row_2:
+ return
+
+ if row_1 > row_2:
+ row_1, row_2 = row_2, row_1
+
+ field_1 = self.input_fields[row_1]
+ field_2 = self.input_fields[row_2]
+
+ self.input_fields[row_1] = field_2
+ self.input_fields[row_2] = field_1
+
+ layout_index = self.inputs_layout.indexOf(field_1)
+ self.inputs_layout.insertWidget(layout_index + 1, field_1)
+
+ field_1.order_changed()
+ field_2.order_changed()
+
+ def add_row(self, row=None, value=None, is_empty=False):
+ # Create new item
+ item_widget = ListItem(
+ self.object_type, self.input_modifiers, self, self.inputs_widget
+ )
+
+ previous_field = None
+ next_field = None
+
+ if row is None:
+ if self.input_fields:
+ previous_field = self.input_fields[-1]
+ self.inputs_layout.addWidget(item_widget)
+ self.input_fields.append(item_widget)
+ else:
+ if row > 0:
+ previous_field = self.input_fields[row - 1]
+
+ max_index = self.count()
+ if row < max_index:
+ next_field = self.input_fields[row]
+
+ self.inputs_layout.insertWidget(row, item_widget)
+ self.input_fields.insert(row, item_widget)
+
+ if previous_field:
+ previous_field.order_changed()
+
+ if next_field:
+ next_field.order_changed()
+
+ if is_empty:
+ item_widget.set_as_empty()
+ item_widget.value_changed.connect(self._on_value_change)
+
+ item_widget.order_changed()
+
+ previous_input = None
+ for input_field in self.input_fields:
+ if previous_input is not None:
+ self.setTabOrder(
+ previous_input, input_field.value_input.focusProxy()
+ )
+ previous_input = input_field.value_input.focusProxy()
+
+ # Set text if entered text is not None
+ # else (when add button clicked) trigger `_on_value_change`
+ if value is not None:
+ if self._is_overriden:
+ item_widget.apply_overrides(value)
+ elif not self._has_studio_override:
+ item_widget.update_default_values(value)
+ else:
+ item_widget.update_studio_values(value)
+ self.hierarchical_style_update()
+ else:
+ self._on_value_change()
+ self.updateGeometry()
+
+ def remove_row(self, item_widget):
+ item_widget.value_changed.disconnect()
+
+ row = self.input_fields.index(item_widget)
+ previous_field = None
+ next_field = None
+ if row > 0:
+ previous_field = self.input_fields[row - 1]
+
+ if row != len(self.input_fields) - 1:
+ next_field = self.input_fields[row + 1]
+
+ self.inputs_layout.removeWidget(item_widget)
+ self.input_fields.pop(row)
+ item_widget.setParent(None)
+ item_widget.deleteLater()
+
+ if previous_field:
+ previous_field.order_changed()
+
+ if next_field:
+ next_field.order_changed()
+
+ if self.count() == 0:
+ self.add_row(is_empty=True)
+
+ self._on_value_change()
+ self.updateGeometry()
+
+ def apply_overrides(self, parent_values):
+ self._is_modified = False
+ if parent_values is NOT_SET or self.key not in parent_values:
+ override_value = NOT_SET
+ else:
+ override_value = parent_values[self.key]
+
+ self.override_value = override_value
+
+ if override_value is NOT_SET:
+ self._is_overriden = False
+ self._was_overriden = False
+ if self.has_studio_override:
+ value = self.studio_value
+ else:
+ value = self.default_value
+ else:
+ self._is_overriden = True
+ self._was_overriden = True
+ value = override_value
+
+ self._is_modified = False
+ self._state = None
+
+ self.set_value(value)
+
+ def hierarchical_style_update(self):
+ for input_field in self.input_fields:
+ input_field.hierarchical_style_update()
+ self.update_style()
+
+ def update_style(self):
+ if self._as_widget:
+ if not self.isEnabled():
+ state = self.style_state(False, False, False, False)
+ else:
+ state = self.style_state(
+ False,
+ self._is_invalid,
+ False,
+ self._is_modified
+ )
+ else:
+ state = self.style_state(
+ self.has_studio_override,
+ self.is_invalid,
+ self.is_overriden,
+ self.is_modified
+ )
+ if self._state == state:
+ return
+
+ if self.label_widget:
+ self.label_widget.setProperty("state", state)
+ self.label_widget.style().polish(self.label_widget)
+
+ def item_value(self):
+ output = []
+ for item in self.input_fields:
+ value = item.config_value()
+ if value is not NOT_SET:
+ output.append(value)
+ return output
+
+
+class ListStrictWidget(QtWidgets.QWidget, InputObject):
+ value_changed = QtCore.Signal(object)
+ _default_input_value = None
+
+ def __init__(
+ self, input_data, parent,
+ as_widget=False, label_widget=None, parent_widget=None
+ ):
+ if parent_widget is None:
+ parent_widget = parent
+ super(ListStrictWidget, self).__init__(parent_widget)
+ self.setObjectName("ListStrictWidget")
+
+ self.initial_attributes(input_data, parent, as_widget)
+
+ self.input_fields = []
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 5)
+ layout.setSpacing(5)
+
+ if not self.as_widget:
+ self.key = input_data["key"]
+ if not label_widget:
+ label_widget = QtWidgets.QLabel(input_data["label"], self)
+ layout.addWidget(label_widget, alignment=QtCore.Qt.AlignTop)
+
+ self.label_widget = label_widget
+
+ self._add_children(layout, input_data)
+
+ def _add_children(self, layout, input_data):
+ inputs_widget = QtWidgets.QWidget(self)
+ inputs_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+ layout.addWidget(inputs_widget)
+
+ horizontal = input_data.get("horizontal", True)
+ if horizontal:
+ inputs_layout = QtWidgets.QHBoxLayout(inputs_widget)
+ else:
+ inputs_layout = QtWidgets.QGridLayout(inputs_widget)
+
+ inputs_layout.setContentsMargins(0, 0, 0, 0)
+ inputs_layout.setSpacing(3)
+
+ self.inputs_widget = inputs_widget
+ self.inputs_layout = inputs_layout
+
+ children_item_mapping = []
+ for child_configuration in input_data["object_types"]:
+ object_type = child_configuration["type"]
+
+ item_widget = ListItem(
+ object_type, child_configuration, self, self.inputs_widget,
+ is_strict=True
+ )
+
+ self.input_fields.append(item_widget)
+ item_widget.value_changed.connect(self._on_value_change)
+
+ label = child_configuration.get("label")
+ label_widget = None
+ if label:
+ label_widget = QtWidgets.QLabel(label, self)
+
+ children_item_mapping.append((label_widget, item_widget))
+
+ if horizontal:
+ self._add_children_horizontally(children_item_mapping)
+ else:
+ self._add_children_vertically(children_item_mapping)
+
+ self.updateGeometry()
+
+ def _add_children_vertically(self, children_item_mapping):
+ any_has_label = False
+ for item_mapping in children_item_mapping:
+ if item_mapping[0]:
+ any_has_label = True
+ break
+
+ row = self.inputs_layout.count()
+ if not any_has_label:
+ self.inputs_layout.setColumnStretch(1, 1)
+ for item_mapping in children_item_mapping:
+ item_widget = item_mapping[1]
+ self.inputs_layout.addWidget(item_widget, row, 0, 1, 1)
+
+ spacer_widget = QtWidgets.QWidget(self.inputs_widget)
+ self.inputs_layout.addWidget(spacer_widget, row, 1, 1, 1)
+ row += 1
+
+ else:
+ self.inputs_layout.setColumnStretch(2, 1)
+ for label_widget, item_widget in children_item_mapping:
+ self.inputs_layout.addWidget(
+ label_widget, row, 0, 1, 1,
+ alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop
+ )
+ self.inputs_layout.addWidget(item_widget, row, 1, 1, 1)
+
+ spacer_widget = QtWidgets.QWidget(self.inputs_widget)
+ self.inputs_layout.addWidget(spacer_widget, row, 2, 1, 1)
+ row += 1
+
+ def _add_children_horizontally(self, children_item_mapping):
+ for label_widget, item_widget in children_item_mapping:
+ if label_widget:
+ self.inputs_layout.addWidget(label_widget, 0)
+ self.inputs_layout.addWidget(item_widget, 0)
+
+ spacer_widget = QtWidgets.QWidget(self.inputs_widget)
+ self.inputs_layout.addWidget(spacer_widget, 1)
+
+ @property
+ def default_input_value(self):
+ if self._default_input_value is None:
+ self.set_value(NOT_SET)
+ self._default_input_value = self.item_value()
+ return self._default_input_value
+
+ def set_value(self, value):
+ if self._is_overriden:
+ method_name = "apply_overrides"
+ elif not self._has_studio_override:
+ method_name = "update_default_values"
+ else:
+ method_name = "update_studio_values"
+
+ for idx, input_field in enumerate(self.input_fields):
+ if value is NOT_SET:
+ _value = value
+ else:
+ if idx > len(value) - 1:
+ _value = NOT_SET
+ else:
+ _value = value[idx]
+ _method = getattr(input_field, method_name)
+ _method(_value)
+
+ def hierarchical_style_update(self):
+ for input_field in self.input_fields:
+ input_field.hierarchical_style_update()
+ self.update_style()
+
+ def update_style(self):
+ if self._as_widget:
+ if not self.isEnabled():
+ state = self.style_state(False, False, False, False)
+ else:
+ state = self.style_state(
+ False,
+ self._is_invalid,
+ False,
+ self._is_modified
+ )
+ else:
+ state = self.style_state(
+ self.has_studio_override,
+ self.is_invalid,
+ self.is_overriden,
+ self.is_modified
+ )
+
+ if self._state == state:
+ return
+
+ if self.label_widget:
+ self.label_widget.setProperty("state", state)
+ self.label_widget.style().polish(self.label_widget)
+
+ def item_value(self):
+ output = []
+ for item in self.input_fields:
+ output.append(item.config_value())
+ return output
+
+
+class ModifiableDictItem(QtWidgets.QWidget, SettingObject):
+ _btn_size = 20
+ value_changed = QtCore.Signal(object)
+
+ def __init__(self, object_type, input_modifiers, config_parent, parent):
+ super(ModifiableDictItem, self).__init__(parent)
+
+ self._set_default_attributes()
+ self._parent = config_parent
+
+ any_parent_as_widget = config_parent.as_widget
+ if not any_parent_as_widget:
+ any_parent_as_widget = config_parent.any_parent_as_widget
+
+ self._any_parent_as_widget = any_parent_as_widget
+ self._any_parent_is_group = True
+
+ self._is_empty = False
+ self.is_key_duplicated = False
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(3)
+
+ ItemKlass = TypeToKlass.types[object_type]
+
+ self.key_input = QtWidgets.QLineEdit(self)
+ self.key_input.setObjectName("DictKey")
+
+ self.value_input = ItemKlass(
+ input_modifiers,
+ self,
+ as_widget=True,
+ label_widget=None
+ )
+ self.add_btn = QtWidgets.QPushButton("+")
+ self.remove_btn = QtWidgets.QPushButton("-")
+
+ self.add_btn.setFocusPolicy(QtCore.Qt.ClickFocus)
+ self.remove_btn.setFocusPolicy(QtCore.Qt.ClickFocus)
+
+ self.add_btn.setProperty("btn-type", "tool-item")
+ self.remove_btn.setProperty("btn-type", "tool-item")
+
+ self.spacer_widget = QtWidgets.QWidget(self)
+ self.spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+ self.spacer_widget.setVisible(False)
+
+ layout.addWidget(self.add_btn, 0)
+ layout.addWidget(self.remove_btn, 0)
+ layout.addWidget(self.key_input, 0)
+ layout.addWidget(self.spacer_widget, 1)
+ layout.addWidget(self.value_input, 1)
+
+ self.setFocusProxy(self.value_input)
+
+ self.add_btn.setFixedSize(self._btn_size, self._btn_size)
+ self.remove_btn.setFixedSize(self._btn_size, self._btn_size)
+ self.add_btn.clicked.connect(self.on_add_clicked)
+ self.remove_btn.clicked.connect(self.on_remove_clicked)
+
+ self.key_input.textChanged.connect(self._on_value_change)
+ self.value_input.value_changed.connect(self._on_value_change)
+
+ self.origin_key = NOT_SET
+
+ def key_value(self):
+ return self.key_input.text()
+
+ def is_key_invalid(self):
+ if self._is_empty:
+ return False
+
+ if self.key_value() == "":
+ return True
+
+ if self.is_key_duplicated:
+ return True
+ return False
+
+ def _on_value_change(self, item=None):
+ self.update_style()
+ self.value_changed.emit(self)
+
+ def update_default_values(self, key, value):
+ self.origin_key = key
+ self.key_input.setText(key)
+ self.value_input.update_default_values(value)
+
+ def update_studio_values(self, key, value):
+ self.origin_key = key
+ self.key_input.setText(key)
+ self.value_input.update_studio_values(value)
+
+ def apply_overrides(self, key, value):
+ self.origin_key = key
+ self.key_input.setText(key)
+ self.value_input.apply_overrides(value)
+
+ @property
+ def is_group(self):
+ return self._parent.is_group
+
+ def on_add_clicked(self):
+ if self._is_empty:
+ self.set_as_empty(False)
+ else:
+ self._parent.add_row(row=self.row() + 1)
+
+ def on_remove_clicked(self):
+ self._parent.remove_row(self)
+
+ def set_as_empty(self, is_empty=True):
+ self._is_empty = is_empty
+
+ self.key_input.setVisible(not is_empty)
+ self.value_input.setVisible(not is_empty)
+ self.remove_btn.setEnabled(not is_empty)
+ self.spacer_widget.setVisible(is_empty)
+ self._on_value_change()
+
+ @property
+ def any_parent_is_group(self):
+ return self._parent.any_parent_is_group
+
+ def is_key_modified(self):
+ return self.key_value() != self.origin_key
+
+ def is_value_modified(self):
+ return self.value_input.is_modified
+
+ @property
+ def is_modified(self):
+ return self.is_value_modified() or self.is_key_modified()
+
+ def hierarchical_style_update(self):
+ self.value_input.hierarchical_style_update()
+ self.update_style()
+
+ @property
+ def is_invalid(self):
+ if self._is_empty:
+ return False
+ return self.is_key_invalid() or self.value_input.is_invalid
+
+ def update_style(self):
+ state = ""
+ if not self._is_empty:
+ if self.is_key_invalid():
+ state = "invalid"
+ elif self.is_key_modified():
+ state = "modified"
+
+ self.key_input.setProperty("state", state)
+ self.key_input.style().polish(self.key_input)
+
+ def row(self):
+ return self._parent.input_fields.index(self)
+
+ def item_value(self):
+ key = self.key_input.text()
+ value = self.value_input.item_value()
+ return {key: value}
+
+ def config_value(self):
+ if self._is_empty:
+ return {}
+ return self.item_value()
+
+ def mouseReleaseEvent(self, event):
+ return QtWidgets.QWidget.mouseReleaseEvent(self, event)
+
+
+class ModifiableDict(QtWidgets.QWidget, InputObject):
+ default_input_value = {}
+ # Should be used only for dictionary with one datatype as value
+ # TODO this is actually input field (do not care if is group or not)
+ value_changed = QtCore.Signal(object)
+ expand_in_grid = True
+
+ def __init__(
+ self, input_data, parent,
+ as_widget=False, label_widget=None, parent_widget=None
+ ):
+ if parent_widget is None:
+ parent_widget = parent
+ super(ModifiableDict, self).__init__(parent_widget)
+ self.setObjectName("ModifiableDict")
+
+ self.initial_attributes(input_data, parent, as_widget)
+
+ self.input_fields = []
+
+ self.key = input_data["key"]
+
+ if input_data.get("highlight_content", False):
+ content_state = "hightlighted"
+ bottom_margin = 5
+ else:
+ content_state = ""
+ bottom_margin = 0
+
+ main_layout = QtWidgets.QHBoxLayout(self)
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.setSpacing(0)
+
+ if as_widget:
+ body_widget = None
+ else:
+ body_widget = ExpandingWidget(input_data["label"], self)
+ main_layout.addWidget(body_widget)
+
+ self.body_widget = body_widget
+ self.label_widget = body_widget.label_widget
+
+ collapsable = input_data.get("collapsable", True)
+ if collapsable:
+ collapsed = input_data.get("collapsed", True)
+ if not collapsed:
+ body_widget.toggle_content()
+
+ else:
+ body_widget.hide_toolbox(hide_content=False)
+
+ if body_widget is None:
+ content_parent_widget = self
+ else:
+ content_parent_widget = body_widget
+
+ content_widget = QtWidgets.QWidget(content_parent_widget)
+ content_widget.setObjectName("ContentWidget")
+ content_widget.setProperty("content_state", content_state)
+ content_layout = QtWidgets.QVBoxLayout(content_widget)
+ content_layout.setContentsMargins(CHILD_OFFSET, 3, 0, bottom_margin)
+
+ if body_widget is None:
+ main_layout.addWidget(content_widget)
+ else:
+ body_widget.set_content_widget(content_widget)
+
+ self.body_widget = body_widget
+ self.content_widget = content_widget
+ self.content_layout = content_layout
+
+ self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+
+ self.object_type = input_data["object_type"]
+ self.input_modifiers = input_data.get("input_modifiers") or {}
+
+ self.add_row(is_empty=True)
+
+ def count(self):
+ return len(self.input_fields)
+
+ def set_value(self, value):
+ previous_inputs = tuple(self.input_fields)
+ for item_key, item_value in value.items():
+ self.add_row(key=item_key, value=item_value)
+
+ for input_field in previous_inputs:
+ self.remove_row(input_field)
+
+ if self.count() == 0:
+ self.add_row(is_empty=True)
+
+ def _on_value_change(self, item=None):
+ if self.ignore_value_changes:
+ return
+
+ fields_by_keys = collections.defaultdict(list)
+ for input_field in self.input_fields:
+ key = input_field.key_value()
+ fields_by_keys[key].append(input_field)
+
+ for fields in fields_by_keys.values():
+ if len(fields) == 1:
+ field = fields[0]
+ if field.is_key_duplicated:
+ field.is_key_duplicated = False
+ field.update_style()
+ else:
+ for field in fields:
+ field.is_key_duplicated = True
+ field.update_style()
+
+ if self.is_overidable:
+ self._is_overriden = True
+ else:
+ self._has_studio_override = True
+
+ if self._is_invalid:
+ self._is_modified = True
+ elif self._is_overriden:
+ self._is_modified = self.item_value() != self.override_value
+ elif self._has_studio_override:
+ self._is_modified = self.item_value() != self.studio_value
+ else:
+ self._is_modified = self.item_value() != self.default_value
+
+ self.update_style()
+
+ self.value_changed.emit(self)
+
+ def hierarchical_style_update(self):
+ for input_field in self.input_fields:
+ input_field.hierarchical_style_update()
+ self.update_style()
+
+ def update_style(self):
+ if self._as_widget:
+ if not self.isEnabled():
+ state = self.style_state(False, False, False, False)
+ else:
+ state = self.style_state(
+ False,
+ self.is_invalid,
+ False,
+ self._is_modified
+ )
+ else:
+ state = self.style_state(
+ self.has_studio_override,
+ self.is_invalid,
+ self.is_overriden,
+ self.is_modified
+ )
+ if self._state == state:
+ return
+
+ if state:
+ child_state = "child-{}".format(state)
+ else:
+ child_state = ""
+
+ if self.body_widget:
+ self.body_widget.side_line_widget.setProperty("state", child_state)
+ self.body_widget.side_line_widget.style().polish(
+ self.body_widget.side_line_widget
+ )
+
+ if not self._as_widget:
+ self.label_widget.setProperty("state", state)
+ self.label_widget.style().polish(self.label_widget)
+
+ self._state = state
+
+ def all_item_values(self):
+ output = {}
+ for item in self.input_fields:
+ output.update(item.item_value())
+ return output
+
+ def item_value(self):
+ output = {}
+ for item in self.input_fields:
+ output.update(item.config_value())
+ return output
+
+ def add_row(self, row=None, key=None, value=None, is_empty=False):
+ # Create new item
+ item_widget = ModifiableDictItem(
+ self.object_type, self.input_modifiers, self, self.content_widget
+ )
+ if is_empty:
+ item_widget.set_as_empty()
+
+ item_widget.value_changed.connect(self._on_value_change)
+
+ if row is None:
+ self.content_layout.addWidget(item_widget)
+ self.input_fields.append(item_widget)
+ else:
+ self.content_layout.insertWidget(row, item_widget)
+ self.input_fields.insert(row, item_widget)
+
+ previous_input = None
+ for input_field in self.input_fields:
+ if previous_input is not None:
+ self.setTabOrder(
+ previous_input, input_field.key_input
+ )
+ previous_input = input_field.value_input.focusProxy()
+ self.setTabOrder(
+ input_field.key_input, previous_input
+ )
+
+ # Set value if entered value is not None
+ # else (when add button clicked) trigger `_on_value_change`
+ if value is not None and key is not None:
+ if not self._has_studio_override:
+ item_widget.update_default_values(key, value)
+ elif self._is_overriden:
+ item_widget.apply_overrides(key, value)
+ else:
+ item_widget.update_studio_values(key, value)
+ self.hierarchical_style_update()
+ else:
+ self._on_value_change()
+ self.parent().updateGeometry()
+
+ def remove_row(self, item_widget):
+ item_widget.value_changed.disconnect()
+
+ self.content_layout.removeWidget(item_widget)
+ self.input_fields.remove(item_widget)
+ item_widget.setParent(None)
+ item_widget.deleteLater()
+
+ if self.count() == 0:
+ self.add_row(is_empty=True)
+
+ self._on_value_change()
+ self.parent().updateGeometry()
+
+ @property
+ def is_invalid(self):
+ return self._is_invalid or self.child_invalid
+
+ @property
+ def child_invalid(self):
+ for input_field in self.input_fields:
+ if input_field.is_invalid:
+ return True
+ return False
+
+
+# Dictionaries
+class DictWidget(QtWidgets.QWidget, SettingObject):
+ value_changed = QtCore.Signal(object)
+ expand_in_grid = True
+
+ def __init__(
+ self, input_data, parent,
+ as_widget=False, label_widget=None, parent_widget=None
+ ):
+ if parent_widget is None:
+ parent_widget = parent
+ super(DictWidget, self).__init__(parent_widget)
+
+ self.initial_attributes(input_data, parent, as_widget)
+
+ self.input_fields = []
+
+ self.checkbox_widget = None
+ self.checkbox_key = input_data.get("checkbox_key")
+
+ self.label_widget = label_widget
+
+ if self.as_widget:
+ self._ui_as_widget(input_data)
+ else:
+ self._ui_as_item(input_data)
+
+ def _ui_as_item(self, input_data):
+ self.key = input_data["key"]
+ if input_data.get("highlight_content", False):
+ content_state = "hightlighted"
+ bottom_margin = 5
+ else:
+ content_state = ""
+ bottom_margin = 0
+
+ main_layout = QtWidgets.QHBoxLayout(self)
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.setSpacing(0)
+
+ body_widget = ExpandingWidget(input_data["label"], self)
+
+ main_layout.addWidget(body_widget)
+
+ content_widget = QtWidgets.QWidget(body_widget)
+ content_widget.setObjectName("ContentWidget")
+ content_widget.setProperty("content_state", content_state)
+ content_layout = QtWidgets.QGridLayout(content_widget)
+ content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, bottom_margin)
+
+ body_widget.set_content_widget(content_widget)
+
+ self.body_widget = body_widget
+ self.content_widget = content_widget
+ self.content_layout = content_layout
+
+ self.label_widget = body_widget.label_widget
+
+ for child_data in input_data.get("children", []):
+ self.add_children_gui(child_data)
+
+ collapsable = input_data.get("collapsable", True)
+ if len(self.input_fields) == 1 and self.checkbox_widget:
+ body_widget.hide_toolbox(hide_content=True)
+
+ elif collapsable:
+ collapsed = input_data.get("collapsed", True)
+ if not collapsed:
+ body_widget.toggle_content()
+ else:
+ body_widget.hide_toolbox(hide_content=False)
+
+ def _ui_as_widget(self, input_data):
+ body = QtWidgets.QWidget(self)
+ body.setObjectName("DictAsWidgetBody")
+
+ content_layout = QtWidgets.QGridLayout(body)
+ content_layout.setContentsMargins(5, 5, 5, 5)
+ self.content_layout = content_layout
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(5)
+ layout.addWidget(body)
+
+ for child_configuration in input_data["children"]:
+ self.add_children_gui(child_configuration)
+
+ def add_children_gui(self, child_configuration):
+ item_type = child_configuration["type"]
+ klass = TypeToKlass.types.get(item_type)
+
+ row = self.content_layout.rowCount()
+ if not getattr(klass, "is_input_type", False):
+ item = klass(child_configuration, self)
+ self.content_layout.addWidget(item, row, 0, 1, 2)
+ return item
+
+ if self.checkbox_key and not self.checkbox_widget:
+ key = child_configuration.get("key")
+ if key == self.checkbox_key:
+ return self._add_checkbox_child(child_configuration)
+
+ label_widget = None
+ if not klass.expand_in_grid:
+ label = child_configuration.get("label")
+ if label is not None:
+ label_widget = GridLabelWidget(label, self)
+ self.content_layout.addWidget(label_widget, row, 0, 1, 1)
+
+ item = klass(child_configuration, self, label_widget=label_widget)
+ item.value_changed.connect(self._on_value_change)
+
+ if label_widget:
+ label_widget.input_field = item
+ self.content_layout.addWidget(item, row, 1, 1, 1)
+ else:
+ self.content_layout.addWidget(item, row, 0, 1, 2)
+
+ self.input_fields.append(item)
+ return item
+
+ def _add_checkbox_child(self, child_configuration):
+ item = BooleanWidget(
+ child_configuration, self, label_widget=self.label_widget
+ )
+ item.value_changed.connect(self._on_value_change)
+
+ self.body_widget.add_widget_after_label(item)
+ self.checkbox_widget = item
+ self.input_fields.append(item)
+ return item
+
+ def remove_overrides(self):
+ self._is_overriden = False
+ self._is_modified = False
+ for input_field in self.input_fields:
+ input_field.remove_overrides()
+
+ def reset_to_pype_default(self):
+ for input_field in self.input_fields:
+ input_field.reset_to_pype_default()
+ self._has_studio_override = False
+
+ def set_studio_default(self):
+ for input_field in self.input_fields:
+ input_field.set_studio_default()
+
+ if self.is_group:
+ self._has_studio_override = True
+
+ def discard_changes(self):
+ self._is_overriden = self._was_overriden
+ self._is_modified = False
+
+ for input_field in self.input_fields:
+ input_field.discard_changes()
+
+ self._is_modified = self.child_modified
+
+ def set_as_overriden(self):
+ if self.is_overriden:
+ return
+
+ if self.is_group:
+ self._is_overriden = True
+ return
+
+ for item in self.input_fields:
+ item.set_as_overriden()
+
+ def update_default_values(self, parent_values):
+ # Make sure this is set to False
+ self._state = None
+ self._child_state = None
+
+ value = NOT_SET
+ if self.as_widget:
+ value = parent_values
+ elif parent_values is not NOT_SET:
+ value = parent_values.get(self.key, NOT_SET)
+
+ for item in self.input_fields:
+ item.update_default_values(value)
+
+ def update_studio_values(self, parent_values):
+ # Make sure this is set to False
+ self._state = None
+ self._child_state = None
+ value = NOT_SET
+ if self.as_widget:
+ value = parent_values
+ else:
+ if parent_values is not NOT_SET:
+ value = parent_values.get(self.key, NOT_SET)
+
+ self._has_studio_override = False
+ if self.is_group and value is not NOT_SET:
+ self._has_studio_override = True
+
+ self._had_studio_override = bool(self._has_studio_override)
+
+ for item in self.input_fields:
+ item.update_studio_values(value)
+
+ def apply_overrides(self, parent_values):
+ # Make sure this is set to False
+ self._state = None
+ self._child_state = None
+
+ if not self.as_widget:
+ metadata = {}
+ groups = tuple()
+ override_values = NOT_SET
+ if parent_values is not NOT_SET:
+ metadata = parent_values.get(METADATA_KEY) or metadata
+ groups = metadata.get("groups") or groups
+ override_values = parent_values.get(self.key, override_values)
+
+ self._is_overriden = self.key in groups
+
+ for item in self.input_fields:
+ item.apply_overrides(override_values)
+
+ if not self.as_widget:
+ if not self._is_overriden:
+ self._is_overriden = (
+ self.is_group
+ and self.is_overidable
+ and self.child_overriden
+ )
+ self._was_overriden = bool(self._is_overriden)
+
+ def _on_value_change(self, item=None):
+ if self.ignore_value_changes:
+ return
+
+ if self.is_group and not (self.as_widget or self.any_parent_as_widget):
+ if self.is_overidable:
+ self._is_overriden = True
+ else:
+ self._has_studio_override = True
+
+ # TODO check if this is required
+ self.hierarchical_style_update()
+
+ self.value_changed.emit(self)
+
+ self.update_style()
+
+ def hierarchical_style_update(self):
+ for input_field in self.input_fields:
+ input_field.hierarchical_style_update()
+ self.update_style()
+
+ def update_style(self, is_overriden=None):
+ # TODO add style update when used as widget
+ if self.as_widget:
+ return
+
+ child_has_studio_override = self.child_has_studio_override
+ child_modified = self.child_modified
+ child_invalid = self.child_invalid
+ child_state = self.style_state(
+ child_has_studio_override,
+ child_invalid,
+ self.child_overriden,
+ child_modified
+ )
+ if child_state:
+ child_state = "child-{}".format(child_state)
+
+ if child_state != self._child_state:
+ self.body_widget.side_line_widget.setProperty("state", child_state)
+ self.body_widget.side_line_widget.style().polish(
+ self.body_widget.side_line_widget
+ )
+ self._child_state = child_state
+
+ state = self.style_state(
+ self.had_studio_override,
+ child_invalid,
+ self.is_overriden,
+ self.is_modified
+ )
+ if self._state == state:
+ return
+
+ self.label_widget.setProperty("state", state)
+ self.label_widget.style().polish(self.label_widget)
+
+ self._state = state
+
+ @property
+ def is_modified(self):
+ if self.is_group:
+ return self._is_modified or self.child_modified
+ return False
+
+ @property
+ def child_has_studio_override(self):
+ for input_field in self.input_fields:
+ if (
+ input_field.has_studio_override
+ or input_field.child_has_studio_override
+ ):
+ return True
+ return False
+
+ @property
+ def child_modified(self):
+ for input_field in self.input_fields:
+ if input_field.child_modified:
+ return True
+ return False
+
+ @property
+ def child_overriden(self):
+ for input_field in self.input_fields:
+ if input_field.is_overriden or input_field.child_overriden:
+ return True
+ return False
+
+ @property
+ def child_invalid(self):
+ for input_field in self.input_fields:
+ if input_field.child_invalid:
+ return True
+ return False
+
+ def get_invalid(self):
+ output = []
+ for input_field in self.input_fields:
+ output.extend(input_field.get_invalid())
+ return output
+
+ def item_value(self):
+ output = {}
+ for input_field in self.input_fields:
+ # TODO maybe merge instead of update should be used
+ # NOTE merge is custom function which merges 2 dicts
+ output.update(input_field.config_value())
+ return output
+
+ def studio_overrides(self):
+ if (
+ not (self.as_widget or self.any_parent_as_widget)
+ and not self.has_studio_override
+ 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
+
+ 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
+
+
+class DictInvisible(QtWidgets.QWidget, SettingObject):
+ # TODO is not overridable by itself
+ value_changed = QtCore.Signal(object)
+ allow_actions = False
+ expand_in_grid = True
+
+ def __init__(
+ self, input_data, parent,
+ as_widget=False, label_widget=None, parent_widget=None
+ ):
+ if parent_widget is None:
+ parent_widget = parent
+ super(DictInvisible, self).__init__(parent_widget)
+ self.setObjectName("DictInvisible")
+
+ self.initial_attributes(input_data, parent, as_widget)
+
+ if self._is_group:
+ raise TypeError("DictInvisible can't be marked as group input.")
+
+ self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+
+ layout = QtWidgets.QGridLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(5)
+
+ self.content_layout = layout
+
+ self.input_fields = []
+
+ self.key = input_data["key"]
+
+ for child_data in input_data.get("children", []):
+ self.add_children_gui(child_data)
+
+ def add_children_gui(self, child_configuration):
+ item_type = child_configuration["type"]
+ klass = TypeToKlass.types.get(item_type)
+
+ row = self.content_layout.rowCount()
+ if not getattr(klass, "is_input_type", False):
+ item = klass(child_configuration, self)
+ self.content_layout.addWidget(item, row, 0, 1, 2)
+ return item
+
+ label_widget = None
+ if not klass.expand_in_grid:
+ label = child_configuration.get("label")
+ if label is not None:
+ label_widget = GridLabelWidget(label, self)
+ self.content_layout.addWidget(label_widget, row, 0, 1, 1)
+
+ item = klass(child_configuration, self, label_widget=label_widget)
+ item.value_changed.connect(self._on_value_change)
+
+ if label_widget:
+ label_widget.input_field = item
+ self.content_layout.addWidget(item, row, 1, 1, 1)
+ else:
+ self.content_layout.addWidget(item, row, 0, 1, 2)
+
+ self.input_fields.append(item)
+ return item
+
+ def update_style(self, *args, **kwargs):
+ return
+
+ @property
+ def child_has_studio_override(self):
+ for input_field in self.input_fields:
+ if (
+ input_field.has_studio_override
+ or input_field.child_has_studio_override
+ ):
+ return True
+ return False
+
+ @property
+ def child_modified(self):
+ for input_field in self.input_fields:
+ if input_field.child_modified:
+ return True
+ return False
+
+ @property
+ def child_overriden(self):
+ for input_field in self.input_fields:
+ if input_field.is_overriden or input_field.child_overriden:
+ return True
+ return False
+
+ @property
+ def child_invalid(self):
+ for input_field in self.input_fields:
+ if input_field.child_invalid:
+ return True
+ return False
+
+ def get_invalid(self):
+ output = []
+ for input_field in self.input_fields:
+ output.extend(input_field.get_invalid())
+ return output
+
+ def item_value(self):
+ output = {}
+ for input_field in self.input_fields:
+ # TODO maybe merge instead of update should be used
+ # NOTE merge is custom function which merges 2 dicts
+ output.update(input_field.config_value())
+ return output
+
+ def _on_value_change(self, item=None):
+ if self.ignore_value_changes:
+ return
+
+ if self.is_group and not self.any_parent_as_widget:
+ if self.is_overidable:
+ self._is_overriden = True
+ else:
+ self._has_studio_override = True
+ self.hierarchical_style_update()
+
+ self.value_changed.emit(self)
+
+ def hierarchical_style_update(self):
+ for input_field in self.input_fields:
+ input_field.hierarchical_style_update()
+ self.update_style()
+
+ def remove_overrides(self):
+ self._is_overriden = False
+ self._is_modified = False
+ for input_field in self.input_fields:
+ input_field.remove_overrides()
+
+ def reset_to_pype_default(self):
+ for input_field in self.input_fields:
+ input_field.reset_to_pype_default()
+ self._has_studio_override = False
+
+ def set_studio_default(self):
+ for input_field in self.input_fields:
+ input_field.set_studio_default()
+
+ if self.is_group:
+ self._has_studio_override = True
+
+ def discard_changes(self):
+ self._is_modified = False
+ self._is_overriden = self._was_overriden
+
+ for input_field in self.input_fields:
+ input_field.discard_changes()
+
+ self._is_modified = self.child_modified
+
+ def set_as_overriden(self):
+ if self.is_overriden:
+ return
+
+ if self.is_group:
+ self._is_overriden = True
+ return
+
+ for item in self.input_fields:
+ item.set_as_overriden()
+
+ def update_default_values(self, parent_values):
+ value = NOT_SET
+ if self._as_widget:
+ value = parent_values
+ elif parent_values is not NOT_SET:
+ value = parent_values.get(self.key, NOT_SET)
+
+ for item in self.input_fields:
+ item.update_default_values(value)
+
+ def update_studio_values(self, parent_values):
+ value = NOT_SET
+ if parent_values is not NOT_SET:
+ value = parent_values.get(self.key, NOT_SET)
+
+ for item in self.input_fields:
+ item.update_studio_values(value)
+
+ def apply_overrides(self, parent_values):
+ # Make sure this is set to False
+ self._state = None
+ self._child_state = None
+
+ metadata = {}
+ groups = tuple()
+ override_values = NOT_SET
+ if parent_values is not NOT_SET:
+ metadata = parent_values.get(METADATA_KEY) or metadata
+ groups = metadata.get("groups") or groups
+ override_values = parent_values.get(self.key, override_values)
+
+ self._is_overriden = self.key in groups
+
+ for item in self.input_fields:
+ item.apply_overrides(override_values)
+
+ if not self._is_overriden:
+ self._is_overriden = (
+ self.is_group
+ and self.is_overidable
+ and self.child_overriden
+ )
+ self._was_overriden = bool(self._is_overriden)
+
+ def studio_overrides(self):
+ if (
+ not (self.as_widget or self.any_parent_as_widget)
+ and not self.has_studio_override
+ 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
+
+ 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
+
+
+class PathWidget(QtWidgets.QWidget, SettingObject):
+ value_changed = QtCore.Signal(object)
+ platforms = ("windows", "darwin", "linux")
+ platform_labels_mapping = {
+ "windows": "Windows",
+ "darwin": "MacOS",
+ "linux": "Linux"
+ }
+
+ def __init__(
+ self, input_data, parent,
+ as_widget=False, label_widget=None, parent_widget=None
+ ):
+ if parent_widget is None:
+ parent_widget = parent
+ super(PathWidget, self).__init__(parent_widget)
+
+ self.initial_attributes(input_data, parent, as_widget)
+
+ # This is partial input and dictionary input
+ if not self.any_parent_is_group and not self._as_widget:
+ self._is_group = True
+ else:
+ self._is_group = False
+
+ self.multiplatform = input_data.get("multiplatform", False)
+ self.multipath = input_data.get("multipath", False)
+
+ self.input_fields = []
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(5)
+
+ if not self._as_widget:
+ self.key = input_data["key"]
+ if not label_widget:
+ label = input_data["label"]
+ label_widget = QtWidgets.QLabel(label)
+ label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+ layout.addWidget(label_widget, 0, alignment=QtCore.Qt.AlignTop)
+ self.label_widget = label_widget
+
+ self.content_widget = QtWidgets.QWidget(self)
+ self.content_layout = QtWidgets.QVBoxLayout(self.content_widget)
+ self.content_layout.setSpacing(0)
+ self.content_layout.setContentsMargins(0, 0, 0, 0)
+
+ layout.addWidget(self.content_widget)
+
+ self.create_gui()
+
+ @property
+ def default_input_value(self):
+ if self.multipath:
+ value_type = list
+ else:
+ value_type = str
+
+ if self.multiplatform:
+ return {
+ platform: value_type()
+ for platform in self.platforms
+ }
+ else:
+ return value_type()
+
+ def create_gui(self):
+ if not self.multiplatform and not self.multipath:
+ input_data = {"key": self.key}
+ path_input = PathInputWidget(
+ input_data, self, label_widget=self.label_widget
+ )
+ self.setFocusProxy(path_input)
+ self.content_layout.addWidget(path_input)
+ self.input_fields.append(path_input)
+ path_input.value_changed.connect(self._on_value_change)
+ return
+
+ input_data_for_list = {
+ "object_type": "path-input"
+ }
+ if not self.multiplatform:
+ input_data_for_list["key"] = self.key
+ input_widget = ListWidget(
+ input_data_for_list, self, label_widget=self.label_widget
+ )
+ self.setFocusProxy(input_widget)
+ self.content_layout.addWidget(input_widget)
+ self.input_fields.append(input_widget)
+ input_widget.value_changed.connect(self._on_value_change)
+ return
+
+ proxy_widget = QtWidgets.QWidget(self.content_widget)
+ proxy_layout = QtWidgets.QFormLayout(proxy_widget)
+ for platform_key in self.platforms:
+ platform_label = self.platform_labels_mapping[platform_key]
+ label_widget = QtWidgets.QLabel(platform_label, proxy_widget)
+ if self.multipath:
+ input_data_for_list["key"] = platform_key
+ input_widget = ListWidget(
+ input_data_for_list, self, label_widget=label_widget
+ )
+ else:
+ input_data = {"key": platform_key}
+ input_widget = PathInputWidget(
+ input_data, self, label_widget=label_widget
+ )
+ proxy_layout.addRow(label_widget, input_widget)
+ self.input_fields.append(input_widget)
+ input_widget.value_changed.connect(self._on_value_change)
+
+ self.setFocusProxy(self.input_fields[0])
+ self.content_layout.addWidget(proxy_widget)
+
+ def update_default_values(self, parent_values):
+ self._state = None
+ self._child_state = None
+ self._is_modified = False
+
+ value = NOT_SET
+ if self._as_widget:
+ value = parent_values
+ elif parent_values is not NOT_SET:
+ if not self.multiplatform:
+ value = parent_values
+ else:
+ value = parent_values.get(self.key, NOT_SET)
+
+ if value is NOT_SET:
+ if self.develop_mode:
+ if self._as_widget or not self.multiplatform:
+ value = {self.key: self.default_input_value}
+ else:
+ value = self.default_input_value
+ self.defaults_not_set = True
+ if value is NOT_SET:
+ raise NotImplementedError((
+ "{} Does not have implemented"
+ " attribute `default_input_value`"
+ ).format(self))
+
+ else:
+ raise ValueError(
+ "Default value is not set. This is implementation BUG."
+ )
+ else:
+ self.defaults_not_set = False
+
+ self.default_value = value
+ self._has_studio_override = False
+ self._had_studio_override = False
+
+ if not self.multiplatform:
+ self.input_fields[0].update_default_values(value)
+ else:
+ for input_field in self.input_fields:
+ input_field.update_default_values(value)
+
+ def update_studio_values(self, parent_values):
+ self._state = None
+ self._child_state = None
+ self._is_modified = False
+
+ value = NOT_SET
+ if self._as_widget:
+ value = parent_values
+ elif parent_values is not NOT_SET:
+ if not self.multiplatform:
+ value = parent_values
+ else:
+ value = parent_values.get(self.key, NOT_SET)
+
+ self.studio_value = value
+ if value is not NOT_SET:
+ self._has_studio_override = True
+ self._had_studio_override = True
+ else:
+ self._has_studio_override = False
+ self._had_studio_override = False
+ value = self.default_value
+
+ if not self.multiplatform:
+ self.input_fields[0].update_studio_values(value)
+ else:
+ for input_field in self.input_fields:
+ input_field.update_studio_values(value)
+
+ def apply_overrides(self, parent_values):
+ self._is_modified = False
+ self._state = None
+ self._child_state = None
+
+ override_values = NOT_SET
+ if self._as_widget:
+ override_values = parent_values
+ elif parent_values is not NOT_SET:
+ if not self.multiplatform:
+ override_values = parent_values
+ else:
+ override_values = parent_values.get(self.key, NOT_SET)
+
+ self._is_overriden = override_values is not NOT_SET
+ self._was_overriden = bool(self._is_overriden)
+
+ if not self.multiplatform:
+ self.input_fields[0].apply_overrides(parent_values)
+ else:
+ for input_field in self.input_fields:
+ input_field.apply_overrides(override_values)
+
+ if not self._is_overriden:
+ self._is_overriden = (
+ self.is_group
+ and self.is_overidable
+ and self.child_overriden
+ )
+ self._is_modified = False
+ self._was_overriden = bool(self._is_overriden)
+
+ def set_value(self, value):
+ if not self.multiplatform:
+ self.input_fields[0].set_value(value)
+
+ else:
+ for input_field in self.input_fields:
+ _value = value[input_field.key]
+ input_field.set_value(_value)
+
+ def _on_value_change(self, item=None):
+ if self.ignore_value_changes:
+ return
+
+ if not self.any_parent_as_widget:
+ if self.is_overidable:
+ self._is_overriden = True
+ else:
+ self._has_studio_override = True
+
+ if self._is_invalid:
+ self._is_modified = True
+ elif self._is_overriden:
+ self._is_modified = self.item_value() != self.override_value
+ elif self._has_studio_override:
+ self._is_modified = self.item_value() != self.studio_value
+ else:
+ self._is_modified = self.item_value() != self.default_value
+
+ self.hierarchical_style_update()
+
+ self.value_changed.emit(self)
+
+ def update_style(self, is_overriden=None):
+ child_has_studio_override = self.child_has_studio_override
+ child_modified = self.child_modified
+ child_invalid = self.child_invalid
+ child_state = self.style_state(
+ child_has_studio_override,
+ child_invalid,
+ self.child_overriden,
+ child_modified
+ )
+ if child_state:
+ child_state = "child-{}".format(child_state)
+
+ if child_state != self._child_state:
+ self.setProperty("state", child_state)
+ self.style().polish(self)
+ self._child_state = child_state
+
+ if not self._as_widget:
+ state = self.style_state(
+ child_has_studio_override,
+ child_invalid,
+ self.is_overriden,
+ self.is_modified
+ )
+ if self._state == state:
+ return
+
+ self.label_widget.setProperty("state", state)
+ self.label_widget.style().polish(self.label_widget)
+
+ self._state = state
+
+ def remove_overrides(self):
+ self._is_overriden = False
+ self._is_modified = False
+ for input_field in self.input_fields:
+ input_field.remove_overrides()
+
+ def reset_to_pype_default(self):
+ for input_field in self.input_fields:
+ input_field.reset_to_pype_default()
+ self._has_studio_override = False
+
+ def set_studio_default(self):
+ for input_field in self.input_fields:
+ input_field.set_studio_default()
+
+ if self.is_group:
+ self._has_studio_override = True
+
+ def discard_changes(self):
+ self._is_modified = False
+ self._is_overriden = self._was_overriden
+
+ for input_field in self.input_fields:
+ input_field.discard_changes()
+
+ self._is_modified = self.child_modified
+
+ def set_as_overriden(self):
+ self._is_overriden = True
+
+ @property
+ def child_has_studio_override(self):
+ for input_field in self.input_fields:
+ if (
+ input_field.has_studio_override
+ or input_field.child_has_studio_override
+ ):
+ return True
+ return False
+
+ @property
+ def child_modified(self):
+ for input_field in self.input_fields:
+ if input_field.child_modified:
+ return True
+ return False
+
+ @property
+ def child_overriden(self):
+ for input_field in self.input_fields:
+ if input_field.child_overriden:
+ return True
+ return False
+
+ @property
+ def child_invalid(self):
+ for input_field in self.input_fields:
+ if input_field.child_invalid:
+ return True
+ return False
+
+ def hierarchical_style_update(self):
+ for input_field in self.input_fields:
+ input_field.hierarchical_style_update()
+ self.update_style()
+
+ def item_value(self):
+ if not self.multiplatform and not self.multipath:
+ return self.input_fields[0].item_value()
+
+ if not self.multiplatform:
+ return self.input_fields[0].item_value()
+
+ output = {}
+ for input_field in self.input_fields:
+ output.update(input_field.config_value())
+ return output
+
+ def studio_overrides(self):
+ if (
+ not (self.as_widget or self.any_parent_as_widget)
+ and not self.has_studio_override
+ and not self.child_has_studio_override
+ ):
+ return NOT_SET, False
+
+ value = self.item_value()
+ if not self.multiplatform:
+ value = {self.key: value}
+ return value, self.is_group
+
+ def overrides(self):
+ if not self.is_overriden and not self.child_overriden:
+ return NOT_SET, False
+
+ value = self.item_value()
+ if not self.multiplatform:
+ value = {self.key: value}
+ return value, self.is_group
+
+
+# Proxy for form layout
+class FormLabel(QtWidgets.QLabel):
+ def __init__(self, *args, **kwargs):
+ super(FormLabel, self).__init__(*args, **kwargs)
+ self.item = None
+
+
+class DictFormWidget(QtWidgets.QWidget, SettingObject):
+ value_changed = QtCore.Signal(object)
+ allow_actions = False
+ expand_in_grid = True
+
+ def __init__(
+ self, input_data, parent,
+ as_widget=False, label_widget=None, parent_widget=None
+ ):
+ if parent_widget is None:
+ parent_widget = parent
+ super(DictFormWidget, self).__init__(parent_widget)
+
+ self.initial_attributes(input_data, parent, as_widget)
+
+ self._as_widget = False
+ self._is_group = False
+
+ self.input_fields = []
+ self.content_layout = QtWidgets.QFormLayout(self)
+ self.content_layout.setContentsMargins(0, 0, 0, 0)
+
+ for child_data in input_data.get("children", []):
+ self.add_children_gui(child_data)
+
+ self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+
+ def add_children_gui(self, child_configuration):
+ item_type = child_configuration["type"]
+ # Pop label to not be set in child
+ label = child_configuration["label"]
+
+ klass = TypeToKlass.types.get(item_type)
+
+ label_widget = FormLabel(label, self)
+
+ item = klass(child_configuration, self, label_widget=label_widget)
+ label_widget.item = item
+
+ item.value_changed.connect(self._on_value_change)
+ self.content_layout.addRow(label_widget, item)
+ self.input_fields.append(item)
+ return item
+
+ def mouseReleaseEvent(self, event):
+ if event.button() == QtCore.Qt.RightButton:
+ position = self.mapFromGlobal(QtGui.QCursor().pos())
+ widget = self.childAt(position)
+ if widget and isinstance(widget, FormLabel):
+ widget.item.mouseReleaseEvent(event)
+ event.accept()
+ return
+ super(DictFormWidget, self).mouseReleaseEvent(event)
+
+ def apply_overrides(self, parent_values):
+ for item in self.input_fields:
+ item.apply_overrides(parent_values)
+
+ def discard_changes(self):
+ self._is_modified = False
+ self._is_overriden = self._was_overriden
+
+ for item in self.input_fields:
+ item.discard_changes()
+
+ self._is_modified = self.child_modified
+
+ def remove_overrides(self):
+ self._is_overriden = False
+ self._is_modified = False
+ for input_field in self.input_fields:
+ input_field.remove_overrides()
+
+ def reset_to_pype_default(self):
+ for input_field in self.input_fields:
+ input_field.reset_to_pype_default()
+ self._has_studio_override = False
+
+ def set_studio_default(self):
+ for input_field in self.input_fields:
+ input_field.set_studio_default()
+
+ if self.is_group:
+ self._has_studio_override = True
+
+ def set_as_overriden(self):
+ if self.is_overriden:
+ return
+
+ if self.is_group:
+ self._is_overriden = True
+ return
+
+ for item in self.input_fields:
+ item.set_as_overriden()
+
+ def update_default_values(self, value):
+ for item in self.input_fields:
+ item.update_default_values(value)
+
+ def update_studio_values(self, value):
+ for item in self.input_fields:
+ item.update_studio_values(value)
+
+ def _on_value_change(self, item=None):
+ if self.ignore_value_changes:
+ return
+
+ self.value_changed.emit(self)
+ if self.any_parent_is_group:
+ self.hierarchical_style_update()
+
+ @property
+ def child_has_studio_override(self):
+ for input_field in self.input_fields:
+ if (
+ input_field.has_studio_override
+ or input_field.child_has_studio_override
+ ):
+ return True
+ return False
+
+ @property
+ def child_modified(self):
+ for input_field in self.input_fields:
+ if input_field.child_modified:
+ return True
+ return False
+
+ @property
+ def child_overriden(self):
+ for input_field in self.input_fields:
+ if input_field.is_overriden or input_field.child_overriden:
+ return True
+ return False
+
+ @property
+ def child_invalid(self):
+ for input_field in self.input_fields:
+ if input_field.child_invalid:
+ return True
+ return False
+
+ def get_invalid(self):
+ output = []
+ for input_field in self.input_fields:
+ output.extend(input_field.get_invalid())
+ return output
+
+ def hierarchical_style_update(self):
+ for input_field in self.input_fields:
+ input_field.hierarchical_style_update()
+
+ def item_value(self):
+ output = {}
+ for input_field in self.input_fields:
+ # TODO maybe merge instead of update should be used
+ # NOTE merge is custom function which merges 2 dicts
+ output.update(input_field.config_value())
+ return output
+
+ def config_value(self):
+ return self.item_value()
+
+ def studio_overrides(self):
+ if (
+ not (self.as_widget or self.any_parent_as_widget)
+ and not self.has_studio_override
+ 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 values, self.is_group
+
+ 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 values, self.is_group
+
+
+class LabelWidget(QtWidgets.QWidget):
+ is_input_type = False
+
+ def __init__(self, configuration, parent=None):
+ super(LabelWidget, self).__init__(parent)
+ self.setObjectName("LabelWidget")
+
+ label = configuration["label"]
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(5, 5, 5, 5)
+ label_widget = QtWidgets.QLabel(label, self)
+ layout.addWidget(label_widget)
+
+
+class SplitterWidget(QtWidgets.QWidget):
+ is_input_type = False
+ _height = 2
+
+ def __init__(self, configuration, parent=None):
+ super(SplitterWidget, self).__init__(parent)
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(5, 5, 5, 5)
+ splitter_item = QtWidgets.QWidget(self)
+ splitter_item.setObjectName("SplitterItem")
+ splitter_item.setMinimumHeight(self._height)
+ splitter_item.setMaximumHeight(self._height)
+ layout.addWidget(splitter_item)
+
+
+TypeToKlass.types["boolean"] = BooleanWidget
+TypeToKlass.types["number"] = NumberWidget
+TypeToKlass.types["text"] = TextWidget
+TypeToKlass.types["path-input"] = PathInputWidget
+TypeToKlass.types["raw-json"] = RawJsonWidget
+TypeToKlass.types["list"] = ListWidget
+TypeToKlass.types["list-strict"] = ListStrictWidget
+TypeToKlass.types["dict-modifiable"] = ModifiableDict
+# DEPRECATED - remove when removed from schemas
+TypeToKlass.types["dict-item"] = DictWidget
+TypeToKlass.types["dict"] = DictWidget
+TypeToKlass.types["dict-invisible"] = DictInvisible
+TypeToKlass.types["path-widget"] = PathWidget
+TypeToKlass.types["form"] = DictFormWidget
+
+TypeToKlass.types["label"] = LabelWidget
+TypeToKlass.types["splitter"] = SplitterWidget
diff --git a/pype/tools/settings/settings/widgets/lib.py b/pype/tools/settings/settings/widgets/lib.py
new file mode 100644
index 0000000000..f54989cfd7
--- /dev/null
+++ b/pype/tools/settings/settings/widgets/lib.py
@@ -0,0 +1,310 @@
+import os
+import json
+import copy
+from pype.settings.lib import OVERRIDEN_KEY
+from queue import Queue
+
+
+# Singleton database of available inputs
+class TypeToKlass:
+ types = {}
+
+
+NOT_SET = type("NOT_SET", (), {"__bool__": lambda obj: False})()
+METADATA_KEY = type("METADATA_KEY", (), {})
+OVERRIDE_VERSION = 1
+CHILD_OFFSET = 15
+
+
+def convert_gui_data_to_overrides(data, first=True):
+ if not data or not isinstance(data, dict):
+ return data
+
+ output = {}
+ if first:
+ output["__override_version__"] = OVERRIDE_VERSION
+
+ if METADATA_KEY in data:
+ metadata = data.pop(METADATA_KEY)
+ for key, value in metadata.items():
+ if key == "groups":
+ output[OVERRIDEN_KEY] = value
+ else:
+ KeyError("Unknown metadata key \"{}\"".format(key))
+
+ for key, value in data.items():
+ output[key] = convert_gui_data_to_overrides(value, False)
+ return output
+
+
+def convert_overrides_to_gui_data(data, first=True):
+ if not data or not isinstance(data, dict):
+ return data
+
+ output = {}
+ if OVERRIDEN_KEY in data:
+ groups = data.pop(OVERRIDEN_KEY)
+ if METADATA_KEY not in output:
+ output[METADATA_KEY] = {}
+ output[METADATA_KEY]["groups"] = groups
+
+ for key, value in data.items():
+ output[key] = convert_overrides_to_gui_data(value, False)
+
+ return output
+
+
+def _fill_inner_schemas(schema_data, schema_collection):
+ if schema_data["type"] == "schema":
+ raise ValueError("First item in schema data can't be schema.")
+
+ children = schema_data.get("children")
+ if not children:
+ return schema_data
+
+ new_children = []
+ for child in children:
+ if child["type"] != "schema":
+ new_child = _fill_inner_schemas(child, schema_collection)
+ new_children.append(new_child)
+ continue
+
+ new_child = _fill_inner_schemas(
+ schema_collection[child["name"]],
+ schema_collection
+ )
+ new_children.append(new_child)
+
+ schema_data["children"] = new_children
+ return schema_data
+
+
+class SchemaMissingFileInfo(Exception):
+ def __init__(self, invalid):
+ full_path_keys = []
+ for item in invalid:
+ full_path_keys.append("\"{}\"".format("/".join(item)))
+
+ msg = (
+ "Schema has missing definition of output file (\"is_file\" key)"
+ " for keys. [{}]"
+ ).format(", ".join(full_path_keys))
+ super(SchemaMissingFileInfo, self).__init__(msg)
+
+
+class SchemeGroupHierarchyBug(Exception):
+ def __init__(self, invalid):
+ full_path_keys = []
+ for item in invalid:
+ full_path_keys.append("\"{}\"".format("/".join(item)))
+
+ msg = (
+ "Items with attribute \"is_group\" can't have another item with"
+ " \"is_group\" attribute as child. Error happened for keys: [{}]"
+ ).format(", ".join(full_path_keys))
+ super(SchemeGroupHierarchyBug, self).__init__(msg)
+
+
+class SchemaDuplicatedKeys(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 keys in one hierarchy level. {}"
+ ).format(" || ".join(items))
+ super(SchemaDuplicatedKeys, self).__init__(msg)
+
+
+def file_keys_from_schema(schema_data):
+ output = []
+ item_type = schema_data["type"]
+ klass = TypeToKlass.types[item_type]
+ if not klass.is_input_type:
+ return output
+
+ keys = []
+ key = schema_data.get("key")
+ if key:
+ keys.append(key)
+
+ for child in schema_data["children"]:
+ if child.get("is_file"):
+ _keys = copy.deepcopy(keys)
+ _keys.append(child["key"])
+ output.append(_keys)
+ continue
+
+ for result in file_keys_from_schema(child):
+ _keys = copy.deepcopy(keys)
+ _keys.extend(result)
+ output.append(_keys)
+ return output
+
+
+def validate_all_has_ending_file(schema_data, is_top=True):
+ item_type = schema_data["type"]
+ klass = TypeToKlass.types[item_type]
+ if not klass.is_input_type:
+ return None
+
+ if schema_data.get("is_file"):
+ return None
+
+ children = schema_data.get("children")
+ if not children:
+ return [[schema_data["key"]]]
+
+ invalid = []
+ keyless = "key" not in schema_data
+ for child in children:
+ result = validate_all_has_ending_file(child, False)
+ if result is None:
+ continue
+
+ if keyless:
+ invalid.extend(result)
+ else:
+ for item in result:
+ new_invalid = [schema_data["key"]]
+ new_invalid.extend(item)
+ invalid.append(new_invalid)
+
+ if not invalid:
+ return None
+
+ if not is_top:
+ return invalid
+
+ raise SchemaMissingFileInfo(invalid)
+
+
+def validate_is_group_is_unique_in_hierarchy(
+ schema_data, any_parent_is_group=False, keys=None
+):
+ is_top = keys is None
+ if keys is None:
+ keys = []
+
+ keyless = "key" not in schema_data
+
+ if not keyless:
+ keys.append(schema_data["key"])
+
+ invalid = []
+ is_group = schema_data.get("is_group")
+ if is_group and any_parent_is_group:
+ invalid.append(copy.deepcopy(keys))
+
+ if is_group:
+ any_parent_is_group = is_group
+
+ children = schema_data.get("children")
+ if not children:
+ return invalid
+
+ for child in children:
+ result = validate_is_group_is_unique_in_hierarchy(
+ child, any_parent_is_group, copy.deepcopy(keys)
+ )
+ if not result:
+ continue
+
+ invalid.extend(result)
+
+ if invalid and is_group and keys not in invalid:
+ invalid.append(copy.deepcopy(keys))
+
+ if not is_top:
+ return invalid
+
+ if invalid:
+ raise SchemeGroupHierarchyBug(invalid)
+
+
+def validate_keys_are_unique(schema_data, keys=None):
+ children = schema_data.get("children")
+ if not children:
+ return
+
+ is_top = keys is None
+ if keys is None:
+ keys = [schema_data["key"]]
+ else:
+ keys.append(schema_data["key"])
+
+ child_queue = Queue()
+ for child in children:
+ child_queue.put(child)
+
+ child_inputs = []
+ while not child_queue.empty():
+ child = child_queue.get()
+ if "key" not in child:
+ _children = child.get("children") or []
+ for _child in _children:
+ child_queue.put(_child)
+ else:
+ child_inputs.append(child)
+
+ duplicated_keys = set()
+ child_keys = set()
+ for child in child_inputs:
+ key = child["key"]
+ if key in child_keys:
+ duplicated_keys.add(key)
+ else:
+ child_keys.add(key)
+
+ invalid = {}
+ if duplicated_keys:
+ joined_keys = "/".join(keys)
+ invalid[joined_keys] = duplicated_keys
+
+ for child in child_inputs:
+ result = validate_keys_are_unique(child, copy.deepcopy(keys))
+ if result:
+ invalid.update(result)
+
+ if not is_top:
+ return invalid
+
+ if invalid:
+ raise SchemaDuplicatedKeys(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)
+
+
+def gui_schema(subfolder, main_schema_name):
+ subfolder, main_schema_name
+ dirpath = os.path.join(
+ os.path.dirname(os.path.dirname(__file__)),
+ "gui_schemas",
+ subfolder
+ )
+
+ loaded_schemas = {}
+ for filename in os.listdir(dirpath):
+ basename, ext = os.path.splitext(filename)
+ if ext != ".json":
+ continue
+
+ filepath = os.path.join(dirpath, filename)
+ with open(filepath, "r") as json_stream:
+ schema_data = json.load(json_stream)
+ loaded_schemas[basename] = schema_data
+
+ main_schema = _fill_inner_schemas(
+ loaded_schemas[main_schema_name],
+ loaded_schemas
+ )
+ validate_schema(main_schema)
+ return main_schema
diff --git a/pype/tools/settings/settings/widgets/tests.py b/pype/tools/settings/settings/widgets/tests.py
new file mode 100644
index 0000000000..fc53e38ad5
--- /dev/null
+++ b/pype/tools/settings/settings/widgets/tests.py
@@ -0,0 +1,136 @@
+from Qt import QtWidgets, QtCore
+
+
+def indented_print(data, indent=0):
+ spaces = " " * (indent * 4)
+ if not isinstance(data, dict):
+ print("{}{}".format(spaces, data))
+ return
+
+ for key, value in data.items():
+ print("{}{}".format(spaces, key))
+ indented_print(value, indent + 1)
+
+
+class SelectableMenu(QtWidgets.QMenu):
+
+ selection_changed = QtCore.Signal()
+
+ def mouseReleaseEvent(self, event):
+ action = self.activeAction()
+ if action and action.isEnabled():
+ action.trigger()
+ self.selection_changed.emit()
+ else:
+ super(SelectableMenu, self).mouseReleaseEvent(event)
+
+ def event(self, event):
+ result = super(SelectableMenu, self).event(event)
+ if event.type() == QtCore.QEvent.Show:
+ parent = self.parent()
+
+ move_point = parent.mapToGlobal(QtCore.QPoint(0, parent.height()))
+ check_point = (
+ move_point
+ + QtCore.QPoint(self.width(), self.height())
+ )
+ visibility_check = (
+ QtWidgets.QApplication.desktop().rect().contains(check_point)
+ )
+ if not visibility_check:
+ move_point -= QtCore.QPoint(0, parent.height() + self.height())
+ self.move(move_point)
+
+ self.updateGeometry()
+ self.repaint()
+
+ return result
+
+
+class AddibleComboBox(QtWidgets.QComboBox):
+ """Searchable ComboBox with empty placeholder value as first value"""
+
+ def __init__(self, placeholder="", parent=None):
+ super(AddibleComboBox, self).__init__(parent)
+
+ self.setEditable(True)
+ # self.setInsertPolicy(self.NoInsert)
+
+ self.lineEdit().setPlaceholderText(placeholder)
+ # self.lineEdit().returnPressed.connect(self.on_return_pressed)
+
+ # Apply completer settings
+ completer = self.completer()
+ completer.setCompletionMode(completer.PopupCompletion)
+ completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
+
+ # def on_return_pressed(self):
+ # text = self.lineEdit().text().strip()
+ # if not text:
+ # return
+ #
+ # index = self.findText(text)
+ # if index < 0:
+ # self.addItems([text])
+ # index = self.findText(text)
+
+ def populate(self, items):
+ self.clear()
+ # self.addItems([""]) # ensure first item is placeholder
+ self.addItems(items)
+
+ def get_valid_value(self):
+ """Return the current text if it's a valid value else None
+
+ Note: The empty placeholder value is valid and returns as ""
+
+ """
+
+ text = self.currentText()
+ lookup = set(self.itemText(i) for i in range(self.count()))
+ if text not in lookup:
+ return None
+
+ return text or None
+
+
+class MultiselectEnum(QtWidgets.QWidget):
+
+ selection_changed = QtCore.Signal()
+
+ def __init__(self, title, parent=None):
+ super(MultiselectEnum, self).__init__(parent)
+ toolbutton = QtWidgets.QToolButton(self)
+ toolbutton.setText(title)
+
+ toolmenu = SelectableMenu(toolbutton)
+
+ toolbutton.setMenu(toolmenu)
+ toolbutton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
+
+ layout = QtWidgets.QHBoxLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(toolbutton)
+
+ self.setLayout(layout)
+
+ toolmenu.selection_changed.connect(self.selection_changed)
+
+ self.toolbutton = toolbutton
+ self.toolmenu = toolmenu
+ self.main_layout = layout
+
+ def populate(self, items):
+ self.toolmenu.clear()
+ self.addItems(items)
+
+ def addItems(self, items):
+ for item in items:
+ action = self.toolmenu.addAction(item)
+ action.setCheckable(True)
+ action.setChecked(True)
+ self.toolmenu.addAction(action)
+
+ def items(self):
+ for action in self.toolmenu.actions():
+ yield action
diff --git a/pype/tools/settings/settings/widgets/widgets.py b/pype/tools/settings/settings/widgets/widgets.py
new file mode 100644
index 0000000000..2a1f5fe804
--- /dev/null
+++ b/pype/tools/settings/settings/widgets/widgets.py
@@ -0,0 +1,281 @@
+from Qt import QtWidgets, QtCore, QtGui
+
+
+class NumberSpinBox(QtWidgets.QDoubleSpinBox):
+ def __init__(self, *args, **kwargs):
+ min_value = kwargs.pop("minimum", -99999)
+ max_value = kwargs.pop("maximum", 99999)
+ decimals = kwargs.pop("decimal", 0)
+ super(NumberSpinBox, self).__init__(*args, **kwargs)
+ self.setFocusPolicy(QtCore.Qt.StrongFocus)
+ self.setDecimals(decimals)
+ self.setMinimum(min_value)
+ self.setMaximum(max_value)
+
+ def wheelEvent(self, event):
+ if self.hasFocus():
+ super(NumberSpinBox, self).wheelEvent(event)
+ else:
+ event.ignore()
+
+ def value(self):
+ output = super(NumberSpinBox, self).value()
+ if self.decimals() == 0:
+ output = int(output)
+ return output
+
+
+class PathInput(QtWidgets.QLineEdit):
+ def clear_end_path(self):
+ value = self.text().strip()
+ if value.endswith("/"):
+ while value and value[-1] == "/":
+ value = value[:-1]
+ self.setText(value)
+
+ def keyPressEvent(self, event):
+ # Always change backslash `\` for forwardslash `/`
+ if event.key() == QtCore.Qt.Key_Backslash:
+ event.accept()
+ new_event = QtGui.QKeyEvent(
+ event.type(),
+ QtCore.Qt.Key_Slash,
+ event.modifiers(),
+ "/",
+ event.isAutoRepeat(),
+ event.count()
+ )
+ QtWidgets.QApplication.sendEvent(self, new_event)
+ return
+ super(PathInput, self).keyPressEvent(event)
+
+ def focusOutEvent(self, event):
+ super(PathInput, self).focusOutEvent(event)
+ self.clear_end_path()
+
+
+class ClickableWidget(QtWidgets.QWidget):
+ clicked = QtCore.Signal()
+
+ def mouseReleaseEvent(self, event):
+ if event.button() == QtCore.Qt.LeftButton:
+ self.clicked.emit()
+ super(ClickableWidget, self).mouseReleaseEvent(event)
+
+
+class ExpandingWidget(QtWidgets.QWidget):
+ def __init__(self, label, parent):
+ super(ExpandingWidget, self).__init__(parent)
+
+ self.toolbox_hidden = False
+
+ top_part = ClickableWidget(parent=self)
+
+ button_size = QtCore.QSize(5, 5)
+ button_toggle = QtWidgets.QToolButton(parent=top_part)
+ button_toggle.setProperty("btn-type", "expand-toggle")
+ button_toggle.setIconSize(button_size)
+ button_toggle.setArrowType(QtCore.Qt.RightArrow)
+ button_toggle.setCheckable(True)
+ button_toggle.setChecked(False)
+
+ label_widget = QtWidgets.QLabel(label, parent=top_part)
+ label_widget.setObjectName("DictLabel")
+
+ side_line_widget = QtWidgets.QWidget(top_part)
+ side_line_widget.setObjectName("SideLineWidget")
+ side_line_layout = QtWidgets.QHBoxLayout(side_line_widget)
+ side_line_layout.setContentsMargins(5, 10, 0, 10)
+ side_line_layout.addWidget(button_toggle)
+ side_line_layout.addWidget(label_widget)
+
+ top_part_layout = QtWidgets.QHBoxLayout(top_part)
+ top_part_layout.setContentsMargins(0, 0, 0, 0)
+ top_part_layout.addWidget(side_line_widget)
+
+ self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+
+ self.top_part_ending = None
+ self.after_label_layout = None
+ self.end_of_layout = None
+
+ self.side_line_widget = side_line_widget
+ self.side_line_layout = side_line_layout
+ self.button_toggle = button_toggle
+ self.label_widget = label_widget
+
+ top_part.clicked.connect(self._top_part_clicked)
+ self.button_toggle.clicked.connect(self._btn_clicked)
+
+ self.main_layout = QtWidgets.QVBoxLayout(self)
+ self.main_layout.setContentsMargins(0, 0, 0, 0)
+ self.main_layout.setSpacing(0)
+ self.main_layout.addWidget(top_part)
+
+ def hide_toolbox(self, hide_content=False):
+ self.button_toggle.setArrowType(QtCore.Qt.NoArrow)
+ self.toolbox_hidden = True
+ self.content_widget.setVisible(not hide_content)
+ self.parent().updateGeometry()
+
+ def set_content_widget(self, content_widget):
+ content_widget.setVisible(False)
+ self.main_layout.addWidget(content_widget)
+ self.content_widget = content_widget
+
+ def _btn_clicked(self):
+ self.toggle_content(self.button_toggle.isChecked())
+
+ def _top_part_clicked(self):
+ self.toggle_content()
+
+ def toggle_content(self, *args):
+ if self.toolbox_hidden:
+ return
+
+ if len(args) > 0:
+ checked = args[0]
+ else:
+ checked = not self.button_toggle.isChecked()
+ arrow_type = QtCore.Qt.RightArrow
+ if checked:
+ arrow_type = QtCore.Qt.DownArrow
+ self.button_toggle.setChecked(checked)
+ self.button_toggle.setArrowType(arrow_type)
+ self.content_widget.setVisible(checked)
+ self.parent().updateGeometry()
+
+ def add_widget_after_label(self, widget):
+ self._add_side_widget_subwidgets()
+ self.after_label_layout.addWidget(widget)
+
+ def _add_side_widget_subwidgets(self):
+ if self.top_part_ending is not None:
+ return
+
+ top_part_ending = QtWidgets.QWidget(self.side_line_widget)
+ top_part_ending.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+
+ top_part_ending_layout = QtWidgets.QHBoxLayout(top_part_ending)
+ top_part_ending_layout.setContentsMargins(0, 0, 0, 0)
+ top_part_ending_layout.setSpacing(0)
+ top_part_ending_layout.setAlignment(QtCore.Qt.AlignVCenter)
+
+ after_label_widget = QtWidgets.QWidget(top_part_ending)
+ spacer_item = QtWidgets.QWidget(top_part_ending)
+ end_of_widget = QtWidgets.QWidget(top_part_ending)
+
+ self.after_label_layout = QtWidgets.QVBoxLayout(after_label_widget)
+ self.after_label_layout.setContentsMargins(0, 0, 0, 0)
+
+ self.end_of_layout = QtWidgets.QVBoxLayout(end_of_widget)
+ self.end_of_layout.setContentsMargins(0, 0, 0, 0)
+
+ spacer_layout = QtWidgets.QVBoxLayout(spacer_item)
+ spacer_layout.setContentsMargins(0, 0, 0, 0)
+
+ top_part_ending_layout.addWidget(after_label_widget, 0)
+ top_part_ending_layout.addWidget(spacer_item, 1)
+ top_part_ending_layout.addWidget(end_of_widget, 0)
+
+ top_part_ending.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+ after_label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+ spacer_item.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+ end_of_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+
+ self.top_part_ending = top_part_ending
+ self.side_line_layout.addWidget(top_part_ending)
+
+ def resizeEvent(self, event):
+ super(ExpandingWidget, self).resizeEvent(event)
+ self.content_widget.updateGeometry()
+
+
+class UnsavedChangesDialog(QtWidgets.QDialog):
+ message = "You have unsaved changes. What do you want to do with them?"
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ message_label = QtWidgets.QLabel(self.message)
+
+ btns_widget = QtWidgets.QWidget(self)
+ btns_layout = QtWidgets.QHBoxLayout(btns_widget)
+
+ btn_ok = QtWidgets.QPushButton("Save")
+ btn_ok.clicked.connect(self.on_ok_pressed)
+ btn_discard = QtWidgets.QPushButton("Discard")
+ btn_discard.clicked.connect(self.on_discard_pressed)
+ btn_cancel = QtWidgets.QPushButton("Cancel")
+ btn_cancel.clicked.connect(self.on_cancel_pressed)
+
+ btns_layout.addWidget(btn_ok)
+ btns_layout.addWidget(btn_discard)
+ btns_layout.addWidget(btn_cancel)
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.addWidget(message_label)
+ layout.addWidget(btns_widget)
+
+ self.state = None
+
+ def on_cancel_pressed(self):
+ self.done(0)
+
+ def on_ok_pressed(self):
+ self.done(1)
+
+ def on_discard_pressed(self):
+ self.done(2)
+
+
+class SpacerWidget(QtWidgets.QWidget):
+ def __init__(self, parent=None):
+ super(SpacerWidget, self).__init__(parent)
+ self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
+
+
+class GridLabelWidget(QtWidgets.QWidget):
+ def __init__(self, label, parent=None):
+ super(GridLabelWidget, self).__init__(parent)
+
+ self.input_field = None
+
+ self.properties = {}
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+
+ label_proxy = QtWidgets.QWidget(self)
+ label_proxy_layout = QtWidgets.QHBoxLayout(label_proxy)
+ label_proxy_layout.setContentsMargins(0, 0, 0, 0)
+ label_proxy_layout.setSpacing(0)
+
+ label_widget = QtWidgets.QLabel(label, label_proxy)
+ spacer_widget_h = SpacerWidget(label_proxy)
+ label_proxy_layout.addWidget(
+ spacer_widget_h, 0, alignment=QtCore.Qt.AlignRight
+ )
+ label_proxy_layout.addWidget(
+ label_widget, 0, alignment=QtCore.Qt.AlignRight
+ )
+
+ spacer_widget_v = SpacerWidget(self)
+
+ layout.addWidget(label_proxy, 0)
+ layout.addWidget(spacer_widget_v, 1)
+
+ self.label_widget = label_widget
+
+ def setProperty(self, name, value):
+ cur_value = self.properties.get(name)
+ if cur_value == value:
+ return
+
+ self.label_widget.setProperty(name, value)
+ self.label_widget.style().polish(self.label_widget)
+
+ def mouseReleaseEvent(self, event):
+ if self.input_field:
+ return self.input_field.show_actions_menu(event)
+ return super(GridLabelWidget, self).mouseReleaseEvent(event)
diff --git a/pype/tools/settings/settings/widgets/window.py b/pype/tools/settings/settings/widgets/window.py
new file mode 100644
index 0000000000..f83da8efe0
--- /dev/null
+++ b/pype/tools/settings/settings/widgets/window.py
@@ -0,0 +1,28 @@
+from Qt import QtWidgets
+from .base import SystemWidget, ProjectWidget
+
+
+class MainWidget(QtWidgets.QWidget):
+ widget_width = 1000
+ widget_height = 600
+
+ def __init__(self, develop, parent=None):
+ super(MainWidget, self).__init__(parent)
+ self.setObjectName("MainWidget")
+ self.setWindowTitle("Pype Settings")
+
+ self.resize(self.widget_width, self.widget_height)
+
+ header_tab_widget = QtWidgets.QTabWidget(parent=self)
+
+ studio_widget = SystemWidget(develop, header_tab_widget)
+ project_widget = ProjectWidget(develop, header_tab_widget)
+ header_tab_widget.addTab(studio_widget, "System")
+ header_tab_widget.addTab(project_widget, "Project")
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.setContentsMargins(5, 5, 5, 5)
+ layout.setSpacing(0)
+ layout.addWidget(header_tab_widget)
+
+ self.setLayout(layout)
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 95a6d3a792..0f90260218 100644
--- a/pype/version.py
+++ b/pype/version.py
@@ -1 +1 @@
-__version__ = "2.12.0"
+__version__ = "2.12.2"