diff --git a/pype/settings/defaults/project_settings/plugins/global/publish.json b/pype/settings/defaults/project_settings/plugins/global/publish.json index b946ac4b32..0a7f6fbf3d 100644 --- a/pype/settings/defaults/project_settings/plugins/global/publish.json +++ b/pype/settings/defaults/project_settings/plugins/global/publish.json @@ -19,30 +19,30 @@ "hosts": [], "outputs": { "h264": { - "filter": { - "families": [ - "render", - "review", - "ftrack" - ] - }, "ext": "mp4", + "tags": [ + "burnin", + "ftrackreview" + ], "ffmpeg_args": { + "video_filters": [], + "audio_filters": [], "input": [ "-gamma 2.2" ], - "video_filters": [], - "audio_filters": [], "output": [ "-pix_fmt yuv420p", "-crf 18", "-intra" ] }, - "tags": [ - "burnin", - "ftrackreview" - ] + "filter": { + "families": [ + "render", + "review", + "ftrack" + ] + } } } } diff --git a/pype/settings/defaults/system_settings/global/applications.json b/pype/settings/defaults/system_settings/global/applications.json index 3a74a85468..e85e5864d9 100644 --- a/pype/settings/defaults/system_settings/global/applications.json +++ b/pype/settings/defaults/system_settings/global/applications.json @@ -1,32 +1,32 @@ { - "blender_2.80": true, - "blender_2.81": true, - "blender_2.82": true, + "blender_2.80": false, + "blender_2.81": false, + "blender_2.82": false, "blender_2.83": true, "celaction_local": true, "celaction_remote": true, "harmony_17": true, - "maya_2017": true, - "maya_2018": true, + "maya_2017": false, + "maya_2018": false, "maya_2019": true, "maya_2020": true, - "nuke_10.0": true, - "nuke_11.2": true, + "nuke_10.0": false, + "nuke_11.2": false, "nuke_11.3": true, "nuke_12.0": true, - "nukex_10.0": true, - "nukex_11.2": true, + "nukex_10.0": false, + "nukex_11.2": false, "nukex_11.3": true, "nukex_12.0": true, - "nukestudio_10.0": true, - "nukestudio_11.2": true, + "nukestudio_10.0": false, + "nukestudio_11.2": false, "nukestudio_11.3": true, "nukestudio_12.0": true, - "houdini_16": true, + "houdini_16": false, "houdini_16.5": false, - "houdini_17": true, + "houdini_17": false, "houdini_18": true, - "premiere_2019": true, + "premiere_2019": false, "premiere_2020": true, "resolve_16": true, "storyboardpro_7": true, diff --git a/pype/settings/defaults/system_settings/global/general.json b/pype/settings/defaults/system_settings/global/general.json new file mode 100644 index 0000000000..bd501b06eb --- /dev/null +++ b/pype/settings/defaults/system_settings/global/general.json @@ -0,0 +1,4 @@ +{ + "studio_name": "", + "studio_code": "" +} \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/global/modules.json b/pype/settings/defaults/system_settings/global/modules.json new file mode 100644 index 0000000000..9bd46602cf --- /dev/null +++ b/pype/settings/defaults/system_settings/global/modules.json @@ -0,0 +1,88 @@ +{ + "Avalon": { + "AVALON_MONGO": "mongodb://localhost:2707", + "AVALON_DB_DATA": "{PYPE_SETUP_PATH}/../mongo_db_data", + "AVALON_THUMBNAIL_ROOT": "{PYPE_SETUP_PATH}/../avalon_thumails" + }, + "Ftrack": { + "enabled": true, + "ftrack_server": "https://pype.ftrackapp.com", + "ftrack_actions_path": [], + "ftrack_events_path": [], + "FTRACK_EVENTS_MONGO_DB": "pype", + "FTRACK_EVENTS_MONGO_COL": "ftrack_events", + "sync_to_avalon": { + "statuses_name_change": [ + "ready", + "not ready" + ] + }, + "status_version_to_task": {}, + "status_update": { + "Ready": [ + "Not Ready" + ], + "In Progress": [ + "_any_" + ] + }, + "intent": { + "items": { + "-": "-", + "wip": "WIP", + "final": "Final", + "test": "Test" + }, + "default": "-" + } + }, + "Rest Api": { + "default_port": 8021, + "exclude_ports": [] + }, + "Timers Manager": { + "enabled": true, + "full_time": 15.0, + "message_time": 0.5 + }, + "Clockify": { + "enabled": true, + "workspace_name": "studio name" + }, + "Deadline": { + "enabled": true, + "DEADLINE_REST_URL": "http://localhost:8082" + }, + "Muster": { + "enabled": false, + "MUSTER_REST_URL": "", + "templates_mapping": { + "file_layers": 7, + "mentalray": 2, + "mentalray_sf": 6, + "redshift": 55, + "renderman": 29, + "software": 1, + "software_sf": 5, + "turtle": 10, + "vector": 4, + "vray": 37, + "ffmpeg": 48 + } + }, + "Logging": { + "enabled": true + }, + "Adobe Communicator": { + "enabled": true + }, + "User setting": { + "enabled": true + }, + "Standalone Publish": { + "enabled": true + }, + "Idle Manager": { + "enabled": true + } +} \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/global/tools.json b/pype/settings/defaults/system_settings/global/tools.json index 93895c0e81..1f8c2ad1ea 100644 --- a/pype/settings/defaults/system_settings/global/tools.json +++ b/pype/settings/defaults/system_settings/global/tools.json @@ -1,6 +1,6 @@ { - "mtoa_3.0.1": true, - "mtoa_3.1.1": true, + "mtoa_3.0.1": false, + "mtoa_3.1.1": false, "mtoa_3.2.0": true, "yeti_2.1.2": true } \ No newline at end of file diff --git a/pype/settings/defaults/system_settings/global/tray_modules.json b/pype/settings/defaults/system_settings/global/tray_modules.json deleted file mode 100644 index 0ff5b15552..0000000000 --- a/pype/settings/defaults/system_settings/global/tray_modules.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "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/tools/settings/settings/README.md b/pype/tools/settings/settings/README.md new file mode 100644 index 0000000000..47bbf28ba5 --- /dev/null +++ b/pype/tools/settings/settings/README.md @@ -0,0 +1,336 @@ +# Creating GUI schemas + +## Basic rules +- configurations does not define GUI, but GUI defines configurations! +- output is always json (yaml is not needed for anatomy templates anymore) +- GUI schema has multiple input types, all inputs are represented by a dictionary +- each input may have "input modifiers" (keys in dictionary) that are required or optional + - only required modifier for all input items is key `"type"` which says what type of item it is +- there are special keys across all inputs + - `"is_file"` - this key is for storing pype defaults in `pype` repo + - reasons of existence: developing new schemas does not require to create defaults manually + - key is validated, must be once in hierarchy else it won't be possible to store pype defaults + - `"is_group"` - define that all values under key in hierarchy will be overriden if any value is modified, this information is also stored to overrides + - this keys is not allowed for all inputs as they may have not reason for that + - key is validated, can be only once in hierarchy but is not required +- currently there are `system configurations` and `project configurations` + +## Inner schema +- GUI schemas are huge json files, to be able to split whole configuration into multiple schema there's type `schema` +- system configuration schemas are stored in `~/tools/settings/settings/gui_schemas/system_schema/` and project configurations in `~/tools/settings/settings/gui_schemas/projects_schema/` +- each schema name is filename of json file except extension (without ".json") + +### schema +- can have only key `"children"` which is list of strings, each string should represent another schema (order matters) string represebts name of the schema +- will just paste schemas from other schema file in order of "children" list + +``` +{ + "type": "schema", + "name": "my_schema_name" +} +``` + +## Basic Dictionary inputs +- these inputs wraps another inputs into {key: value} relation + +### dict-invisible +- this input gives ability to wrap another inputs but keep them in same widget without visible divider + - this is for example used as first input widget +- has required keys `"key"` and `"children"` + - "children" says what children inputs are underneath + - "key" is key under which will be stored value from it's children +- output is dictionary `{the "key": children values}` +- can't have `"is_group"` key set to True as it breaks visual override showing +``` +{ + "type": "dict-invisible", + "key": "global", + "children": [ + ...ITEMS... + ] +} +``` + +## dict +- this is another dictionary input wrapping more inputs but visually makes them different +- item may be used as widget (in `list` or `dict-modifiable`) + - in that case the only key modifier is `children` which is list of it's keys + - USAGE: e.g. List of dictionaries where each dictionary have same structure. +- item options if is not used as widget + - required keys are `"key"` under which will be stored and `"label"` which will be shown in GUI + - this input can be expandable + - that can be set with key `"expandable"` as `True`/`False` (Default: `True`) + - with key `"expanded"` as `True`/`False` can be set that is expanded when GUI is opened (Default: `False`) + - it is possible to add darker background with `"highlight_content"` (Default: `False`) + - darker background has limits of maximum applies after 3-4 nested highlighted items there is not difference in the color +``` +# Example +{ + "key": "applications", + "type": "dict", + "label": "Applications", + "expandable": true, + "highlight_content": true, + "is_group": true, + "is_file": true, + "children": [ + ...ITEMS... + ] +} + +# When used as widget +{ + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": "dict-item", + "input_modifiers": { + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, { + "key": "hosts", + "label": "Hosts", + "type": "list", + "object_type": "text" + } + ... + ] + } +} +``` + +## Inputs for setting any kind of value (`Pure` inputs) +- all these input must have defined `"key"` under which will be stored and `"label"` which will be shown next to input + - unless they are used in different types of inputs (later) "as widgets" in that case `"key"` and `"label"` are not required as there is not place where to set them + +### boolean +- simple checkbox, nothing more to set +``` +{ + "type": "boolean", + "key": "my_boolean_key", + "label": "Do you want to use Pype?" +} +``` + +### number +- number input, can be used for both integer and float + - key `"decimal"` defines how many decimal places will be used, 0 is for integer input (Default: `0`) + - key `"minimum"` as minimum allowed number to enter (Default: `-99999`) + - key `"maxium"` as maximum allowed number to enter (Default: `99999`) +``` +{ + "type": "number", + "key": "fps", + "label": "Frame rate (FPS)" + "decimal": 2, + "minimum": 1, + "maximum": 300000 +} +``` + +### text +- simple text input + - key `"multiline"` allows to enter multiple lines of text (Default: `False`) + - key `"placeholder"` allows to show text inside input when is empty (Default: `None`) + +``` +{ + "type": "text", + "key": "deadline_pool", + "label": "Deadline pool" +} +``` + +### path-input +- enhanced text input + - does not allow to enter backslash, is auto-converted to forward slash + - may be added another validations, like do not allow end path with slash +- this input is implemented to add additional features to text input +- this is meant to be used in proxy input `path-widget` + - DO NOT USE this input in schema please + +### raw-json +- a little bit enhanced text input for raw json +- has validations of json format + - empty value is invalid value, always must be at least `{}` of `[]` + +``` +{ + "type": "raw-json", + "key": "profiles", + "label": "Extract Review profiles" +} +``` + +## Inputs for setting value using Pure inputs +- these inputs also have required `"key"` and `"label"` +- they use Pure inputs "as widgets" + +### list +- output is list +- items can be added and removed +- items in list must be the same type + - type of items is defined with key `"object_type"` where Pure input name is entered (e.g. `number`) + - because Pure inputs may have modifiers (`number` input has `minimum`, `maximum` and `decimals`) you can set these in key `"input_modifiers"` + +``` +{ + "type": "list", + "object_type": "number", + "key": "exclude_ports", + "label": "Exclude ports", + "input_modifiers": { + "minimum": 1, + "maximum": 65535 + } +} +``` + +### dict-modifiable +- one of dictionary inputs, this is only used as value input +- items in this input can be removed and added same way as in `list` input +- value items in dictionary must be the same type + - type of items is defined with key `"object_type"` where Pure input name is entered (e.g. `number`) + - because Pure inputs may have modifiers (`number` input has `minimum`, `maximum` and `decimals`) you can set these in key `"input_modifiers"` +- this input can be expandable + - that can be set with key `"expandable"` as `True`/`False` (Default: `True`) + - with key `"expanded"` as `True`/`False` can be set that is expanded when GUI is opened (Default: `False`) + +``` +{ + "type": "dict-modifiable", + "object_type": "number", + "input_modifiers": { + "minimum": 0, + "maximum": 300 + }, + "is_group": true, + "key": "templates_mapping", + "label": "Muster - Templates mapping", + "is_file": true +} +``` + +### path-widget +- input for paths, use `path-input` internally +- has 2 input modifiers `"multiplatform"` and `"multipath"` + - `"multiplatform"` - adds `"windows"`, `"linux"` and `"darwin"` path inputs result is dictionary + - `"multipath"` - it is possible to enter multiple paths + - if both are enabled result is dictionary with lists + +``` +{ + "type": "path-widget", + "key": "ffmpeg_path", + "label": "FFmpeg path", + "multiplatform": true, + "multipath": true +} +``` + +### list-strict +- input for strict number of items in list +- each child item can be different type with different possible modifiers +- it is possible to display them in horizontal or vertical layout + - key `"horizontal"` as `True`/`False` (Default: `True`) +- each child may have defined `"label"` which is shown next to input + - label does not reflect modifications or overrides (TODO) +- children item are defined under key `"object_types"` which is list of dictionaries + - key `"children"` is not used because is used for hierarchy validations in schema +- USAGE: For colors, transformations, etc. Custom number and different modifiers + give ability to define if color is HUE or RGB, 0-255, 0-1, 0-100 etc. + +``` +{ + "type": "list-strict", + "key": "color", + "label": "Color", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Alpha", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] +} +``` + + +## Noninteractive widgets +- have nothing to do with data + +### label +- add label with note or explanations +- it is possible to use html tags inside the label + +``` +{ + "type": "label", + "label": "RED LABEL: Normal label" +} +``` + +### splitter +- visual splitter of items (more divider than splitter) + +``` +{ + "type": "splitter" +} +``` + +## Proxy wrappers +- should wraps multiple inputs only visually +- these does not have `"key"` key and do not allow to have `"is_file"` or `"is_group"` modifiers enabled + +### form +- DEPRECATED + - may be used only in `dict` and `dict-invisible` where is currently used grid layout so form is not needed + - item is kept as still may be used in specific cases +- wraps inputs into form look layout +- should be used only for Pure inputs + +``` +{ + "type": "dict-form", + "children": [ + { + "type": "text", + "key": "deadline_department", + "label": "Deadline apartment" + }, { + "type": "number", + "key": "deadline_priority", + "label": "Deadline priority" + }, { + ... + } + ] +} +``` diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/0_project_gui_schema.json b/pype/tools/settings/settings/gui_schemas/projects_schema/0_project_gui_schema.json index fa7c6a366d..cf95bf4c45 100644 --- 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 @@ -22,9 +22,7 @@ "children": [ { "type": "schema", - "children": [ - "1_plugins_gui_schema" - ] + "name": "1_plugins_gui_schema" } ] } diff --git a/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json b/pype/tools/settings/settings/gui_schemas/projects_schema/1_plugins_gui_schema.json index 6bb14463c1..f357b51dc5 100644 --- 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 @@ -30,34 +30,29 @@ "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": "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" } ] } @@ -174,9 +169,94 @@ "key": "enabled", "label": "Enabled" }, { - "type": "raw-json", + "type": "list", "key": "profiles", - "label": "Profiles" + "label": "Profiles", + "object_type": "dict", + "input_modifiers": { + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, { + "key": "hosts", + "label": "Hosts", + "type": "list", + "object_type": "text" + }, { + "type": "splitter" + }, { + "key": "outputs", + "label": "Output Definitions", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": "dict", + "input_modifiers": { + "children": [ + { + "key": "ext", + "label": "Output extension", + "type": "text" + }, { + "key": "tags", + "label": "Tags", + "type": "list", + "object_type": "text" + }, { + "key": "ffmpeg_args", + "label": "FFmpeg arguments", + "type": "dict", + "highlight_content": true, + "children": [ + { + "key": "video_filters", + "label": "Video filters", + "type": "list", + "object_type": "text" + }, { + "type": "splitter" + }, { + "key": "audio_filters", + "label": "Audio filters", + "type": "list", + "object_type": "text" + }, { + "type": "splitter" + }, { + "key": "input", + "label": "Input arguments", + "type": "list", + "object_type": "text" + }, { + "type": "splitter" + }, { + "key": "output", + "label": "Output arguments", + "type": "list", + "object_type": "text" + } + ] + }, { + "key": "filter", + "label": "Additional output filtering", + "type": "dict", + "highlight_content": true, + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + } + ] + } + ] + } + } + ] + } } ] }, { @@ -614,6 +694,52 @@ ] } ] + }, + { + "type": "dict", + "collapsable": true, + "key": "standalonepublisher", + "label": "Standalone Publisher", + "children": [ + { + "type": "dict", + "collapsable": true, + "key": "publish", + "label": "Publish plugins", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsable": true, + "key": "ExtractThumbnailSP", + "label": "ExtractThumbnailSP", + "is_group": true, + "children": [ + { + "type": "dict", + "collapsable": false, + "key": "ffmpeg_args", + "label": "ffmpeg_args", + "children": [ + { + "type": "list", + "object_type": "text", + "key": "input", + "label": "input" + }, + { + "type": "list", + "object_type": "text", + "key": "output", + "label": "output" + } + ] + } + ] + } + ] + } + ] } ] } diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/0_system_gui_schema.json index b16545111c..c5f229fc2f 100644 --- 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 @@ -7,27 +7,16 @@ "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 + "name": "1_intents_gui_schema" + },{ + "type": "schema", + "name": "1_modules_gui_schema" + }, { + "type": "schema", + "name": "1_applications_gui_schema" + }, { + "type": "schema", + "name": "1_tools_gui_schema" }] } ] diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_applications_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_applications_gui_schema.json index 48f8ecbd7c..3427f98253 100644 --- 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 @@ -7,138 +7,133 @@ "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" - } - ] + "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 index a884dcb31e..0578968508 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_examples.json @@ -72,6 +72,144 @@ "minimum": 10, "maximum": 100 } + }, { + "type": "list-strict", + "key": "strict_list_labels_horizontal", + "label": "StrictList-labels-horizontal (color)", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Alpha", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] + }, { + "type": "list-strict", + "key": "strict_list_labels_vertical", + "label": "StrictList-labels-vertical (color)", + "horizontal": false, + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "label": "Alpha", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] + }, { + "type": "list-strict", + "key": "strict_list_nolabels_horizontal", + "label": "StrictList-nolabels-horizontal (color)", + "object_types": [ + { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] + }, { + "type": "list-strict", + "key": "strict_list_nolabels_vertical", + "label": "StrictList-nolabels-vertical (color)", + "horizontal": false, + "object_types": [ + { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 255, + "decimal": 0 + }, { + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 6 + } + ] + }, { + "type": "list", + "key": "dict_item", + "label": "DictItem in List", + "object_type": "dict-item", + "input_modifiers": { + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, { + "key": "hosts", + "label": "Hosts", + "type": "list", + "object_type": "text" + } + ] + } }, { "type": "path-widget", "key": "single_path_input", @@ -207,7 +345,7 @@ "label": "Inputs with form", "children": [ { - "type": "dict-form", + "type": "form", "children": [ { "type": "text", 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 index 0c252d2ca9..7f71da26cd 100644 --- 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 @@ -1,20 +1,16 @@ { - "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" - } - ] -} + "key": "general", + "type": "dict", + "label": "General", + "collapsable": true, + "is_file": true, + "children": [{ + "key": "studio_name", + "type": "text", + "label": "Studio Name" + },{ + "key": "studio_code", + "type": "text", + "label": "Studio Short Code" + } +]} diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_modules_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_modules_gui_schema.json new file mode 100644 index 0000000000..7b3d8cdd13 --- /dev/null +++ b/pype/tools/settings/settings/gui_schemas/system_schema/1_modules_gui_schema.json @@ -0,0 +1,283 @@ +{ + "key": "modules", + "type": "dict", + "label": "Modules", + "collapsable": true, + "is_file": true, + "children": [{ + "type": "dict", + "key": "Avalon", + "label": "Avalon", + "collapsable": true, + "children": [ + { + "type": "text", + "key": "AVALON_MONGO", + "label": "Avalon Mongo URL" + }, + { + "type": "text", + "key": "AVALON_DB_DATA", + "label": "Avalon Mongo Data Location" + }, + { + "type": "text", + "key": "AVALON_THUMBNAIL_ROOT", + "label": "Thumbnail Storage Location" + } + ] + },{ + "type": "dict", + "key": "Ftrack", + "label": "Ftrack", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "ftrack_server", + "label": "Server" + }, + { + "type": "label", + "label": "Additional Ftrack paths" + }, + { + "type": "list", + "key": "ftrack_actions_path", + "label": "Action paths", + "object_type": "text" + }, + { + "type": "list", + "key": "ftrack_events_path", + "label": "Event paths", + "object_type": "text" + }, + { + "type": "label", + "label": "Ftrack event server advanced settings" + }, + { + "type": "text", + "key": "FTRACK_EVENTS_MONGO_DB", + "label": "Event Mongo DB" + }, + { + "type": "text", + "key": "FTRACK_EVENTS_MONGO_COL", + "label": "Events Mongo Collection" + }, + { + "type": "dict", + "key": "sync_to_avalon", + "label": "Sync to avalon", + "children": [{ + "type": "list", + "key": "statuses_name_change", + "label": "Status name change", + "object_type": "text", + "input_modifiers": { + "multiline": false + } + }] + }, + { + "type": "dict-modifiable", + "key": "status_version_to_task", + "label": "Version to Task status mapping", + "object_type": "text" + }, + { + "type": "dict-modifiable", + "key": "status_update", + "label": "Status Updates", + "object_type": "list", + "input_modifiers": { + "object_type": "text" + } + }, + { + "key": "intent", + "type": "dict-invisible", + "children": [ + { + "type": "dict-modifiable", + "object_type": "text", + "key": "items", + "label": "Intent Key/Label" + }, + { + "key": "default", + "type": "text", + "label": "Defautl Intent" + } + ] + } + ] + }, { + "type": "dict", + "key": "Rest Api", + "label": "Rest Api", + "collapsable": true, + "children": [{ + "type": "number", + "key": "default_port", + "label": "Default Port", + "minimum": 1, + "maximum": 65535 + }, + { + "type": "list", + "object_type": "number", + "key": "exclude_ports", + "label": "Exclude ports", + "input_modifiers": { + "minimum": 1, + "maximum": 65535 + } + } + ] + }, { + "type": "dict", + "key": "Timers Manager", + "label": "Timers Manager", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "number", + "decimal": 2, + "key": "full_time", + "label": "Max idle time" + }, { + "type": "number", + "decimal": 2, + "key": "message_time", + "label": "When dialog will show" + } + ] + }, { + "type": "dict", + "key": "Clockify", + "label": "Clockify", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "workspace_name", + "label": "Workspace name" + } + ] + }, { + "type": "dict", + "key": "Deadline", + "label": "Deadline", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + },{ + "type": "text", + "key": "DEADLINE_REST_URL", + "label": "Deadline Resl URL" + }] + }, { + "type": "dict", + "key": "Muster", + "label": "Muster", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + },{ + "type": "text", + "key": "MUSTER_REST_URL", + "label": "Muster Resl URL" + },{ + "type": "dict-modifiable", + "object_type": "number", + "input_modifiers": { + "minimum": 0, + "maximum": 300 + }, + "is_group": true, + "key": "templates_mapping", + "label": "Templates mapping", + "is_file": true + }] + }, { + "type": "dict", + "key": "Logging", + "label": "Logging", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }] + }, { + "type": "dict", + "key": "Adobe Communicator", + "label": "Adobe Communicator", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }] + }, { + "type": "dict", + "key": "User setting", + "label": "User setting", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }] + }, { + "type": "dict", + "key": "Standalone Publish", + "label": "Standalone Publish", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }] + }, { + "type": "dict", + "key": "Idle Manager", + "label": "Idle Manager", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }] + } + ] +} diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/1_tools_gui_schema.json b/pype/tools/settings/settings/gui_schemas/system_schema/1_tools_gui_schema.json index d9540eeb3e..08b8d13d89 100644 --- 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 @@ -7,26 +7,21 @@ "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" - } - ] + "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 deleted file mode 100644 index 6da974a415..0000000000 --- a/pype/tools/settings/settings/gui_schemas/system_schema/1_tray_items.json +++ /dev/null @@ -1,125 +0,0 @@ -{ - "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/style.css b/pype/tools/settings/settings/style/style.css index 38f69fef50..3f648abef8 100644 --- a/pype/tools/settings/settings/style/style.css +++ b/pype/tools/settings/settings/style/style.css @@ -152,6 +152,12 @@ QPushButton[btn-type="expand-toggle"] { background: #141a1f; } +#DictAsWidgetBody{ + background: transparent; + border: 2px solid #cccccc; + border-radius: 5px; +} + #SplitterItem { background-color: #1d272f; } diff --git a/pype/tools/settings/settings/widgets/base.py b/pype/tools/settings/settings/widgets/base.py index dbcc380daf..423380d54c 100644 --- a/pype/tools/settings/settings/widgets/base.py +++ b/pype/tools/settings/settings/widgets/base.py @@ -34,6 +34,8 @@ class SystemWidget(QtWidgets.QWidget): is_overidable = False has_studio_override = _has_studio_override = False is_overriden = _is_overriden = False + as_widget = _as_widget = False + any_parent_as_widget = _any_parent_as_widget = False is_group = _is_group = False any_parent_is_group = _any_parent_is_group = False @@ -396,6 +398,8 @@ class ProjectListWidget(QtWidgets.QWidget): class ProjectWidget(QtWidgets.QWidget): has_studio_override = _has_studio_override = False is_overriden = _is_overriden = False + as_widget = _as_widget = False + any_parent_as_widget = _any_parent_as_widget = False is_group = _is_group = False any_parent_is_group = _any_parent_is_group = False diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index e2d59c2e69..e589b89c0f 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -5,35 +5,28 @@ from Qt import QtWidgets, QtCore, QtGui from .widgets import ( ExpandingWidget, NumberSpinBox, - PathInput + PathInput, + GridLabelWidget ) from .lib import NOT_SET, METADATA_KEY, TypeToKlass, CHILD_OFFSET from avalon.vendor import qtawesome class SettingObject: + """Partially abstract class for Setting's item type workflow.""" # `is_input_type` attribute says if has implemented item type methods is_input_type = True - # each input must have implemented default value for development - # when defaults are not filled yet + # 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) + # Will allow to show actions for the item type (disabled for proxies) else + # item is skipped and try to trigger actions on it's parent. allow_actions = True - # 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 + # All item types must have implemented Qt signal which is emitted when + # it's or it's children value has changed, + value_changed = None + # Item will expand to full width in grid layout + expand_in_grid = False def _set_default_attributes(self): """Create and reset attributes required for all item types. @@ -54,6 +47,7 @@ class SettingObject: self._as_widget = False self._is_group = False + self._any_parent_as_widget = None self._any_parent_is_group = None # Parent input @@ -89,6 +83,12 @@ class SettingObject: # TODO not implemented yet self._is_nullable = input_data.get("is_nullable", False) + any_parent_as_widget = parent.as_widget + if not any_parent_as_widget: + any_parent_as_widget = parent.any_parent_as_widget + + self._any_parent_as_widget = any_parent_as_widget + any_parent_is_group = parent.is_group if not any_parent_is_group: any_parent_is_group = parent.any_parent_is_group @@ -116,6 +116,9 @@ class SettingObject: def had_studio_override(self): """Item had studio overrides on refresh. + Use attribute `_had_studio_override` which should be changed only + during methods `update_studio_values` and `update_default_values`. + Returns: bool @@ -127,7 +130,7 @@ class SettingObject: """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). + is modified (not value change). Returns: bool @@ -135,6 +138,34 @@ class SettingObject: """ return self._has_studio_override or self._parent.has_studio_override + @property + def as_widget(self): + """Item is used as widget in parent item. + + Returns: + bool + + """ + return self._as_widget + + @property + def any_parent_as_widget(self): + """Any parent of item is used as widget. + + Attribute holding this information is set during creation and + stored to `_any_parent_as_widget`. + + Why is this information useful: If any parent is used as widget then + modifications and override are not important for whole part. + + Returns: + bool + + """ + if self._any_parent_as_widget is None: + return super(SettingObject, self).any_parent_as_widget + return self._any_parent_as_widget + @property def is_group(self): """Item represents key that can be overriden. @@ -168,6 +199,9 @@ class SettingObject: @property def is_modified(self): """Has object any changes that require saving.""" + if self.any_parent_as_widget: + return self._is_modified + if self._is_modified or self.defaults_not_set: return True @@ -203,7 +237,8 @@ class SettingObject: @property def is_overidable(self): - """Should care about overrides.""" + """ care about overrides.""" + return self._parent.is_overidable def any_parent_overriden(self): @@ -213,6 +248,7 @@ class SettingObject: bool """ + if self._parent._is_overriden: return True return self._parent.any_parent_overriden() @@ -235,6 +271,7 @@ class SettingObject: def style_state( cls, has_studio_override, is_invalid, is_overriden, is_modified ): + """Return stylesheet state by intered booleans.""" items = [] if is_invalid: items.append("invalid") @@ -247,67 +284,77 @@ class SettingObject: if not items and has_studio_override: items.append("studio") - return "-".join(items) or cls.default_state + return "-".join(items) or "" + + def show_actions_menu(self, event=None): + if event and event.button() != QtCore.Qt.RightButton: + return + + if not self.allow_actions: + if event: + return self.mouseReleaseEvent(event) + return + + menu = QtWidgets.QMenu() + + actions_mapping = {} + if self.child_modified: + action = QtWidgets.QAction("Discard changes") + actions_mapping[action] = self._discard_changes + menu.addAction(action) + + if ( + self.is_overidable + and not self.is_overriden + and not self.any_parent_is_group + ): + action = QtWidgets.QAction("Set project override") + actions_mapping[action] = self._set_as_overriden + menu.addAction(action) + + if ( + not self.is_overidable + and ( + self.has_studio_override + ) + ): + action = QtWidgets.QAction("Reset to pype default") + actions_mapping[action] = self._reset_to_pype_default + menu.addAction(action) + + if ( + not self.is_overidable + and not self.is_overriden + and not self.any_parent_is_group + and not self._had_studio_override + ): + action = QtWidgets.QAction("Set studio default") + actions_mapping[action] = self._set_studio_default + menu.addAction(action) + + if ( + not self.any_parent_overriden() + and (self.is_overriden or self.child_overriden) + ): + # TODO better label + action = QtWidgets.QAction("Remove project override") + actions_mapping[action] = self._remove_overrides + menu.addAction(action) + + if not actions_mapping: + action = QtWidgets.QAction("< No action >") + actions_mapping[action] = None + menu.addAction(action) + + result = menu.exec_(QtGui.QCursor.pos()) + if result: + to_run = actions_mapping[result] + if to_run: + to_run() def mouseReleaseEvent(self, event): if self.allow_actions and event.button() == QtCore.Qt.RightButton: - 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 + return self.show_actions_menu() mro = type(self).mro() index = mro.index(self.__class__) @@ -327,6 +374,15 @@ class SettingObject: self.ignore_value_changes = False def discard_changes(self): + """Item's implementation to discard all changes made by user. + + Reset all values to same values as had when opened GUI + or when changed project. + + Must not affect `had_studio_override` value or `was_overriden` + value. It must be marked that there are keys/values which are not in + defaults or overrides. + """ raise NotImplementedError( "{} Method `discard_changes` not implemented!".format( repr(self) @@ -339,6 +395,10 @@ class SettingObject: self.ignore_value_changes = False def set_studio_default(self): + """Item's implementation to set current values as studio's overrides. + + Mark item and it's children as they have studio overrides. + """ raise NotImplementedError( "{} Method `set_studio_default` not implemented!".format( repr(self) @@ -351,6 +411,11 @@ class SettingObject: self.ignore_value_changes = False def reset_to_pype_default(self): + """Item's implementation to remove studio overrides. + + Mark item as it does not have studio overrides unset studio + override values. + """ raise NotImplementedError( "{} Method `reset_to_pype_default` not implemented!".format( repr(self) @@ -363,6 +428,11 @@ class SettingObject: self.ignore_value_changes = False def remove_overrides(self): + """Item's implementation to remove project overrides. + + Mark item as does not have project overrides. Must not change + `was_overriden` attribute value. + """ raise NotImplementedError( "{} Method `remove_overrides` not implemented!".format( repr(self) @@ -375,11 +445,21 @@ class SettingObject: self.ignore_value_changes = False def set_as_overriden(self): + """Item's implementation to set values as overriden for project. + + Mark item and all it's children as they're overriden. Must skip + items with children items that has attributes `is_group` + and `any_parent_is_group` set to False. In that case those items + are not meant to be overridable and should trigger the method on it's + children. + + """ raise NotImplementedError( "{} Method `set_as_overriden` not implemented!".format(repr(self)) ) def hierarchical_style_update(self): + """Trigger update style method down the hierarchy.""" raise NotImplementedError( "{} Method `hierarchical_style_update` not implemented!".format( repr(self) @@ -387,23 +467,51 @@ class SettingObject: ) def update_default_values(self, parent_values): + """Fill default values on startup or on refresh. + + Default values stored in `pype` repository should update all items in + schema. Each item should take values for his key and set it's value or + pass values down to children items. + + Args: + parent_values (dict): Values of parent's item. But in case item is + used as widget, `parent_values` contain value for item. + """ raise NotImplementedError( "{} does not have implemented `update_default_values`".format(self) ) def update_studio_values(self, parent_values): + """Fill studio override values on startup or on refresh. + + Set studio value if is not set to NOT_SET, in that case studio + overrides are not set yet. + + Args: + parent_values (dict): Values of parent's item. But in case item is + used as widget, `parent_values` contain value for item. + """ raise NotImplementedError( "{} does not have implemented `update_studio_values`".format(self) ) def apply_overrides(self, parent_values): + """Fill project override values on startup, refresh or project change. + + Set project value if is not set to NOT_SET, in that case project + overrides are not set yet. + + Args: + parent_values (dict): Values of parent's item. But in case item is + used as widget, `parent_values` contain value for item. + """ raise NotImplementedError( "{} does not have implemented `apply_overrides`".format(self) ) @property def child_has_studio_override(self): - """Any children item is modified.""" + """Any children item has studio overrides.""" raise NotImplementedError( "{} does not have implemented `child_has_studio_override`".format( self @@ -419,7 +527,7 @@ class SettingObject: @property def child_overriden(self): - """Any children item is overriden.""" + """Any children item has project overrides.""" raise NotImplementedError( "{} does not have implemented `child_overriden`".format(self) ) @@ -443,12 +551,14 @@ class SettingObject: "Method `item_value` not implemented!" ) - def studio_value(self): - """Output for saving changes or overrides.""" - return {self.key: self.item_value()} - class InputObject(SettingObject): + """Class for inputs with pre-implemented methods. + + Class is for item types not creating or using other item types, most + of methods has same code in that case. + """ + def update_default_values(self, parent_values): self._state = None self._is_modified = False @@ -461,8 +571,8 @@ class InputObject(SettingObject): if value is NOT_SET: if self.develop_mode: - value = self.default_input_value self.defaults_not_set = True + value = self.default_input_value if value is NOT_SET: raise NotImplementedError(( "{} Does not have implemented" @@ -473,6 +583,8 @@ class InputObject(SettingObject): raise ValueError( "Default value is not set. This is implementation BUG." ) + else: + self.defaults_not_set = False self.default_value = value self._has_studio_override = False @@ -531,10 +643,11 @@ class InputObject(SettingObject): if self.ignore_value_changes: return - if self.is_overidable: - self._is_overriden = True - else: - self._has_studio_override = True + if not self.any_parent_as_widget: + if self.is_overidable: + self._is_overriden = True + else: + self._has_studio_override = True if self._is_invalid: self._is_modified = True @@ -550,12 +663,18 @@ class InputObject(SettingObject): self.value_changed.emit(self) def studio_overrides(self): - if not self.has_studio_override: + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.has_studio_override + ): return NOT_SET, False return self.config_value(), self.is_group def overrides(self): - if not self.is_overriden: + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.is_overriden + ): return NOT_SET, False return self.config_value(), self.is_group @@ -563,6 +682,8 @@ class InputObject(SettingObject): self.update_style() def remove_overrides(self): + self._is_overriden = False + self._is_modified = False if self.has_studio_override: self.set_value(self.studio_value) else: @@ -799,6 +920,7 @@ class TextWidget(QtWidgets.QWidget, InputObject): self.initial_attributes(input_data, parent, as_widget) self.multiline = input_data.get("multiline", False) + placeholder = input_data.get("placeholder") layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) @@ -809,6 +931,9 @@ class TextWidget(QtWidgets.QWidget, InputObject): else: self.text_input = QtWidgets.QLineEdit(self) + if placeholder: + self.text_input.setPlaceholderText(placeholder) + self.setFocusProxy(self.text_input) layout_kwargs = {} @@ -1089,13 +1214,19 @@ class ListItem(QtWidgets.QWidget, SettingObject): _btn_size = 20 value_changed = QtCore.Signal(object) - def __init__(self, object_type, input_modifiers, config_parent, parent): + def __init__( + self, object_type, input_modifiers, config_parent, parent, + is_strict=False + ): super(ListItem, self).__init__(parent) self._set_default_attributes() + self._is_strict = is_strict + self._parent = config_parent self._any_parent_is_group = True + self._is_empty = False layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) @@ -1104,37 +1235,38 @@ class ListItem(QtWidgets.QWidget, SettingObject): 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) + if not self._is_strict: + self.add_btn = QtWidgets.QPushButton("+") + self.remove_btn = QtWidgets.QPushButton("-") + self.up_btn = QtWidgets.QPushButton(char_up) + self.down_btn = QtWidgets.QPushButton(char_down) - font_up_down = qtawesome.font("fa", 13) - self.up_btn.setFont(font_up_down) - self.down_btn.setFont(font_up_down) + 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.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.setFixedSize(self._btn_size, self._btn_size) + self.remove_btn.setFixedSize(self._btn_size, self._btn_size) + self.up_btn.setFixedSize(self._btn_size, self._btn_size) + self.down_btn.setFixedSize(self._btn_size, self._btn_size) - self.add_btn.setProperty("btn-type", "tool-item") - self.remove_btn.setProperty("btn-type", "tool-item") - self.up_btn.setProperty("btn-type", "tool-item") - self.down_btn.setProperty("btn-type", "tool-item") + self.add_btn.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) - self.add_btn.clicked.connect(self._on_add_clicked) - self.remove_btn.clicked.connect(self._on_remove_clicked) - self.up_btn.clicked.connect(self._on_up_clicked) - self.down_btn.clicked.connect(self._on_down_clicked) + layout.addWidget(self.add_btn, 0) + layout.addWidget(self.remove_btn, 0) ItemKlass = TypeToKlass.types[object_type] self.value_input = ItemKlass( @@ -1143,16 +1275,37 @@ class ListItem(QtWidgets.QWidget, SettingObject): as_widget=True, label_widget=None ) + layout.addWidget(self.value_input, 1) - layout.addWidget(self.up_btn, 0) - layout.addWidget(self.down_btn, 0) + if not self._is_strict: + self.spacer_widget = QtWidgets.QWidget(self) + self.spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.spacer_widget.setVisible(False) + + layout.addWidget(self.spacer_widget, 1) + + layout.addWidget(self.up_btn, 0) + layout.addWidget(self.down_btn, 0) self.value_input.value_changed.connect(self._on_value_change) + @property + def as_widget(self): + return self._parent.as_widget + + @property + def any_parent_as_widget(self): + return self.as_widget or self._parent.any_parent_as_widget + def set_as_empty(self, is_empty=True): - self.value_input.setEnabled(not is_empty) + self._is_empty = is_empty + + self.spacer_widget.setVisible(is_empty) + self.value_input.setVisible(not is_empty) self.remove_btn.setEnabled(not is_empty) + self.up_btn.setVisible(not is_empty) + self.down_btn.setVisible(not is_empty) self.order_changed() self._on_value_change() @@ -1160,10 +1313,15 @@ class ListItem(QtWidgets.QWidget, SettingObject): 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) + self.up_btn.setVisible(False) + self.down_btn.setVisible(False) + return - elif row == 0: + if not self.up_btn.isVisible(): + self.up_btn.setVisible(True) + self.down_btn.setVisible(True) + + if row == 0: self.up_btn.setEnabled(False) self.down_btn.setEnabled(True) @@ -1185,10 +1343,10 @@ class ListItem(QtWidgets.QWidget, SettingObject): 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: + if self._is_empty: self.set_as_empty(False) + else: + self._parent.add_row(row=self.row() + 1) def _on_remove_clicked(self): self._parent.remove_row(self) @@ -1202,7 +1360,7 @@ class ListItem(QtWidgets.QWidget, SettingObject): self._parent.swap_rows(row, row + 1) def config_value(self): - if self.value_input.isEnabled(): + if not self._is_empty: return self.value_input.item_value() return NOT_SET @@ -1252,17 +1410,17 @@ class ListWidget(QtWidgets.QWidget, InputObject): 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) + if not self.as_widget: + self.key = input_data["key"] + if not label_widget: + label_widget = QtWidgets.QLabel(input_data["label"], self) + layout.addWidget(label_widget, alignment=QtCore.Qt.AlignTop) self.label_widget = label_widget @@ -1322,28 +1480,31 @@ class ListWidget(QtWidgets.QWidget, InputObject): item_widget = ListItem( self.object_type, self.input_modifiers, self, self.inputs_widget ) + + previous_field = None + next_field = None + if row is None: if self.input_fields: - self.input_fields[-1].order_changed() + previous_field = self.input_fields[-1] 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 previous_field: + previous_field.order_changed() + + if next_field: + next_field.order_changed() if is_empty: item_widget.set_as_empty() @@ -1454,8 +1615,9 @@ class ListWidget(QtWidgets.QWidget, InputObject): if self._state == state: return - self.label_widget.setProperty("state", state) - self.label_widget.style().polish(self.label_widget) + if self.label_widget: + self.label_widget.setProperty("state", state) + self.label_widget.style().polish(self.label_widget) def item_value(self): output = [] @@ -1466,6 +1628,184 @@ class ListWidget(QtWidgets.QWidget, InputObject): return output +class ListStrictWidget(QtWidgets.QWidget, InputObject): + value_changed = QtCore.Signal(object) + _default_input_value = None + + def __init__( + self, input_data, parent, + as_widget=False, label_widget=None, parent_widget=None + ): + if parent_widget is None: + parent_widget = parent + super(ListStrictWidget, self).__init__(parent_widget) + self.setObjectName("ListStrictWidget") + + self.initial_attributes(input_data, parent, as_widget) + + self.input_fields = [] + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 5) + layout.setSpacing(5) + + if not self.as_widget: + self.key = input_data["key"] + if not label_widget: + label_widget = QtWidgets.QLabel(input_data["label"], self) + layout.addWidget(label_widget, alignment=QtCore.Qt.AlignTop) + + self.label_widget = label_widget + + self._add_children(layout, input_data) + + def _add_children(self, layout, input_data): + inputs_widget = QtWidgets.QWidget(self) + inputs_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + layout.addWidget(inputs_widget) + + horizontal = input_data.get("horizontal", True) + if horizontal: + inputs_layout = QtWidgets.QHBoxLayout(inputs_widget) + else: + inputs_layout = QtWidgets.QGridLayout(inputs_widget) + + inputs_layout.setContentsMargins(0, 0, 0, 0) + inputs_layout.setSpacing(3) + + self.inputs_widget = inputs_widget + self.inputs_layout = inputs_layout + + children_item_mapping = [] + for child_configuration in input_data["object_types"]: + object_type = child_configuration["type"] + + item_widget = ListItem( + object_type, child_configuration, self, self.inputs_widget, + is_strict=True + ) + + self.input_fields.append(item_widget) + item_widget.value_changed.connect(self._on_value_change) + + label = child_configuration.get("label") + label_widget = None + if label: + label_widget = QtWidgets.QLabel(label, self) + + children_item_mapping.append((label_widget, item_widget)) + + if horizontal: + self._add_children_horizontally(children_item_mapping) + else: + self._add_children_vertically(children_item_mapping) + + self.updateGeometry() + + def _add_children_vertically(self, children_item_mapping): + any_has_label = False + for item_mapping in children_item_mapping: + if item_mapping[0]: + any_has_label = True + break + + row = self.inputs_layout.count() + if not any_has_label: + self.inputs_layout.setColumnStretch(1, 1) + for item_mapping in children_item_mapping: + item_widget = item_mapping[1] + self.inputs_layout.addWidget(item_widget, row, 0, 1, 1) + + spacer_widget = QtWidgets.QWidget(self.inputs_widget) + self.inputs_layout.addWidget(spacer_widget, row, 1, 1, 1) + row += 1 + + else: + self.inputs_layout.setColumnStretch(2, 1) + for label_widget, item_widget in children_item_mapping: + self.inputs_layout.addWidget( + label_widget, row, 0, 1, 1, + alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignTop + ) + self.inputs_layout.addWidget(item_widget, row, 1, 1, 1) + + spacer_widget = QtWidgets.QWidget(self.inputs_widget) + self.inputs_layout.addWidget(spacer_widget, row, 2, 1, 1) + row += 1 + + def _add_children_horizontally(self, children_item_mapping): + for label_widget, item_widget in children_item_mapping: + if label_widget: + self.inputs_layout.addWidget(label_widget, 0) + self.inputs_layout.addWidget(item_widget, 0) + + spacer_widget = QtWidgets.QWidget(self.inputs_widget) + self.inputs_layout.addWidget(spacer_widget, 1) + + @property + def default_input_value(self): + if self._default_input_value is None: + self.set_value(NOT_SET) + self._default_input_value = self.item_value() + return self._default_input_value + + def set_value(self, value): + if self._is_overriden: + method_name = "apply_overrides" + elif not self._has_studio_override: + method_name = "update_default_values" + else: + method_name = "update_studio_values" + + for idx, input_field in enumerate(self.input_fields): + if value is NOT_SET: + _value = value + else: + if idx > len(value) - 1: + _value = NOT_SET + else: + _value = value[idx] + _method = getattr(input_field, method_name) + _method(_value) + + def hierarchical_style_update(self): + for input_field in self.input_fields: + input_field.hierarchical_style_update() + self.update_style() + + def update_style(self): + if self._as_widget: + if not self.isEnabled(): + state = self.style_state(False, False, False, False) + else: + state = self.style_state( + False, + self._is_invalid, + False, + self._is_modified + ) + else: + state = self.style_state( + self.has_studio_override, + self.is_invalid, + self.is_overriden, + self.is_modified + ) + + if self._state == state: + return + + if self.label_widget: + self.label_widget.setProperty("state", state) + self.label_widget.style().polish(self.label_widget) + + def item_value(self): + output = [] + for item in self.input_fields: + output.append(item.config_value()) + return output + + class ModifiableDictItem(QtWidgets.QWidget, SettingObject): _btn_size = 20 value_changed = QtCore.Signal(object) @@ -1476,6 +1816,14 @@ class ModifiableDictItem(QtWidgets.QWidget, SettingObject): self._set_default_attributes() self._parent = config_parent + any_parent_as_widget = config_parent.as_widget + if not any_parent_as_widget: + any_parent_as_widget = config_parent.any_parent_as_widget + + self._any_parent_as_widget = any_parent_as_widget + self._any_parent_is_group = True + + self._is_empty = False self.is_key_duplicated = False layout = QtWidgets.QHBoxLayout(self) @@ -1502,9 +1850,14 @@ class ModifiableDictItem(QtWidgets.QWidget, SettingObject): self.add_btn.setProperty("btn-type", "tool-item") self.remove_btn.setProperty("btn-type", "tool-item") + self.spacer_widget = QtWidgets.QWidget(self) + self.spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.spacer_widget.setVisible(False) + layout.addWidget(self.add_btn, 0) layout.addWidget(self.remove_btn, 0) layout.addWidget(self.key_input, 0) + layout.addWidget(self.spacer_widget, 1) layout.addWidget(self.value_input, 1) self.setFocusProxy(self.value_input) @@ -1522,11 +1875,8 @@ class ModifiableDictItem(QtWidgets.QWidget, SettingObject): 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(): + if self._is_empty: return False if self.key_value() == "": @@ -1560,18 +1910,21 @@ class ModifiableDictItem(QtWidgets.QWidget, SettingObject): return self._parent.is_group def on_add_clicked(self): - if self._is_enabled(): - self._parent.add_row(row=self.row() + 1) - else: + if self._is_empty: self.set_as_empty(False) + else: + self._parent.add_row(row=self.row() + 1) def on_remove_clicked(self): self._parent.remove_row(self) def set_as_empty(self, is_empty=True): - self.key_input.setEnabled(not is_empty) - self.value_input.setEnabled(not is_empty) + self._is_empty = is_empty + + self.key_input.setVisible(not is_empty) + self.value_input.setVisible(not is_empty) self.remove_btn.setEnabled(not is_empty) + self.spacer_widget.setVisible(is_empty) self._on_value_change() @property @@ -1594,13 +1947,13 @@ class ModifiableDictItem(QtWidgets.QWidget, SettingObject): @property def is_invalid(self): - if not self._is_enabled(): + if self._is_empty: return False return self.is_key_invalid() or self.value_input.is_invalid def update_style(self): state = "" - if self._is_enabled(): + if not self._is_empty: if self.is_key_invalid(): state = "invalid" elif self.is_key_modified(): @@ -1618,9 +1971,9 @@ class ModifiableDictItem(QtWidgets.QWidget, SettingObject): return {key: value} def config_value(self): - if self._is_enabled(): - return self.item_value() - return {} + if self._is_empty: + return {} + return self.item_value() def mouseReleaseEvent(self, event): return QtWidgets.QWidget.mouseReleaseEvent(self, event) @@ -1631,6 +1984,7 @@ class ModifiableDict(QtWidgets.QWidget, InputObject): # Should be used only for dictionary with one datatype as value # TODO this is actually input field (do not care if is group or not) value_changed = QtCore.Signal(object) + expand_in_grid = True def __init__( self, input_data, parent, @@ -1647,21 +2001,22 @@ class ModifiableDict(QtWidgets.QWidget, InputObject): self.key = input_data["key"] + if input_data.get("highlight_content", False): + content_state = "hightlighted" + bottom_margin = 5 + else: + content_state = "" + bottom_margin = 0 + main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) - 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 @@ -1675,6 +2030,22 @@ class ModifiableDict(QtWidgets.QWidget, InputObject): else: body_widget.hide_toolbox(hide_content=False) + if body_widget is None: + content_parent_widget = self + else: + content_parent_widget = body_widget + + content_widget = QtWidgets.QWidget(content_parent_widget) + content_widget.setObjectName("ContentWidget") + content_widget.setProperty("content_state", content_state) + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(CHILD_OFFSET, 3, 0, bottom_margin) + + if body_widget is None: + main_layout.addWidget(content_widget) + else: + body_widget.set_content_widget(content_widget) + self.body_widget = body_widget self.content_widget = content_widget self.content_layout = content_layout @@ -1864,23 +2235,32 @@ class ModifiableDict(QtWidgets.QWidget, InputObject): # Dictionaries class DictWidget(QtWidgets.QWidget, SettingObject): value_changed = QtCore.Signal(object) + expand_in_grid = True def __init__( self, input_data, parent, as_widget=False, label_widget=None, parent_widget=None ): - if 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) + self.input_fields = [] + + self.checkbox_widget = None + self.checkbox_key = input_data.get("checkbox_key") + + self.label_widget = label_widget + + if self.as_widget: + self._ui_as_widget(input_data) + else: + self._ui_as_item(input_data) + + def _ui_as_item(self, input_data): + self.key = input_data["key"] if input_data.get("highlight_content", False): content_state = "hightlighted" bottom_margin = 5 @@ -1888,10 +2268,6 @@ class DictWidget(QtWidgets.QWidget, SettingObject): 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) @@ -1903,7 +2279,7 @@ class DictWidget(QtWidgets.QWidget, SettingObject): 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 = QtWidgets.QGridLayout(content_widget) content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, bottom_margin) body_widget.set_content_widget(content_widget) @@ -1914,9 +2290,6 @@ class DictWidget(QtWidgets.QWidget, SettingObject): 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) @@ -1931,13 +2304,30 @@ class DictWidget(QtWidgets.QWidget, SettingObject): else: body_widget.hide_toolbox(hide_content=False) + def _ui_as_widget(self, input_data): + body = QtWidgets.QWidget(self) + body.setObjectName("DictAsWidgetBody") + + content_layout = QtWidgets.QGridLayout(body) + content_layout.setContentsMargins(5, 5, 5, 5) + self.content_layout = content_layout + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + layout.addWidget(body) + + for child_configuration in input_data["children"]: + self.add_children_gui(child_configuration) + def add_children_gui(self, child_configuration): item_type = child_configuration["type"] klass = TypeToKlass.types.get(item_type) - if not klass.is_input_type: + row = self.content_layout.rowCount() + if not getattr(klass, "is_input_type", False): item = klass(child_configuration, self) - self.content_layout.addWidget(item) + self.content_layout.addWidget(item, row, 0, 1, 2) return item if self.checkbox_key and not self.checkbox_widget: @@ -1945,9 +2335,21 @@ class DictWidget(QtWidgets.QWidget, SettingObject): if key == self.checkbox_key: return self._add_checkbox_child(child_configuration) - item = klass(child_configuration, self) + label_widget = None + if not klass.expand_in_grid: + label = child_configuration.get("label") + if label is not None: + label_widget = GridLabelWidget(label, self) + self.content_layout.addWidget(label_widget, row, 0, 1, 1) + + item = klass(child_configuration, self, label_widget=label_widget) item.value_changed.connect(self._on_value_change) - self.content_layout.addWidget(item) + + if label_widget: + label_widget.input_field = item + self.content_layout.addWidget(item, row, 1, 1, 1) + else: + self.content_layout.addWidget(item, row, 0, 1, 2) self.input_fields.append(item) return item @@ -2002,8 +2404,12 @@ class DictWidget(QtWidgets.QWidget, SettingObject): item.set_as_overriden() def update_default_values(self, parent_values): + # Make sure this is set to False + self._state = None + self._child_state = None + value = NOT_SET - if self._as_widget: + if self.as_widget: value = parent_values elif parent_values is not NOT_SET: value = parent_values.get(self.key, NOT_SET) @@ -2012,15 +2418,21 @@ class DictWidget(QtWidgets.QWidget, SettingObject): item.update_default_values(value) def update_studio_values(self, parent_values): + # Make sure this is set to False + self._state = None + self._child_state = None value = NOT_SET - if parent_values is not NOT_SET: - value = parent_values.get(self.key, NOT_SET) + if self.as_widget: + value = parent_values + else: + if parent_values is not NOT_SET: + value = parent_values.get(self.key, NOT_SET) - self._has_studio_override = False - if self.is_group and value is not NOT_SET: - self._has_studio_override = True + self._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) + self._had_studio_override = bool(self._has_studio_override) for item in self.input_fields: item.update_studio_values(value) @@ -2030,37 +2442,40 @@ class DictWidget(QtWidgets.QWidget, SettingObject): 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) + if not self.as_widget: + metadata = {} + groups = tuple() + override_values = NOT_SET + if parent_values is not NOT_SET: + metadata = parent_values.get(METADATA_KEY) or metadata + groups = metadata.get("groups") or groups + override_values = parent_values.get(self.key, override_values) - self._is_overriden = self.key in groups + 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) + if not self.as_widget: + if not self._is_overriden: + self._is_overriden = ( + self.is_group + and self.is_overidable + and self.child_overriden + ) + self._was_overriden = bool(self._is_overriden) def _on_value_change(self, item=None): if self.ignore_value_changes: return - if self.is_group: + if self.is_group and not (self.as_widget or self.any_parent_as_widget): if self.is_overidable: self._is_overriden = True else: self._has_studio_override = True + # TODO check if this is required self.hierarchical_style_update() self.value_changed.emit(self) @@ -2073,6 +2488,10 @@ class DictWidget(QtWidgets.QWidget, SettingObject): self.update_style() def update_style(self, is_overriden=None): + # TODO add style update when used as widget + if self.as_widget: + return + child_has_studio_override = self.child_has_studio_override child_modified = self.child_modified child_invalid = self.child_invalid @@ -2158,7 +2577,11 @@ class DictWidget(QtWidgets.QWidget, SettingObject): return output def studio_overrides(self): - if not self.has_studio_override and not self.child_has_studio_override: + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.has_studio_override + and not self.child_has_studio_override + ): return NOT_SET, False values = {} @@ -2194,6 +2617,7 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): # TODO is not overridable by itself value_changed = QtCore.Signal(object) allow_actions = False + expand_in_grid = True def __init__( self, input_data, parent, @@ -2211,10 +2635,12 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - layout = QtWidgets.QVBoxLayout(self) + layout = QtWidgets.QGridLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(5) + self.content_layout = layout + self.input_fields = [] self.key = input_data["key"] @@ -2226,16 +2652,28 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): item_type = child_configuration["type"] klass = TypeToKlass.types.get(item_type) - if not klass.is_input_type: + row = self.content_layout.rowCount() + if not getattr(klass, "is_input_type", False): item = klass(child_configuration, self) - self.layout().addWidget(item) + self.content_layout.addWidget(item, row, 0, 1, 2) return item - item = klass(child_configuration, self) - self.layout().addWidget(item) + label_widget = None + if not klass.expand_in_grid: + label = child_configuration.get("label") + if label is not None: + label_widget = GridLabelWidget(label, self) + self.content_layout.addWidget(label_widget, row, 0, 1, 1) + item = klass(child_configuration, self, label_widget=label_widget) item.value_changed.connect(self._on_value_change) + if label_widget: + label_widget.input_field = item + self.content_layout.addWidget(item, row, 1, 1, 1) + else: + self.content_layout.addWidget(item, row, 0, 1, 2) + self.input_fields.append(item) return item @@ -2291,7 +2729,7 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): if self.ignore_value_changes: return - if self.is_group: + if self.is_group and not self.any_parent_as_widget: if self.is_overidable: self._is_overriden = True else: @@ -2388,7 +2826,11 @@ class DictInvisible(QtWidgets.QWidget, SettingObject): self._was_overriden = bool(self._is_overriden) def studio_overrides(self): - if not self.has_studio_override and not self.child_has_studio_override: + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.has_studio_override + and not self.child_has_studio_override + ): return NOT_SET, False values = {} @@ -2566,6 +3008,8 @@ class PathWidget(QtWidgets.QWidget, SettingObject): raise ValueError( "Default value is not set. This is implementation BUG." ) + else: + self.defaults_not_set = False self.default_value = value self._has_studio_override = False @@ -2651,10 +3095,11 @@ class PathWidget(QtWidgets.QWidget, SettingObject): if self.ignore_value_changes: return - if self.is_overidable: - self._is_overriden = True - else: - self._has_studio_override = True + if not self.any_parent_as_widget: + if self.is_overidable: + self._is_overriden = True + else: + self._has_studio_override = True if self._is_invalid: self._is_modified = True @@ -2781,7 +3226,11 @@ class PathWidget(QtWidgets.QWidget, SettingObject): return output def studio_overrides(self): - if not self.has_studio_override and not self.child_has_studio_override: + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.has_studio_override + and not self.child_has_studio_override + ): return NOT_SET, False value = self.item_value() @@ -2809,6 +3258,7 @@ class FormLabel(QtWidgets.QLabel): class DictFormWidget(QtWidgets.QWidget, SettingObject): value_changed = QtCore.Signal(object) allow_actions = False + expand_in_grid = True def __init__( self, input_data, parent, @@ -2970,7 +3420,11 @@ class DictFormWidget(QtWidgets.QWidget, SettingObject): return self.item_value() def studio_overrides(self): - if not self.has_studio_override and not self.child_has_studio_override: + if ( + not (self.as_widget or self.any_parent_as_widget) + and not self.has_studio_override + and not self.child_has_studio_override + ): return NOT_SET, False values = {} @@ -3039,11 +3493,14 @@ TypeToKlass.types["text"] = TextWidget TypeToKlass.types["path-input"] = PathInputWidget TypeToKlass.types["raw-json"] = RawJsonWidget TypeToKlass.types["list"] = ListWidget +TypeToKlass.types["list-strict"] = ListStrictWidget TypeToKlass.types["dict-modifiable"] = ModifiableDict +# DEPRECATED - remove when removed from schemas +TypeToKlass.types["dict-item"] = DictWidget TypeToKlass.types["dict"] = DictWidget TypeToKlass.types["dict-invisible"] = DictInvisible TypeToKlass.types["path-widget"] = PathWidget -TypeToKlass.types["dict-form"] = DictFormWidget +TypeToKlass.types["form"] = DictFormWidget TypeToKlass.types["label"] = LabelWidget TypeToKlass.types["splitter"] = SplitterWidget diff --git a/pype/tools/settings/settings/widgets/lib.py b/pype/tools/settings/settings/widgets/lib.py index e225d65417..f54989cfd7 100644 --- a/pype/tools/settings/settings/widgets/lib.py +++ b/pype/tools/settings/settings/widgets/lib.py @@ -69,12 +69,11 @@ def _fill_inner_schemas(schema_data, 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) + new_child = _fill_inner_schemas( + schema_collection[child["name"]], + schema_collection + ) + new_children.append(new_child) schema_data["children"] = new_children return schema_data diff --git a/pype/tools/settings/settings/widgets/widgets.py b/pype/tools/settings/settings/widgets/widgets.py index 400b9371fd..2a1f5fe804 100644 --- a/pype/tools/settings/settings/widgets/widgets.py +++ b/pype/tools/settings/settings/widgets/widgets.py @@ -226,3 +226,56 @@ class UnsavedChangesDialog(QtWidgets.QDialog): def on_discard_pressed(self): self.done(2) + + +class SpacerWidget(QtWidgets.QWidget): + def __init__(self, parent=None): + super(SpacerWidget, self).__init__(parent) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + +class GridLabelWidget(QtWidgets.QWidget): + def __init__(self, label, parent=None): + super(GridLabelWidget, self).__init__(parent) + + self.input_field = None + + self.properties = {} + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + label_proxy = QtWidgets.QWidget(self) + label_proxy_layout = QtWidgets.QHBoxLayout(label_proxy) + label_proxy_layout.setContentsMargins(0, 0, 0, 0) + label_proxy_layout.setSpacing(0) + + label_widget = QtWidgets.QLabel(label, label_proxy) + spacer_widget_h = SpacerWidget(label_proxy) + label_proxy_layout.addWidget( + spacer_widget_h, 0, alignment=QtCore.Qt.AlignRight + ) + label_proxy_layout.addWidget( + label_widget, 0, alignment=QtCore.Qt.AlignRight + ) + + spacer_widget_v = SpacerWidget(self) + + layout.addWidget(label_proxy, 0) + layout.addWidget(spacer_widget_v, 1) + + self.label_widget = label_widget + + def setProperty(self, name, value): + cur_value = self.properties.get(name) + if cur_value == value: + return + + self.label_widget.setProperty(name, value) + self.label_widget.style().polish(self.label_widget) + + def mouseReleaseEvent(self, event): + if self.input_field: + return self.input_field.show_actions_menu(event) + return super(GridLabelWidget, self).mouseReleaseEvent(event)