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/hooks/resolve/prelaunch.py b/pype/hooks/resolve/prelaunch.py
index bddeccf4a3..a122b87868 100644
--- a/pype/hooks/resolve/prelaunch.py
+++ b/pype/hooks/resolve/prelaunch.py
@@ -46,13 +46,14 @@ class ResolvePrelaunch(PypeHook):
"`RESOLVE_UTILITY_SCRIPTS_DIR` or reinstall DaVinci Resolve. \n"
f"RESOLVE_UTILITY_SCRIPTS_DIR: `{us_dir}`"
)
+ self.log.debug(f"-- us_dir: `{us_dir}`")
# correctly format path for pre python script
pre_py_sc = os.path.normpath(env.get("PRE_PYTHON_SCRIPT", ""))
env["PRE_PYTHON_SCRIPT"] = pre_py_sc
-
+ self.log.debug(f"-- pre_py_sc: `{pre_py_sc}`...")
try:
- __import__("pype.resolve")
+ __import__("pype.hosts.resolve")
__import__("pyblish")
except ImportError as e:
@@ -62,6 +63,7 @@ class ResolvePrelaunch(PypeHook):
else:
# Resolve Setup integration
importlib.reload(utils)
+ self.log.debug(f"-- utils.__file__: `{utils.__file__}`")
utils.setup(env)
return True
diff --git a/pype/hosts/resolve/__init__.py b/pype/hosts/resolve/__init__.py
index 72d6314b5e..c8f45259ff 100644
--- a/pype/hosts/resolve/__init__.py
+++ b/pype/hosts/resolve/__init__.py
@@ -1,17 +1,34 @@
+from .utils import (
+ setup,
+ get_resolve_module
+)
+
from .pipeline import (
install,
uninstall,
ls,
containerise,
publish,
- launch_workfiles_app
+ launch_workfiles_app,
+ maintained_selection
)
-from .utils import (
- setup,
- get_resolve_module
+from .lib import (
+ get_project_manager,
+ get_current_project,
+ get_current_sequence,
+ get_current_track_items,
+ create_current_sequence_media_bin,
+ create_compound_clip,
+ swap_clips,
+ get_pype_clip_metadata,
+ set_project_manager_to_folder_name
)
+from .menu import launch_pype_menu
+
+from .plugin import Creator
+
from .workio import (
open_file,
save_file,
@@ -21,12 +38,8 @@ from .workio import (
work_root
)
-from .lib import (
- get_project_manager,
- set_project_manager_to_folder_name
-)
-
-from .menu import launch_pype_menu
+bmdvr = None
+bmdvf = None
__all__ = [
# pipeline
@@ -37,6 +50,7 @@ __all__ = [
"reload_pipeline",
"publish",
"launch_workfiles_app",
+ "maintained_selection",
# utils
"setup",
@@ -44,16 +58,30 @@ __all__ = [
# lib
"get_project_manager",
+ "get_current_project",
+ "get_current_sequence",
+ "get_current_track_items",
+ "create_current_sequence_media_bin",
+ "create_compound_clip",
+ "swap_clips",
+ "get_pype_clip_metadata",
"set_project_manager_to_folder_name",
# menu
"launch_pype_menu",
+ # plugin
+ "Creator",
+
# workio
"open_file",
"save_file",
"current_file",
"has_unsaved_changes",
"file_extensions",
- "work_root"
+ "work_root",
+
+ # singleton with black magic resolve module
+ "bmdvr",
+ "bmdvf"
]
diff --git a/pype/hosts/resolve/action.py b/pype/hosts/resolve/action.py
index 31830937c1..a9803cef4e 100644
--- a/pype/hosts/resolve/action.py
+++ b/pype/hosts/resolve/action.py
@@ -21,9 +21,9 @@ class SelectInvalidAction(pyblish.api.Action):
def process(self, context, plugin):
try:
- from pype.hosts.resolve.utils import get_resolve_module
- resolve = get_resolve_module()
- self.log.debug(resolve)
+ from . import get_project_manager
+ pm = get_project_manager()
+ self.log.debug(pm)
except ImportError:
raise ImportError("Current host is not Resolve")
diff --git a/pype/hosts/resolve/lib.py b/pype/hosts/resolve/lib.py
index 2576136df5..deb4fa6339 100644
--- a/pype/hosts/resolve/lib.py
+++ b/pype/hosts/resolve/lib.py
@@ -1,20 +1,406 @@
import sys
-from .utils import get_resolve_module
-from pypeapp import Logger
+import json
+from opentimelineio import opentime
+from pprint import pformat
+
+from pype.api import Logger
log = Logger().get_logger(__name__, "resolve")
self = sys.modules[__name__]
self.pm = None
+self.rename_index = 0
+self.rename_add = 0
+self.pype_metadata_key = "VFX Notes"
def get_project_manager():
+ from . import bmdvr
if not self.pm:
- resolve = get_resolve_module()
- self.pm = resolve.GetProjectManager()
+ self.pm = bmdvr.GetProjectManager()
return self.pm
+def get_current_project():
+ # initialize project manager
+ get_project_manager()
+
+ return self.pm.GetCurrentProject()
+
+
+def get_current_sequence():
+ # get current project
+ project = get_current_project()
+
+ return project.GetCurrentTimeline()
+
+
+def get_current_track_items(
+ filter=False,
+ track_type=None,
+ selecting_color=None):
+ """ Gets all available current timeline track items
+ """
+ track_type = track_type or "video"
+ selecting_color = selecting_color or "Chocolate"
+ project = get_current_project()
+ sequence = get_current_sequence()
+ selected_clips = list()
+
+ # get all tracks count filtered by track type
+ selected_track_count = sequence.GetTrackCount(track_type)
+
+ # loop all tracks and get items
+ _clips = dict()
+ for track_index in range(1, (int(selected_track_count) + 1)):
+ track_name = sequence.GetTrackName(track_type, track_index)
+ track_track_items = sequence.GetItemListInTrack(
+ track_type, track_index)
+ _clips[track_index] = track_track_items
+
+ _data = {
+ "project": project,
+ "sequence": sequence,
+ "track": {
+ "name": track_name,
+ "index": track_index,
+ "type": track_type}
+ }
+ # get track item object and its color
+ for clip_index, ti in enumerate(_clips[track_index]):
+ data = _data.copy()
+ data["clip"] = {
+ "item": ti,
+ "index": clip_index
+ }
+ ti_color = ti.GetClipColor()
+ if filter is True:
+ if selecting_color in ti_color:
+ selected_clips.append(data)
+ # ti.ClearClipColor()
+ else:
+ selected_clips.append(data)
+
+ return selected_clips
+
+
+def create_current_sequence_media_bin(sequence):
+ seq_name = sequence.GetName()
+ media_pool = get_current_project().GetMediaPool()
+ root_folder = media_pool.GetRootFolder()
+ sub_folders = root_folder.GetSubFolderList()
+ testing_names = list()
+
+ print(f"_ sub_folders: {sub_folders}")
+ for subfolder in sub_folders:
+ subf_name = subfolder.GetName()
+ if seq_name in subf_name:
+ testing_names.append(subfolder)
+ else:
+ testing_names.append(False)
+
+ matching = next((f for f in testing_names if f is not False), None)
+
+ if not matching:
+ new_folder = media_pool.AddSubFolder(root_folder, seq_name)
+ media_pool.SetCurrentFolder(new_folder)
+ else:
+ media_pool.SetCurrentFolder(matching)
+
+ return media_pool.GetCurrentFolder()
+
+
+def get_name_with_data(clip_data, presets):
+ """
+ Take hierarchy data from presets and build name with parents data
+
+ Args:
+ clip_data (dict): clip data from `get_current_track_items()`
+ presets (dict): data from create plugin
+
+ Returns:
+ list: name, data
+
+ """
+ def _replace_hash_to_expression(name, text):
+ _spl = text.split("#")
+ _len = (len(_spl) - 1)
+ _repl = f"{{{name}:0>{_len}}}"
+ new_text = text.replace(("#" * _len), _repl)
+ return new_text
+
+ # presets data
+ clip_name = presets["clipName"]
+ hierarchy = presets["hierarchy"]
+ hierarchy_data = presets["hierarchyData"].copy()
+ count_from = presets["countFrom"]
+ steps = presets["steps"]
+
+ # reset rename_add
+ if self.rename_add < count_from:
+ self.rename_add = count_from
+
+ # shot num calculate
+ if self.rename_index == 0:
+ shot_num = self.rename_add
+ else:
+ shot_num = self.rename_add + steps
+
+ print(f"shot_num: {shot_num}")
+
+ # clip data
+ _data = {
+ "sequence": clip_data["sequence"].GetName(),
+ "track": clip_data["track"]["name"].replace(" ", "_"),
+ "shot": shot_num
+ }
+
+ # solve # in test to pythonic explression
+ for k, v in hierarchy_data.items():
+ if "#" not in v:
+ continue
+ hierarchy_data[k] = _replace_hash_to_expression(k, v)
+
+ # fill up pythonic expresisons
+ for k, v in hierarchy_data.items():
+ hierarchy_data[k] = v.format(**_data)
+
+ # fill up clip name and hierarchy keys
+ hierarchy = hierarchy.format(**hierarchy_data)
+ clip_name = clip_name.format(**hierarchy_data)
+
+ self.rename_add = shot_num
+ print(f"shot_num: {shot_num}")
+
+ return (clip_name, {
+ "hierarchy": hierarchy,
+ "hierarchyData": hierarchy_data
+ })
+
+
+def create_compound_clip(clip_data, folder, rename=False, **kwargs):
+ """
+ Convert timeline object into nested timeline object
+
+ Args:
+ clip_data (dict): timeline item object packed into dict
+ with project, timeline (sequence)
+ folder (resolve.MediaPool.Folder): media pool folder object,
+ rename (bool)[optional]: renaming in sequence or not
+ kwargs (optional): additional data needed for rename=True (presets)
+
+ Returns:
+ resolve.MediaPoolItem: media pool item with compound clip timeline(cct)
+ """
+ # get basic objects form data
+ project = clip_data["project"]
+ sequence = clip_data["sequence"]
+ clip = clip_data["clip"]
+
+ # get details of objects
+ clip_item = clip["item"]
+ track = clip_data["track"]
+
+ mp = project.GetMediaPool()
+
+ # get clip attributes
+ clip_attributes = get_clip_attributes(clip_item)
+ print(f"_ clip_attributes: {pformat(clip_attributes)}")
+
+ if rename:
+ presets = kwargs.get("presets")
+ if presets:
+ name, data = get_name_with_data(clip_data, presets)
+ # add hirarchy data to clip attributes
+ clip_attributes.update(data)
+ else:
+ name = "{:0>3}_{:0>4}".format(
+ int(track["index"]), int(clip["index"]))
+ else:
+ # build name
+ clip_name_split = clip_item.GetName().split(".")
+ name = "_".join([
+ track["name"],
+ str(track["index"]),
+ clip_name_split[0],
+ str(clip["index"])]
+ )
+
+ # get metadata
+ mp_item = clip_item.GetMediaPoolItem()
+ mp_props = mp_item.GetClipProperty()
+
+ mp_first_frame = int(mp_props["Start"])
+ mp_last_frame = int(mp_props["End"])
+
+ # initialize basic source timing for otio
+ ci_l_offset = clip_item.GetLeftOffset()
+ ci_duration = clip_item.GetDuration()
+ rate = float(mp_props["FPS"])
+
+ # source rational times
+ mp_in_rc = opentime.RationalTime((ci_l_offset), rate)
+ mp_out_rc = opentime.RationalTime((ci_l_offset + ci_duration - 1), rate)
+
+ # get frame in and out for clip swaping
+ in_frame = opentime.to_frames(mp_in_rc)
+ out_frame = opentime.to_frames(mp_out_rc)
+
+ # keep original sequence
+ sq_origin = sequence
+
+ # Set current folder to input media_pool_folder:
+ mp.SetCurrentFolder(folder)
+
+ # check if clip doesnt exist already:
+ clips = folder.GetClipList()
+ cct = next((c for c in clips
+ if c.GetName() in name), None)
+
+ if cct:
+ print(f"_ cct exists: {cct}")
+ else:
+ # Create empty timeline in current folder and give name:
+ cct = mp.CreateEmptyTimeline(name)
+
+ # check if clip doesnt exist already:
+ clips = folder.GetClipList()
+ cct = next((c for c in clips
+ if c.GetName() in name), None)
+ print(f"_ cct created: {cct}")
+
+ # Set current timeline to created timeline:
+ project.SetCurrentTimeline(cct)
+
+ # Add input clip to the current timeline:
+ mp.AppendToTimeline([{
+ "mediaPoolItem": mp_item,
+ "startFrame": mp_first_frame,
+ "endFrame": mp_last_frame
+ }])
+
+ # Set current timeline to the working timeline:
+ project.SetCurrentTimeline(sq_origin)
+
+ # Add collected metadata and attributes to the comound clip:
+ if mp_item.GetMetadata(self.pype_metadata_key):
+ clip_attributes[self.pype_metadata_key] = mp_item.GetMetadata(
+ self.pype_metadata_key)[self.pype_metadata_key]
+
+ # stringify
+ clip_attributes = json.dumps(clip_attributes)
+
+ # add attributes to metadata
+ for k, v in mp_item.GetMetadata().items():
+ cct.SetMetadata(k, v)
+
+ # add metadata to cct
+ cct.SetMetadata(self.pype_metadata_key, clip_attributes)
+
+ # reset start timecode of the compound clip
+ cct.SetClipProperty("Start TC", mp_props["Start TC"])
+
+ # swap clips on timeline
+ swap_clips(clip_item, cct, name, in_frame, out_frame)
+
+ cct.SetClipColor("Pink")
+ return cct
+
+
+def swap_clips(from_clip, to_clip, to_clip_name, to_in_frame, to_out_frame):
+ """
+ Swaping clips on timeline in timelineItem
+
+ It will add take and activate it to the frame range which is inputted
+
+ Args:
+ from_clip (resolve.mediaPoolItem)
+ to_clip (resolve.mediaPoolItem)
+ to_clip_name (str): name of to_clip
+ to_in_frame (float): cut in frame, usually `GetLeftOffset()`
+ to_out_frame (float): cut out frame, usually left offset plus duration
+
+ Returns:
+ bool: True if successfully replaced
+
+ """
+ # add clip item as take to timeline
+ take = from_clip.AddTake(
+ to_clip,
+ float(to_in_frame),
+ float(to_out_frame)
+ )
+
+ if not take:
+ return False
+
+ for take_index in range(1, (int(from_clip.GetTakesCount()) + 1)):
+ take_item = from_clip.GetTakeByIndex(take_index)
+ take_mp_item = take_item["mediaPoolItem"]
+ if to_clip_name in take_mp_item.GetName():
+ from_clip.SelectTakeByIndex(take_index)
+ from_clip.FinalizeTake()
+ return True
+ return False
+
+
+def validate_tc(x):
+ # Validate and reformat timecode string
+
+ if len(x) != 11:
+ print('Invalid timecode. Try again.')
+
+ c = ':'
+ colonized = x[:2] + c + x[3:5] + c + x[6:8] + c + x[9:]
+
+ if colonized.replace(':', '').isdigit():
+ print(f"_ colonized: {colonized}")
+ return colonized
+ else:
+ print('Invalid timecode. Try again.')
+
+
+def get_pype_clip_metadata(clip):
+ """
+ Get pype metadata created by creator plugin
+
+ Attributes:
+ clip (resolve.TimelineItem): resolve's object
+
+ Returns:
+ dict: hierarchy, orig clip attributes
+ """
+ mp_item = clip.GetMediaPoolItem()
+ metadata = mp_item.GetMetadata()
+
+ return metadata.get(self.pype_metadata_key)
+
+
+def get_clip_attributes(clip):
+ """
+ Collect basic atrributes from resolve timeline item
+
+ Args:
+ clip (resolve.TimelineItem): timeline item object
+
+ Returns:
+ dict: all collected attributres as key: values
+ """
+ mp_item = clip.GetMediaPoolItem()
+
+ data = {
+ "clipIn": clip.GetStart(),
+ "clipOut": clip.GetEnd(),
+ "clipLeftOffset": clip.GetLeftOffset(),
+ "clipRightOffset": clip.GetRightOffset(),
+ "clipMarkers": clip.GetMarkers(),
+ "clipFlags": clip.GetFlagList(),
+ "sourceId": mp_item.GetMediaId(),
+ "sourceProperties": mp_item.GetClipProperty()
+ }
+ return data
+
+
def set_project_manager_to_folder_name(folder_name):
"""
Sets context of Project manager to given folder by name.
diff --git a/pype/hosts/resolve/menu_style.qss b/pype/hosts/resolve/menu_style.qss
index df4fd7e949..ea11c4ca2e 100644
--- a/pype/hosts/resolve/menu_style.qss
+++ b/pype/hosts/resolve/menu_style.qss
@@ -1,6 +1,7 @@
QWidget {
background-color: #282828;
border-radius: 3;
+ font-size: 13px;
}
QPushButton {
@@ -20,10 +21,38 @@ QPushButton:hover {
color: #e64b3d;
}
+QSpinBox {
+ border: 1px solid #090909;
+ background-color: #201f1f;
+ color: #ffffff;
+ padding: 2;
+ max-width: 8em;
+ qproperty-alignment: AlignCenter;
+}
+
+QLineEdit {
+ border: 1px solid #090909;
+ border-radius: 3px;
+ background-color: #201f1f;
+ color: #ffffff;
+ padding: 2;
+ min-width: 10em;
+ qproperty-alignment: AlignCenter;
+}
+
#PypeMenu {
border: 1px solid #fef9ef;
}
-#Spacer {
+QVBoxLayout {
background-color: #282828;
}
+
+#Devider {
+ border: 1px solid #090909;
+ background-color: #585858;
+}
+
+QLabel {
+ color: #77776b;
+}
diff --git a/pype/hosts/resolve/pipeline.py b/pype/hosts/resolve/pipeline.py
index 967aed1436..92bef2e13b 100644
--- a/pype/hosts/resolve/pipeline.py
+++ b/pype/hosts/resolve/pipeline.py
@@ -2,27 +2,23 @@
Basic avalon integration
"""
import os
-# import sys
+import contextlib
from avalon.tools import workfiles
from avalon import api as avalon
from pyblish import api as pyblish
-from pypeapp import Logger
+import pype
+from pype.api import Logger
log = Logger().get_logger(__name__, "resolve")
-# self = sys.modules[__name__]
-
AVALON_CONFIG = os.environ["AVALON_CONFIG"]
-PARENT_DIR = os.path.dirname(__file__)
-PACKAGE_DIR = os.path.dirname(PARENT_DIR)
-PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins")
-LOAD_PATH = os.path.join(PLUGINS_DIR, "resolve", "load")
-CREATE_PATH = os.path.join(PLUGINS_DIR, "resolve", "create")
-INVENTORY_PATH = os.path.join(PLUGINS_DIR, "resolve", "inventory")
+LOAD_PATH = os.path.join(pype.PLUGINS_DIR, "resolve", "load")
+CREATE_PATH = os.path.join(pype.PLUGINS_DIR, "resolve", "create")
+INVENTORY_PATH = os.path.join(pype.PLUGINS_DIR, "resolve", "inventory")
PUBLISH_PATH = os.path.join(
- PLUGINS_DIR, "resolve", "publish"
+ pype.PLUGINS_DIR, "resolve", "publish"
).replace("\\", "/")
AVALON_CONTAINERS = ":AVALON_CONTAINERS"
@@ -40,11 +36,13 @@ def install():
See the Maya equivalent for inspiration on how to implement this.
"""
+ from . import get_resolve_module
# Disable all families except for the ones we explicitly want to see
family_states = [
"imagesequence",
- "mov"
+ "mov",
+ "clip"
]
avalon.data["familiesStateDefault"] = False
avalon.data["familiesStateToggled"] = family_states
@@ -59,6 +57,8 @@ def install():
avalon.register_plugin_path(avalon.Creator, CREATE_PATH)
avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH)
+ get_resolve_module()
+
def uninstall():
"""Uninstall all tha was installed
@@ -140,3 +140,26 @@ def publish(parent):
"""Shorthand to publish from within host"""
from avalon.tools import publish
return publish.show(parent)
+
+
+@contextlib.contextmanager
+def maintained_selection():
+ """Maintain selection during context
+
+ Example:
+ >>> with maintained_selection():
+ ... node['selected'].setValue(True)
+ >>> print(node['selected'].value())
+ False
+ """
+ try:
+ # do the operation
+ yield
+ finally:
+ pass
+
+
+def reset_selection():
+ """Deselect all selected nodes
+ """
+ pass
diff --git a/pype/hosts/resolve/plugin.py b/pype/hosts/resolve/plugin.py
index 628d4bdb26..72eec04896 100644
--- a/pype/hosts/resolve/plugin.py
+++ b/pype/hosts/resolve/plugin.py
@@ -1,6 +1,182 @@
+import re
from avalon import api
-# from pype.hosts.resolve import lib as drlib
+from pype.hosts import resolve
from avalon.vendor import qargparse
+from pype.api import config
+
+from Qt import QtWidgets, QtCore
+
+
+class CreatorWidget(QtWidgets.QDialog):
+
+ # output items
+ items = dict()
+
+ def __init__(self, name, info, presets, parent=None):
+ super(CreatorWidget, self).__init__(parent)
+
+ self.setObjectName(name)
+
+ self.setWindowFlags(
+ QtCore.Qt.Window
+ | QtCore.Qt.CustomizeWindowHint
+ | QtCore.Qt.WindowTitleHint
+ | QtCore.Qt.WindowCloseButtonHint
+ | QtCore.Qt.WindowStaysOnTopHint
+ )
+ self.setWindowTitle(name or "Pype Creator Input")
+
+ # Where inputs and labels are set
+ self.content_widget = [QtWidgets.QWidget(self)]
+ top_layout = QtWidgets.QFormLayout(self.content_widget[0])
+ top_layout.setObjectName("ContentLayout")
+ top_layout.addWidget(Spacer(5, self))
+
+ # first add widget tag line
+ top_layout.addWidget(QtWidgets.QLabel(info))
+
+ top_layout.addWidget(Spacer(5, self))
+
+ # main dynamic layout
+ self.content_widget.append(QtWidgets.QWidget(self))
+ content_layout = QtWidgets.QFormLayout(self.content_widget[-1])
+
+ # add preset data into input widget layout
+ self.items = self.add_presets_to_layout(content_layout, presets)
+
+ # Confirmation buttons
+ btns_widget = QtWidgets.QWidget(self)
+ btns_layout = QtWidgets.QHBoxLayout(btns_widget)
+
+ cancel_btn = QtWidgets.QPushButton("Cancel")
+ btns_layout.addWidget(cancel_btn)
+
+ ok_btn = QtWidgets.QPushButton("Ok")
+ btns_layout.addWidget(ok_btn)
+
+ # Main layout of the dialog
+ main_layout = QtWidgets.QVBoxLayout(self)
+ main_layout.setContentsMargins(10, 10, 10, 10)
+ main_layout.setSpacing(0)
+
+ # adding content widget
+ for w in self.content_widget:
+ main_layout.addWidget(w)
+
+ main_layout.addWidget(btns_widget)
+
+ ok_btn.clicked.connect(self._on_ok_clicked)
+ cancel_btn.clicked.connect(self._on_cancel_clicked)
+
+ stylesheet = resolve.menu.load_stylesheet()
+ self.setStyleSheet(stylesheet)
+
+ def _on_ok_clicked(self):
+ self.result = self.value(self.items)
+ self.close()
+
+ def _on_cancel_clicked(self):
+ self.result = None
+ self.close()
+
+ def value(self, data):
+ for k, v in data.items():
+ if isinstance(v, dict):
+ print(f"nested: {k}")
+ data[k] = self.value(v)
+ elif getattr(v, "value", None):
+ print(f"normal int: {k}")
+ result = v.value()
+ data[k] = result()
+ else:
+ print(f"normal text: {k}")
+ result = v.text()
+ data[k] = result()
+ return data
+
+ def camel_case_split(self, text):
+ matches = re.finditer(
+ '.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)', text)
+ return " ".join([str(m.group(0)).capitalize() for m in matches])
+
+ def create_row(self, layout, type, text, **kwargs):
+ # get type attribute from qwidgets
+ attr = getattr(QtWidgets, type)
+
+ # convert label text to normal capitalized text with spaces
+ label_text = self.camel_case_split(text)
+
+ # assign the new text to lable widget
+ label = QtWidgets.QLabel(label_text)
+ label.setObjectName("LineLabel")
+
+ # create attribute name text strip of spaces
+ attr_name = text.replace(" ", "")
+
+ # create attribute and assign default values
+ setattr(
+ self,
+ attr_name,
+ attr(parent=self))
+
+ # assign the created attribute to variable
+ item = getattr(self, attr_name)
+ for func, val in kwargs.items():
+ if getattr(item, func):
+ func_attr = getattr(item, func)
+ func_attr(val)
+
+ # add to layout
+ layout.addRow(label, item)
+
+ return item
+
+ def add_presets_to_layout(self, content_layout, data):
+ for k, v in data.items():
+ if isinstance(v, dict):
+ # adding spacer between sections
+ self.content_widget.append(QtWidgets.QWidget(self))
+ devider = QtWidgets.QVBoxLayout(self.content_widget[-1])
+ devider.addWidget(Spacer(5, self))
+ devider.setObjectName("Devider")
+
+ # adding nested layout with label
+ self.content_widget.append(QtWidgets.QWidget(self))
+ nested_content_layout = QtWidgets.QFormLayout(
+ self.content_widget[-1])
+ nested_content_layout.setObjectName("NestedContentLayout")
+
+ # add nested key as label
+ self.create_row(nested_content_layout, "QLabel", k)
+ data[k] = self.add_presets_to_layout(nested_content_layout, v)
+ elif isinstance(v, str):
+ print(f"layout.str: {k}")
+ print(f"content_layout: {content_layout}")
+ data[k] = self.create_row(
+ content_layout, "QLineEdit", k, setText=v)
+ elif isinstance(v, int):
+ print(f"layout.int: {k}")
+ print(f"content_layout: {content_layout}")
+ data[k] = self.create_row(
+ content_layout, "QSpinBox", k, setValue=v)
+ return data
+
+
+class Spacer(QtWidgets.QWidget):
+ def __init__(self, height, *args, **kwargs):
+ super(self.__class__, self).__init__(*args, **kwargs)
+
+ self.setFixedHeight(height)
+
+ real_spacer = QtWidgets.QWidget(self)
+ real_spacer.setObjectName("Spacer")
+ real_spacer.setFixedHeight(height)
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(real_spacer)
+
+ self.setLayout(layout)
def get_reference_node_parents(ref):
@@ -73,3 +249,25 @@ class SequenceLoader(api.Loader):
"""Remove an existing `container`
"""
pass
+
+
+class Creator(api.Creator):
+ """Creator class wrapper
+ """
+ marker_color = "Purple"
+
+ def __init__(self, *args, **kwargs):
+ super(Creator, self).__init__(*args, **kwargs)
+ self.presets = config.get_presets()['plugins']["resolve"][
+ "create"].get(self.__class__.__name__, {})
+
+ # adding basic current context resolve objects
+ self.project = resolve.get_current_project()
+ self.sequence = resolve.get_current_sequence()
+
+ if (self.options or {}).get("useSelection"):
+ self.selected = resolve.get_current_track_items(filter=True)
+ else:
+ self.selected = resolve.get_current_track_items(filter=False)
+
+ self.widget = CreatorWidget
diff --git a/pype/hosts/resolve/preload_console.py b/pype/hosts/resolve/preload_console.py
index ea1bd4f180..58975777b8 100644
--- a/pype/hosts/resolve/preload_console.py
+++ b/pype/hosts/resolve/preload_console.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
import time
from pype.hosts.resolve.utils import get_resolve_module
-from pypeapp import Logger
+from pype.api import Logger
log = Logger().get_logger(__name__, "resolve")
diff --git a/pype/hosts/resolve/utility_scripts/Pype_menu.py b/pype/hosts/resolve/utility_scripts/Pype_menu.py
index 1f5cd36277..230a7a80f0 100644
--- a/pype/hosts/resolve/utility_scripts/Pype_menu.py
+++ b/pype/hosts/resolve/utility_scripts/Pype_menu.py
@@ -3,7 +3,7 @@ import sys
import avalon.api as avalon
import pype
-from pypeapp import Logger
+from pype.api import Logger
log = Logger().get_logger(__name__)
diff --git a/pype/hosts/resolve/utility_scripts/__dev_compound_clip.py b/pype/hosts/resolve/utility_scripts/__dev_compound_clip.py
deleted file mode 100644
index fe47008c70..0000000000
--- a/pype/hosts/resolve/utility_scripts/__dev_compound_clip.py
+++ /dev/null
@@ -1,65 +0,0 @@
-#! python3
-# -*- coding: utf-8 -*-
-
-
-# convert clip def
-def convert_clip(timeline=None):
- """Convert timeline item (clip) into compound clip pype container
-
- Args:
- timeline (MediaPool.Timeline): Object of timeline
-
- Returns:
- bool: `True` if success
-
- Raises:
- Exception: description
-
- """
- pass
-
-
-# decorator function create_current_timeline_media_bin()
-def create_current_timeline_media_bin(timeline=None):
- """Convert timeline item (clip) into compound clip pype container
-
- Args:
- timeline (MediaPool.Timeline): Object of timeline
-
- Returns:
- bool: `True` if success
-
- Raises:
- Exception: description
-
- """
- pass
-
-
-# decorator function get_selected_track_items()
-def get_selected_track_items():
- """Convert timeline item (clip) into compound clip pype container
-
- Args:
- timeline (MediaPool.Timeline): Object of timeline
-
- Returns:
- bool: `True` if success
-
- Raises:
- Exception: description
-
- """
- print("testText")
-
-
-# PypeCompoundClip() class
-class PypeCompoundClip(object):
- """docstring for ."""
-
- def __init__(self, arg):
- super(self).__init__()
- self.arg = arg
-
- def create_compound_clip(self):
- pass
diff --git a/pype/hosts/resolve/utility_scripts/__test_pyblish.py b/pype/hosts/resolve/utility_scripts/__test_pyblish.py
deleted file mode 100644
index a6fe991025..0000000000
--- a/pype/hosts/resolve/utility_scripts/__test_pyblish.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import os
-import sys
-import pype
-import importlib
-import pyblish.api
-import pyblish.util
-import avalon.api
-from avalon.tools import publish
-from pypeapp import Logger
-
-log = Logger().get_logger(__name__)
-
-
-def main(env):
- # Registers pype's Global pyblish plugins
- pype.install()
-
- # Register Host (and it's pyblish plugins)
- host_name = env["AVALON_APP"]
- # TODO not sure if use "pype." or "avalon." for host import
- host_import_str = f"pype.{host_name}"
-
- try:
- host_module = importlib.import_module(host_import_str)
- except ModuleNotFoundError:
- log.error((
- f"Host \"{host_name}\" can't be imported."
- f" Import string \"{host_import_str}\" failed."
- ))
- return False
-
- avalon.api.install(host_module)
-
- # Register additional paths
- addition_paths_str = env.get("PUBLISH_PATHS") or ""
- addition_paths = addition_paths_str.split(os.pathsep)
- for path in addition_paths:
- path = os.path.normpath(path)
- if not os.path.exists(path):
- continue
-
- pyblish.api.register_plugin_path(path)
-
- # Register project specific plugins
- project_name = os.environ["AVALON_PROJECT"]
- project_plugins_paths = env.get("PYPE_PROJECT_PLUGINS") or ""
- for path in project_plugins_paths.split(os.pathsep):
- plugin_path = os.path.join(path, project_name, "plugins")
- if os.path.exists(plugin_path):
- pyblish.api.register_plugin_path(plugin_path)
-
- return publish.show()
-
-
-if __name__ == "__main__":
- result = main(os.environ)
- sys.exit(not bool(result))
diff --git a/pype/hosts/resolve/utility_scripts/__test_subprocess.py b/pype/hosts/resolve/utility_scripts/__test_subprocess.py
deleted file mode 100644
index bdc57bbf00..0000000000
--- a/pype/hosts/resolve/utility_scripts/__test_subprocess.py
+++ /dev/null
@@ -1,35 +0,0 @@
-#! python3
-# -*- coding: utf-8 -*-
-import os
-from pypeapp import execute, Logger
-from pype.hosts.resolve.utils import get_resolve_module
-
-log = Logger().get_logger("Resolve")
-
-CURRENT_DIR = os.getenv("RESOLVE_UTILITY_SCRIPTS_DIR", "")
-python_dir = os.getenv("PYTHON36_RESOLVE")
-python_exe = os.path.normpath(
- os.path.join(python_dir, "python.exe")
-)
-
-resolve = get_resolve_module()
-PM = resolve.GetProjectManager()
-P = PM.GetCurrentProject()
-
-log.info(P.GetName())
-
-
-# ______________________________________________________
-# testing subprocessing Scripts
-testing_py = os.path.join(CURRENT_DIR, "ResolvePageSwitcher.py")
-testing_py = os.path.normpath(testing_py)
-log.info(f"Testing path to script: `{testing_py}`")
-
-returncode = execute(
- [python_exe, os.path.normpath(testing_py)],
- env=dict(os.environ)
-)
-
-# Check if output file exists
-if returncode != 0:
- log.error("Executing failed!")
diff --git a/pype/hosts/resolve/utility_scripts/test.py b/pype/hosts/resolve/utility_scripts/test.py
new file mode 100644
index 0000000000..69dc4768bd
--- /dev/null
+++ b/pype/hosts/resolve/utility_scripts/test.py
@@ -0,0 +1,21 @@
+#! python3
+import sys
+from pype.api import Logger
+import DaVinciResolveScript as bmdvr
+
+
+log = Logger().get_logger(__name__)
+
+
+def main():
+ import pype.hosts.resolve as bmdvr
+ bm = bmdvr.utils.get_resolve_module()
+ log.info(f"blackmagicmodule: {bm}")
+
+
+print(f"_>> bmdvr.scriptapp(Resolve): {bmdvr.scriptapp('Resolve')}")
+
+
+if __name__ == "__main__":
+ result = main()
+ sys.exit(not bool(result))
diff --git a/pype/hosts/resolve/utils.py b/pype/hosts/resolve/utils.py
index f5add53a6b..e11cc64b3b 100644
--- a/pype/hosts/resolve/utils.py
+++ b/pype/hosts/resolve/utils.py
@@ -9,18 +9,16 @@ import os
import shutil
from pypeapp import Logger
-
log = Logger().get_logger(__name__, "resolve")
-self = sys.modules[__name__]
-self.bmd = None
-
def get_resolve_module():
+ from pype.hosts import resolve
# dont run if already loaded
- if self.bmd:
- return self.bmd
-
+ if resolve.bmdvr:
+ log.info(("resolve module is assigned to "
+ f"`pype.hosts.resolve.bmdvr`: {resolve.bmdvr}"))
+ return resolve.bmdvr
try:
"""
The PYTHONPATH needs to be set correctly for this import
@@ -71,8 +69,14 @@ def get_resolve_module():
)
sys.exit()
# assign global var and return
- self.bmd = bmd.scriptapp("Resolve")
- return self.bmd
+ bmdvr = bmd.scriptapp("Resolve")
+ # bmdvf = bmd.scriptapp("Fusion")
+ resolve.bmdvr = bmdvr
+ resolve.bmdvf = bmdvr.Fusion()
+ log.info(("Assigning resolve module to "
+ f"`pype.hosts.resolve.bmdvr`: {resolve.bmdvr}"))
+ log.info(("Assigning resolve module to "
+ f"`pype.hosts.resolve.bmdvf`: {resolve.bmdvf}"))
def _sync_utility_scripts(env=None):
diff --git a/pype/hosts/resolve/workio.py b/pype/hosts/resolve/workio.py
index e1e30a8734..9d8d320a3c 100644
--- a/pype/hosts/resolve/workio.py
+++ b/pype/hosts/resolve/workio.py
@@ -2,8 +2,9 @@
import os
from pypeapp import Logger
-from .lib import (
+from . import (
get_project_manager,
+ get_current_project,
set_project_manager_to_folder_name
)
@@ -26,7 +27,7 @@ def save_file(filepath):
pm = get_project_manager()
file = os.path.basename(filepath)
fname, _ = os.path.splitext(file)
- project = pm.GetCurrentProject()
+ project = get_current_project()
name = project.GetName()
if "Untitled Project" not in name:
diff --git a/pype/modules/websocket_server/__init__.py b/pype/modules/websocket_server/__init__.py
new file mode 100644
index 0000000000..eb5a0d9f27
--- /dev/null
+++ b/pype/modules/websocket_server/__init__.py
@@ -0,0 +1,5 @@
+from .websocket_server import WebSocketServer
+
+
+def tray_init(tray_widget, main_widget):
+ return WebSocketServer()
diff --git a/pype/modules/websocket_server/hosts/__init__.py b/pype/modules/websocket_server/hosts/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/pype/modules/websocket_server/hosts/external_app_1.py b/pype/modules/websocket_server/hosts/external_app_1.py
new file mode 100644
index 0000000000..9352787175
--- /dev/null
+++ b/pype/modules/websocket_server/hosts/external_app_1.py
@@ -0,0 +1,47 @@
+import asyncio
+
+from pype.api import Logger
+from wsrpc_aiohttp import WebSocketRoute
+
+log = Logger().get_logger("WebsocketServer")
+
+
+class ExternalApp1(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
+ """
+
+ def init(self, **kwargs):
+ # Python __init__ must be return "self".
+ # This method might return anything.
+ log.debug("someone called ExternalApp1 route")
+ return kwargs
+
+ async def server_function_one(self):
+ log.info('In function one')
+
+ async def server_function_two(self):
+ log.info('In function two')
+ return 'function two'
+
+ async def server_function_three(self):
+ log.info('In function three')
+ asyncio.ensure_future(self.do_notify())
+ return '{"message":"function tree"}'
+
+ async def server_function_four(self, *args, **kwargs):
+ log.info('In function four args {} kwargs {}'.format(args, kwargs))
+ ret = dict(**kwargs)
+ ret["message"] = "function four received arguments"
+ return str(ret)
+
+ # This method calls function on the client side
+ async def do_notify(self):
+ import time
+ time.sleep(5)
+ log.info('Calling function on server after delay')
+ awesome = 'Somebody server_function_three method!'
+ await self.socket.call('notify', result=awesome)
diff --git a/pype/modules/websocket_server/test_client/wsrpc_client.html b/pype/modules/websocket_server/test_client/wsrpc_client.html
new file mode 100644
index 0000000000..9c3f469aca
--- /dev/null
+++ b/pype/modules/websocket_server/test_client/wsrpc_client.html
@@ -0,0 +1,179 @@
+
+
+
+
+ Title
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Test of wsrpc javascript client
+
+
+
+
+
+
+
+
+
+ - Calls server_function_one
+ - Function only logs on server
+ - No return value
+ -
+ -
+ -
+
+
+
+
+
+
+
+
+ - Calls server_function_two
+ - Function logs on server
+ - Returns simple text value
+ -
+ -
+ -
+
+
+
+
+
+
+
+
+ - Calls server_function_three
+ - Function logs on server
+ - Returns json payload
+ - Server then calls function ON the client after delay
+ -
+
+
+
+
+
+
+
+
+ - Calls server_function_four
+ - Function logs on server
+ - Returns modified sent values
+ -
+ -
+ -
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pype/modules/websocket_server/test_client/wsrpc_client.py b/pype/modules/websocket_server/test_client/wsrpc_client.py
new file mode 100644
index 0000000000..ef861513ae
--- /dev/null
+++ b/pype/modules/websocket_server/test_client/wsrpc_client.py
@@ -0,0 +1,34 @@
+import asyncio
+
+from wsrpc_aiohttp import WSRPCClient
+
+"""
+ Simple testing Python client for wsrpc_aiohttp
+ Calls sequentially multiple methods on server
+"""
+
+loop = asyncio.get_event_loop()
+
+
+async def main():
+ print("main")
+ client = WSRPCClient("ws://127.0.0.1:8099/ws/",
+ loop=asyncio.get_event_loop())
+
+ client.add_route('notify', notify)
+ await client.connect()
+ print("connected")
+ print(await client.proxy.ExternalApp1.server_function_one())
+ print(await client.proxy.ExternalApp1.server_function_two())
+ print(await client.proxy.ExternalApp1.server_function_three())
+ print(await client.proxy.ExternalApp1.server_function_four(foo="one"))
+ await client.close()
+
+
+def notify(socket, *args, **kwargs):
+ print("called from server")
+
+
+if __name__ == "__main__":
+ # loop.run_until_complete(main())
+ asyncio.run(main())
diff --git a/pype/modules/websocket_server/websocket_server.py b/pype/modules/websocket_server/websocket_server.py
new file mode 100644
index 0000000000..56e71ea895
--- /dev/null
+++ b/pype/modules/websocket_server/websocket_server.py
@@ -0,0 +1,187 @@
+from pype.api import config, Logger
+
+import threading
+from aiohttp import web
+import asyncio
+from wsrpc_aiohttp import STATIC_DIR, WebSocketAsync
+
+import os
+import sys
+import pyclbr
+import importlib
+
+log = Logger().get_logger("WebsocketServer")
+
+
+class WebSocketServer():
+ """
+ Basic POC implementation of asychronic websocket RPC server.
+ Uses class in external_app_1.py to mimic implementation for single
+ external application.
+ 'test_client' folder contains two test implementations of client
+
+ WIP
+ """
+
+ def __init__(self):
+ self.qaction = None
+ self.failed_icon = None
+ self._is_running = False
+ default_port = 8099
+
+ 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)))
+
+ self.app = web.Application()
+
+ self.app.router.add_route("*", "/ws/", WebSocketAsync)
+ self.app.router.add_static("/js", STATIC_DIR)
+ self.app.router.add_static("/", ".")
+
+ # add route with multiple methods for single "external app"
+ directories_with_routes = ['hosts']
+ self.add_routes_for_directories(directories_with_routes)
+
+ self.websocket_thread = WebsocketServerThread(self, default_port)
+
+ def add_routes_for_directories(self, directories_with_routes):
+ """ Loops through selected directories to find all modules and
+ in them all classes implementing 'WebSocketRoute' that could be
+ used as route.
+ All methods in these classes are registered automatically.
+ """
+ for dir_name in directories_with_routes:
+ dir_name = os.path.join(os.path.dirname(__file__), dir_name)
+ for file_name in os.listdir(dir_name):
+ if '.py' in file_name and '__' not in file_name:
+ self.add_routes_for_module(file_name, dir_name)
+
+ def add_routes_for_module(self, file_name, dir_name):
+ """ Auto routes for all classes implementing 'WebSocketRoute'
+ in 'file_name' in 'dir_name'
+ """
+ module_name = file_name.replace('.py', '')
+ module_info = pyclbr.readmodule(module_name, [dir_name])
+
+ for class_name, cls_object in module_info.items():
+ sys.path.append(dir_name)
+ if 'WebSocketRoute' in cls_object.super:
+ log.debug('Adding route for {}'.format(class_name))
+ module = importlib.import_module(module_name)
+ cls = getattr(module, class_name)
+ WebSocketAsync.add_route(class_name, cls)
+ sys.path.pop()
+
+ def tray_start(self):
+ self.websocket_thread.start()
+
+ def tray_exit(self):
+ self.stop()
+
+ def stop_websocket_server(self):
+
+ self.stop()
+
+ @property
+ def is_running(self):
+ return self.websocket_thread.is_running
+
+ def stop(self):
+ if not self.is_running:
+ return
+ try:
+ log.debug("Stopping websocket server")
+ self.websocket_thread.is_running = False
+ self.websocket_thread.stop()
+ except Exception:
+ log.warning(
+ "Error has happened during Killing websocket server",
+ exc_info=True
+ )
+
+ def thread_stopped(self):
+ self._is_running = False
+
+
+class WebsocketServerThread(threading.Thread):
+ """ Listener for websocket rpc requests.
+
+ It would be probably better to "attach" this to main thread (as for
+ example Harmony needs to run something on main thread), but currently
+ it creates separate thread and separate asyncio event loop
+ """
+ def __init__(self, module, port):
+ super(WebsocketServerThread, self).__init__()
+ self.is_running = False
+ self.port = port
+ self.module = module
+ self.loop = None
+ self.runner = None
+ self.site = None
+
+ def run(self):
+ self.is_running = True
+
+ try:
+ log.info("Starting websocket server")
+ self.loop = asyncio.new_event_loop() # create new loop for thread
+ asyncio.set_event_loop(self.loop)
+
+ self.loop.run_until_complete(self.start_server())
+
+ log.debug(
+ "Running Websocket server on URL:"
+ " \"ws://localhost:{}\"".format(self.port)
+ )
+
+ asyncio.ensure_future(self.check_shutdown(), loop=self.loop)
+ self.loop.run_forever()
+ except Exception:
+ log.warning(
+ "Websocket Server service has failed", exc_info=True
+ )
+ finally:
+ self.loop.close() # optional
+
+ self.is_running = False
+ self.module.thread_stopped()
+ log.info("Websocket server stopped")
+
+ async def start_server(self):
+ """ Starts runner and TCPsite """
+ self.runner = web.AppRunner(self.module.app)
+ await self.runner.setup()
+ self.site = web.TCPSite(self.runner, 'localhost', self.port)
+ await self.site.start()
+
+ def stop(self):
+ """Sets is_running flag to false, 'check_shutdown' shuts server down"""
+ self.is_running = False
+
+ async def check_shutdown(self):
+ """ Future that is running and checks if server should be running
+ periodically.
+ """
+ while self.is_running:
+ await asyncio.sleep(0.5)
+
+ log.debug("Starting shutdown")
+ await self.site.stop()
+ log.debug("Site stopped")
+ await self.runner.cleanup()
+ log.debug("Runner stopped")
+ tasks = [task for task in asyncio.all_tasks() if
+ task is not asyncio.current_task()]
+ list(map(lambda task: task.cancel(), tasks)) # cancel all the tasks
+ results = await asyncio.gather(*tasks, return_exceptions=True)
+ log.debug(f'Finished awaiting cancelled tasks, results: {results}...')
+ await self.loop.shutdown_asyncgens()
+ # to really make sure everything else has time to stop
+ await asyncio.sleep(0.07)
+ self.loop.stop()
diff --git a/pype/plugins/resolve/create/create_shot_clip.py b/pype/plugins/resolve/create/create_shot_clip.py
new file mode 100644
index 0000000000..bd2e013fac
--- /dev/null
+++ b/pype/plugins/resolve/create/create_shot_clip.py
@@ -0,0 +1,79 @@
+from pprint import pformat
+from pype.hosts import resolve
+from pype.hosts.resolve import lib
+
+
+class CreateShotClip(resolve.Creator):
+ """Publishable clip"""
+
+ label = "Shot"
+ family = "clip"
+ icon = "film"
+ defaults = ["Main"]
+
+ gui_name = "Pype sequencial rename with hirerarchy"
+ gui_info = "Define sequencial rename and fill hierarchy data."
+ gui_inputs = {
+ "clipName": "{episode}{sequence}{shot}",
+ "hierarchy": "{folder}/{sequence}/{shot}",
+ "countFrom": 10,
+ "steps": 10,
+ "hierarchyData": {
+ "folder": "shots",
+ "shot": "sh####",
+ "track": "{track}",
+ "sequence": "sc010",
+ "episode": "ep01"
+ }
+ }
+ presets = None
+
+ def process(self):
+ # solve gui inputs overwrites from presets
+ # overwrite gui inputs from presets
+ for k, v in self.gui_inputs.items():
+ if isinstance(v, dict):
+ # nested dictionary (only one level allowed)
+ for _k, _v in v.items():
+ if self.presets.get(_k):
+ self.gui_inputs[k][_k] = self.presets[_k]
+ if self.presets.get(k):
+ self.gui_inputs[k] = self.presets[k]
+
+ # open widget for plugins inputs
+ widget = self.widget(self.gui_name, self.gui_info, self.gui_inputs)
+ widget.exec_()
+
+ print(f"__ selected_clips: {self.selected}")
+ if len(self.selected) < 1:
+ return
+
+ if not widget.result:
+ print("Operation aborted")
+ return
+
+ # sequence attrs
+ sq_frame_start = self.sequence.GetStartFrame()
+ sq_markers = self.sequence.GetMarkers()
+ print(f"__ sq_frame_start: {pformat(sq_frame_start)}")
+ print(f"__ seq_markers: {pformat(sq_markers)}")
+
+ # create media bin for compound clips (trackItems)
+ mp_folder = resolve.create_current_sequence_media_bin(self.sequence)
+ print(f"_ mp_folder: {mp_folder.GetName()}")
+
+ lib.rename_add = 0
+ for i, t_data in enumerate(self.selected):
+ lib.rename_index = i
+
+ # clear color after it is done
+ t_data["clip"]["item"].ClearClipColor()
+
+ # convert track item to timeline media pool item
+ resolve.create_compound_clip(
+ t_data,
+ mp_folder,
+ rename=True,
+ **dict(
+ {"presets": widget.result})
+ )
diff --git a/pype/plugins/resolve/publish/collect_clips.py b/pype/plugins/resolve/publish/collect_clips.py
new file mode 100644
index 0000000000..f86e5c8384
--- /dev/null
+++ b/pype/plugins/resolve/publish/collect_clips.py
@@ -0,0 +1,162 @@
+import os
+from pyblish import api
+from pype.hosts import resolve
+import json
+
+
+class CollectClips(api.ContextPlugin):
+ """Collect all Track items selection."""
+
+ order = api.CollectorOrder + 0.01
+ label = "Collect Clips"
+ hosts = ["resolve"]
+
+ def process(self, context):
+ # create asset_names conversion table
+ if not context.data.get("assetsShared"):
+ self.log.debug("Created `assetsShared` in context")
+ context.data["assetsShared"] = dict()
+
+ projectdata = context.data["projectEntity"]["data"]
+ selection = resolve.get_current_track_items(
+ filter=True, selecting_color="Pink")
+
+ for clip_data in selection:
+ data = dict()
+
+ # get basic objects form data
+ project = clip_data["project"]
+ sequence = clip_data["sequence"]
+ clip = clip_data["clip"]
+
+ # sequence attrs
+ sq_frame_start = sequence.GetStartFrame()
+ self.log.debug(f"sq_frame_start: {sq_frame_start}")
+
+ sq_markers = sequence.GetMarkers()
+
+ # get details of objects
+ clip_item = clip["item"]
+ track = clip_data["track"]
+
+ mp = project.GetMediaPool()
+
+ # get clip attributes
+ clip_metadata = resolve.get_pype_clip_metadata(clip_item)
+ clip_metadata = json.loads(clip_metadata)
+ self.log.debug(f"clip_metadata: {clip_metadata}")
+
+ compound_source_prop = clip_metadata["sourceProperties"]
+ self.log.debug(f"compound_source_prop: {compound_source_prop}")
+
+ asset_name = clip_item.GetName()
+ mp_item = clip_item.GetMediaPoolItem()
+ mp_prop = mp_item.GetClipProperty()
+ source_first = int(compound_source_prop["Start"])
+ source_last = int(compound_source_prop["End"])
+ source_duration = compound_source_prop["Frames"]
+ fps = float(mp_prop["FPS"])
+ self.log.debug(f"source_first: {source_first}")
+ self.log.debug(f"source_last: {source_last}")
+ self.log.debug(f"source_duration: {source_duration}")
+ self.log.debug(f"fps: {fps}")
+
+ source_path = os.path.normpath(
+ compound_source_prop["File Path"])
+ source_name = compound_source_prop["File Name"]
+ source_id = clip_metadata["sourceId"]
+ self.log.debug(f"source_path: {source_path}")
+ self.log.debug(f"source_name: {source_name}")
+ self.log.debug(f"source_id: {source_id}")
+
+ clip_left_offset = int(clip_item.GetLeftOffset())
+ clip_right_offset = int(clip_item.GetRightOffset())
+ self.log.debug(f"clip_left_offset: {clip_left_offset}")
+ self.log.debug(f"clip_right_offset: {clip_right_offset}")
+
+ # source in/out
+ source_in = int(source_first + clip_left_offset)
+ source_out = int(source_first + clip_right_offset)
+ self.log.debug(f"source_in: {source_in}")
+ self.log.debug(f"source_out: {source_out}")
+
+ clip_in = int(clip_item.GetStart() - sq_frame_start)
+ clip_out = int(clip_item.GetEnd() - sq_frame_start)
+ clip_duration = int(clip_item.GetDuration())
+ self.log.debug(f"clip_in: {clip_in}")
+ self.log.debug(f"clip_out: {clip_out}")
+ self.log.debug(f"clip_duration: {clip_duration}")
+
+ is_sequence = False
+
+ self.log.debug(
+ "__ assets_shared: {}".format(
+ context.data["assetsShared"]))
+
+ # Check for clips with the same range
+ # this is for testing if any vertically neighbouring
+ # clips has been already processed
+ clip_matching_with_range = next(
+ (k for k, v in context.data["assetsShared"].items()
+ if (v.get("_clipIn", 0) == clip_in)
+ and (v.get("_clipOut", 0) == clip_out)
+ ), False)
+
+ # check if clip name is the same in matched
+ # vertically neighbouring clip
+ # if it is then it is correct and resent variable to False
+ # not to be rised wrong name exception
+ if asset_name in str(clip_matching_with_range):
+ clip_matching_with_range = False
+
+ # rise wrong name exception if found one
+ assert (not clip_matching_with_range), (
+ "matching clip: {asset}"
+ " timeline range ({clip_in}:{clip_out})"
+ " conflicting with {clip_matching_with_range}"
+ " >> rename any of clips to be the same as the other <<"
+ ).format(
+ **locals())
+
+ if ("[" in source_name) and ("]" in source_name):
+ is_sequence = True
+
+ data.update({
+ "name": "_".join([
+ track["name"], asset_name, source_name]),
+ "item": clip_item,
+ "source": mp_item,
+ # "timecodeStart": str(source.timecodeStart()),
+ "timelineStart": sq_frame_start,
+ "sourcePath": source_path,
+ "sourceFileHead": source_name,
+ "isSequence": is_sequence,
+ "track": track["name"],
+ "trackIndex": track["index"],
+ "sourceFirst": source_first,
+
+ "sourceIn": source_in,
+ "sourceOut": source_out,
+ "mediaDuration": source_duration,
+ "clipIn": clip_in,
+ "clipOut": clip_out,
+ "clipDuration": clip_duration,
+ "asset": asset_name,
+ "subset": "plateMain",
+ "family": "clip",
+ "families": [],
+ "handleStart": projectdata.get("handleStart", 0),
+ "handleEnd": projectdata.get("handleEnd", 0)})
+
+ instance = context.create_instance(**data)
+
+ self.log.info("Created instance: {}".format(instance))
+ self.log.info("Created instance.data: {}".format(instance.data))
+
+ context.data["assetsShared"][asset_name] = {
+ "_clipIn": clip_in,
+ "_clipOut": clip_out
+ }
+ self.log.info(
+ "context.data[\"assetsShared\"]: {}".format(
+ context.data["assetsShared"]))
diff --git a/pype/plugins/resolve/publish/collect_host.py b/pype/plugins/resolve/publish/collect_host.py
deleted file mode 100644
index a5c4b0936c..0000000000
--- a/pype/plugins/resolve/publish/collect_host.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import pyblish.api
-from pype.hosts.resolve.utils import get_resolve_module
-
-
-class CollectProject(pyblish.api.ContextPlugin):
- """Collect Project object"""
-
- order = pyblish.api.CollectorOrder - 0.1
- label = "Collect Project"
- hosts = ["resolve"]
-
- def process(self, context):
- resolve = get_resolve_module()
- PM = resolve.GetProjectManager()
- P = PM.GetCurrentProject()
-
- self.log.info(P.GetName())
diff --git a/pype/plugins/resolve/publish/collect_project.py b/pype/plugins/resolve/publish/collect_project.py
new file mode 100644
index 0000000000..aa57f93619
--- /dev/null
+++ b/pype/plugins/resolve/publish/collect_project.py
@@ -0,0 +1,29 @@
+import os
+import pyblish.api
+from pype.hosts.resolve.utils import get_resolve_module
+
+
+class CollectProject(pyblish.api.ContextPlugin):
+ """Collect Project object"""
+
+ order = pyblish.api.CollectorOrder - 0.1
+ label = "Collect Project"
+ hosts = ["resolve"]
+
+ def process(self, context):
+ exported_projet_ext = ".drp"
+ current_dir = os.getenv("AVALON_WORKDIR")
+ resolve = get_resolve_module()
+ PM = resolve.GetProjectManager()
+ P = PM.GetCurrentProject()
+ name = P.GetName()
+
+ fname = name + exported_projet_ext
+ current_file = os.path.join(current_dir, fname)
+ normalised = os.path.normpath(current_file)
+
+ context.data["project"] = P
+ context.data["currentFile"] = normalised
+
+ self.log.info(name)
+ self.log.debug(normalised)
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..b946ac4b32
--- /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": {
+ "filter": {
+ "families": [
+ "render",
+ "review",
+ "ftrack"
+ ]
+ },
+ "ext": "mp4",
+ "ffmpeg_args": {
+ "input": [
+ "-gamma 2.2"
+ ],
+ "video_filters": [],
+ "audio_filters": [],
+ "output": [
+ "-pix_fmt yuv420p",
+ "-crf 18",
+ "-intra"
+ ]
+ },
+ "tags": [
+ "burnin",
+ "ftrackreview"
+ ]
+ }
+ }
+ }
+ ]
+ },
+ "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..3a74a85468
--- /dev/null
+++ b/pype/settings/defaults/system_settings/global/applications.json
@@ -0,0 +1,34 @@
+{
+ "blender_2.80": true,
+ "blender_2.81": true,
+ "blender_2.82": true,
+ "blender_2.83": true,
+ "celaction_local": true,
+ "celaction_remote": true,
+ "harmony_17": true,
+ "maya_2017": true,
+ "maya_2018": true,
+ "maya_2019": true,
+ "maya_2020": true,
+ "nuke_10.0": true,
+ "nuke_11.2": true,
+ "nuke_11.3": true,
+ "nuke_12.0": true,
+ "nukex_10.0": true,
+ "nukex_11.2": true,
+ "nukex_11.3": true,
+ "nukex_12.0": true,
+ "nukestudio_10.0": true,
+ "nukestudio_11.2": true,
+ "nukestudio_11.3": true,
+ "nukestudio_12.0": true,
+ "houdini_16": true,
+ "houdini_16.5": false,
+ "houdini_17": true,
+ "houdini_18": true,
+ "premiere_2019": true,
+ "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/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/tools.json b/pype/settings/defaults/system_settings/global/tools.json
new file mode 100644
index 0000000000..93895c0e81
--- /dev/null
+++ b/pype/settings/defaults/system_settings/global/tools.json
@@ -0,0 +1,6 @@
+{
+ "mtoa_3.0.1": true,
+ "mtoa_3.1.1": true,
+ "mtoa_3.2.0": true,
+ "yeti_2.1.2": true
+}
\ No newline at end of file
diff --git a/pype/settings/defaults/system_settings/global/tray_modules.json b/pype/settings/defaults/system_settings/global/tray_modules.json
new file mode 100644
index 0000000000..0ff5b15552
--- /dev/null
+++ b/pype/settings/defaults/system_settings/global/tray_modules.json
@@ -0,0 +1,28 @@
+{
+ "item_usage": {
+ "User settings": false,
+ "Ftrack": true,
+ "Muster": false,
+ "Avalon": true,
+ "Clockify": false,
+ "Standalone Publish": true,
+ "Logging": true,
+ "Idle Manager": true,
+ "Timers Manager": true,
+ "Rest Api": true,
+ "Adobe Communicator": true
+ },
+ "attributes": {
+ "Rest Api": {
+ "default_port": 8021,
+ "exclude_ports": []
+ },
+ "Timers Manager": {
+ "full_time": 15.0,
+ "message_time": 0.5
+ },
+ "Clockify": {
+ "workspace_name": ""
+ }
+ }
+}
\ 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/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/__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..fa7c6a366d
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/projects_schema/0_project_gui_schema.json
@@ -0,0 +1,32 @@
+{
+ "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",
+ "children": [
+ "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..6bb14463c1
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json
@@ -0,0 +1,619 @@
+{
+ "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": "dict-form",
+ "children": [
+ {
+ "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": "raw-json",
+ "key": "profiles",
+ "label": "Profiles"
+ }
+ ]
+ }, {
+ "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"
+ }
+ ]
+ }
+
+ ]
+ }
+ ]
+ }
+ ]
+}
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..b16545111c
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json
@@ -0,0 +1,34 @@
+{
+ "key": "system",
+ "type": "dict-invisible",
+ "children": [
+ {
+ "type": "dict-invisible",
+ "key": "global",
+ "children": [{
+ "type": "schema",
+ "children": [
+ "1_tray_items",
+ "1_applications_gui_schema",
+ "1_tools_gui_schema",
+ "1_intents_gui_schema"
+ ]
+ }]
+ }, {
+ "type": "dict-invisible",
+ "key": "muster",
+ "children": [{
+ "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
+ }]
+ }
+ ]
+}
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..48f8ecbd7c
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_applications_gui_schema.json
@@ -0,0 +1,144 @@
+{
+ "key": "applications",
+ "type": "dict",
+ "label": "Applications",
+ "collapsable": true,
+ "is_group": true,
+ "is_file": true,
+ "children": [
+ {
+ "type": "dict-form",
+ "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..a884dcb31e
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json
@@ -0,0 +1,234 @@
+{
+ "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": "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": "dict-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..0c252d2ca9
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_intents_gui_schema.json
@@ -0,0 +1,20 @@
+{
+ "key": "intent",
+ "type": "dict",
+ "label": "Intent Setting",
+ "collapsable": true,
+ "is_group": true,
+ "is_file": true,
+ "children": [
+ {
+ "type": "dict-modifiable",
+ "object_type": "text",
+ "key": "items",
+ "label": "Intent Key/Label"
+ }, {
+ "type": "text",
+ "key": "default",
+ "label": "Default intent"
+ }
+ ]
+}
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..d9540eeb3e
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_tools_gui_schema.json
@@ -0,0 +1,32 @@
+{
+ "key": "tools",
+ "type": "dict",
+ "label": "Tools",
+ "collapsable": true,
+ "is_group": true,
+ "is_file": true,
+ "children": [
+ {
+ "type": "dict-form",
+ "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/gui_schemas/system_schema/1_tray_items.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_tray_items.json
new file mode 100644
index 0000000000..6da974a415
--- /dev/null
+++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_tray_items.json
@@ -0,0 +1,125 @@
+{
+ "key": "tray_modules",
+ "type": "dict",
+ "label": "Modules",
+ "collapsable": true,
+ "is_group": true,
+ "is_file": true,
+ "children": [
+ {
+ "key": "item_usage",
+ "type": "dict-invisible",
+ "children": [
+ {
+ "type": "dict-form",
+ "children": [
+ {
+ "type": "boolean",
+ "key": "User settings",
+ "label": "User settings"
+ }, {
+ "type": "boolean",
+ "key": "Ftrack",
+ "label": "Ftrack"
+ }, {
+ "type": "boolean",
+ "key": "Muster",
+ "label": "Muster"
+ }, {
+ "type": "boolean",
+ "key": "Avalon",
+ "label": "Avalon"
+ }, {
+ "type": "boolean",
+ "key": "Clockify",
+ "label": "Clockify"
+ }, {
+ "type": "boolean",
+ "key": "Standalone Publish",
+ "label": "Standalone Publish"
+ }, {
+ "type": "boolean",
+ "key": "Logging",
+ "label": "Logging"
+ }, {
+ "type": "boolean",
+ "key": "Idle Manager",
+ "label": "Idle Manager"
+ }, {
+ "type": "boolean",
+ "key": "Timers Manager",
+ "label": "Timers Manager"
+ }, {
+ "type": "boolean",
+ "key": "Rest Api",
+ "label": "Rest Api"
+ }, {
+ "type": "boolean",
+ "key": "Adobe Communicator",
+ "label": "Adobe Communicator"
+ }
+ ]
+ }
+ ]
+ }, {
+ "key": "attributes",
+ "type": "dict-invisible",
+ "children": [
+ {
+ "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,
+ "children": [
+ {
+ "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,
+ "children": [
+ {
+ "type": "text",
+ "key": "workspace_name",
+ "label": "Workspace name"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
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..38f69fef50
--- /dev/null
+++ b/pype/tools/settings/settings/style/style.css
@@ -0,0 +1,315 @@
+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;
+}
+
+#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..dbcc380daf
--- /dev/null
+++ b/pype/tools/settings/settings/widgets/base.py
@@ -0,0 +1,735 @@
+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
+ 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
+ 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..e2d59c2e69
--- /dev/null
+++ b/pype/tools/settings/settings/widgets/item_types.py
@@ -0,0 +1,3049 @@
+import json
+import logging
+import collections
+from Qt import QtWidgets, QtCore, QtGui
+from .widgets import (
+ ExpandingWidget,
+ NumberSpinBox,
+ PathInput
+)
+from .lib import NOT_SET, METADATA_KEY, TypeToKlass, CHILD_OFFSET
+from avalon.vendor import qtawesome
+
+
+class SettingObject:
+ # `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)
+ allow_actions = True
+ # default state of item type
+ default_state = ""
+
+ @classmethod
+ def style_state(cls, 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")
+ return "-".join(items) or cls.default_state
+
+ 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_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_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.
+
+ 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
+ has changes (not just value change).
+
+ Returns:
+ bool
+
+ """
+ return self._has_studio_override or self._parent.has_studio_override
+
+ @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._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):
+ """Should 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
+ ):
+ 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 cls.default_state
+
+ def mouseReleaseEvent(self, event):
+ if self.allow_actions and event.button() == QtCore.Qt.RightButton:
+ 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()
+ return
+
+ 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):
+ 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):
+ 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):
+ 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):
+ 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):
+ raise NotImplementedError(
+ "{} Method `set_as_overriden` not implemented!".format(repr(self))
+ )
+
+ def hierarchical_style_update(self):
+ raise NotImplementedError(
+ "{} Method `hierarchical_style_update` not implemented!".format(
+ repr(self)
+ )
+ )
+
+ def update_default_values(self, parent_values):
+ raise NotImplementedError(
+ "{} does not have implemented `update_default_values`".format(self)
+ )
+
+ def update_studio_values(self, parent_values):
+ raise NotImplementedError(
+ "{} does not have implemented `update_studio_values`".format(self)
+ )
+
+ def apply_overrides(self, parent_values):
+ raise NotImplementedError(
+ "{} does not have implemented `apply_overrides`".format(self)
+ )
+
+ @property
+ def child_has_studio_override(self):
+ """Any children item is modified."""
+ 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 is overriden."""
+ 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!"
+ )
+
+ def studio_value(self):
+ """Output for saving changes or overrides."""
+ return {self.key: self.item_value()}
+
+
+class InputObject(SettingObject):
+ 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:
+ 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."
+ )
+
+ 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 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.has_studio_override:
+ return NOT_SET, False
+ return self.config_value(), self.is_group
+
+ def overrides(self):
+ if 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):
+ 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)
+
+ 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)
+
+ 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):
+ super(ListItem, self).__init__(parent)
+
+ self._set_default_attributes()
+
+ self._parent = config_parent
+ self._any_parent_is_group = True
+
+ 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")
+
+ 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")
+
+ layout.addWidget(self.add_btn, 0)
+ layout.addWidget(self.remove_btn, 0)
+
+ 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)
+
+ ItemKlass = TypeToKlass.types[object_type]
+ self.value_input = ItemKlass(
+ input_modifiers,
+ self,
+ as_widget=True,
+ label_widget=None
+ )
+ layout.addWidget(self.value_input, 1)
+
+ layout.addWidget(self.up_btn, 0)
+ layout.addWidget(self.down_btn, 0)
+
+ self.value_input.value_changed.connect(self._on_value_change)
+
+ def set_as_empty(self, is_empty=True):
+ self.value_input.setEnabled(not is_empty)
+ self.remove_btn.setEnabled(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.setEnabled(False)
+ self.down_btn.setEnabled(False)
+
+ elif 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.value_input.isEnabled():
+ self._parent.add_row(row=self.row() + 1)
+ else:
+ self.set_as_empty(False)
+
+ 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 self.value_input.isEnabled():
+ 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.key = input_data["key"]
+
+ self.input_fields = []
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 5)
+ layout.setSpacing(5)
+
+ 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
+ )
+ if row is None:
+ if self.input_fields:
+ self.input_fields[-1].order_changed()
+ self.inputs_layout.addWidget(item_widget)
+ self.input_fields.append(item_widget)
+ else:
+ previous_field = None
+ if row > 0:
+ previous_field = self.input_fields[row - 1]
+
+ next_field = None
+ 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
+
+ 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 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
+
+ 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")
+
+ layout.addWidget(self.add_btn, 0)
+ layout.addWidget(self.remove_btn, 0)
+ layout.addWidget(self.key_input, 0)
+ 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_enabled(self):
+ return self.key_input.isEnabled()
+
+ def is_key_invalid(self):
+ if not self._is_enabled():
+ 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_enabled():
+ self._parent.add_row(row=self.row() + 1)
+ else:
+ self.set_as_empty(False)
+
+ def on_remove_clicked(self):
+ self._parent.remove_row(self)
+
+ def set_as_empty(self, is_empty=True):
+ self.key_input.setEnabled(not is_empty)
+ self.value_input.setEnabled(not is_empty)
+ self.remove_btn.setEnabled(not 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 not self._is_enabled():
+ return False
+ return self.is_key_invalid() or self.value_input.is_invalid
+
+ def update_style(self):
+ state = ""
+ if self._is_enabled():
+ 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_enabled():
+ return self.item_value()
+ return {}
+
+ 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)
+
+ 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"]
+
+ main_layout = QtWidgets.QHBoxLayout(self)
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.setSpacing(0)
+
+ content_widget = QtWidgets.QWidget(self)
+ content_layout = QtWidgets.QVBoxLayout(content_widget)
+ content_layout.setContentsMargins(CHILD_OFFSET, 3, 0, 3)
+
+ if as_widget:
+ main_layout.addWidget(content_widget)
+ body_widget = None
+ else:
+ body_widget = ExpandingWidget(input_data["label"], self)
+ main_layout.addWidget(body_widget)
+ body_widget.set_content_widget(content_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)
+
+ 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)
+
+ def __init__(
+ self, input_data, parent,
+ as_widget=False, label_widget=None, parent_widget=None
+ ):
+ if as_widget:
+ raise TypeError("Can't use \"{}\" as widget item.".format(
+ self.__class__.__name__
+ ))
+
+ if parent_widget is None:
+ parent_widget = parent
+ super(DictWidget, self).__init__(parent_widget)
+ self.setObjectName("DictWidget")
+
+ self.initial_attributes(input_data, parent, as_widget)
+
+ if input_data.get("highlight_content", False):
+ content_state = "hightlighted"
+ bottom_margin = 5
+ else:
+ content_state = ""
+ bottom_margin = 0
+
+ self.input_fields = []
+
+ self.key = input_data["key"]
+
+ 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.QVBoxLayout(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
+
+ self.checkbox_widget = None
+ self.checkbox_key = input_data.get("checkbox_key")
+
+ 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 add_children_gui(self, child_configuration):
+ item_type = child_configuration["type"]
+ klass = TypeToKlass.types.get(item_type)
+
+ if not klass.is_input_type:
+ item = klass(child_configuration, self)
+ self.content_layout.addWidget(item)
+ 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)
+
+ item = klass(child_configuration, self)
+ item.value_changed.connect(self._on_value_change)
+ self.content_layout.addWidget(item)
+
+ 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):
+ 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)
+
+ 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
+
+ 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 _on_value_change(self, item=None):
+ if self.ignore_value_changes:
+ return
+
+ if self.is_group:
+ if self.is_overidable:
+ self._is_overriden = True
+ else:
+ self._has_studio_override = True
+
+ 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):
+ 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.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
+
+ 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.QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(5)
+
+ 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)
+
+ if not klass.is_input_type:
+ item = klass(child_configuration, self)
+ self.layout().addWidget(item)
+ return item
+
+ item = klass(child_configuration, self)
+ self.layout().addWidget(item)
+
+ item.value_changed.connect(self._on_value_change)
+
+ 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:
+ 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.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."
+ )
+
+ 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 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.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
+
+ 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.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["dict-modifiable"] = ModifiableDict
+TypeToKlass.types["dict"] = DictWidget
+TypeToKlass.types["dict-invisible"] = DictInvisible
+TypeToKlass.types["path-widget"] = PathWidget
+TypeToKlass.types["dict-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..e225d65417
--- /dev/null
+++ b/pype/tools/settings/settings/widgets/lib.py
@@ -0,0 +1,311 @@
+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
+
+ for schema_name in child["children"]:
+ new_child = _fill_inner_schemas(
+ schema_collection[schema_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..400b9371fd
--- /dev/null
+++ b/pype/tools/settings/settings/widgets/widgets.py
@@ -0,0 +1,228 @@
+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)
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/tray/modules_imports.json b/pype/tools/tray/modules_imports.json
index e9526dcddb..6a278840ea 100644
--- a/pype/tools/tray/modules_imports.json
+++ b/pype/tools/tray/modules_imports.json
@@ -54,5 +54,10 @@
"type": "module",
"import_path": "pype.modules.adobe_communicator",
"fromlist": ["pype", "modules"]
+ }, {
+ "title": "Websocket Server",
+ "type": "module",
+ "import_path": "pype.modules.websocket_server",
+ "fromlist": ["pype", "modules"]
}
]