[Automated] Merged develop into main

This commit is contained in:
pypebot 2021-08-14 05:35:33 +02:00 committed by GitHub
commit db0539762a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 1471 additions and 177 deletions

View file

@ -29,7 +29,7 @@ The main things you will need to run and build OpenPype are:
- PowerShell 5.0+ (Windows)
- Bash (Linux)
- [**Python 3.7.8**](#python) or higher
- [**MongoDB**](#database)
- [**MongoDB**](#database) (needed only for local development)
It can be built and ran on all common platforms. We develop and test on the following:
@ -126,6 +126,16 @@ pyenv local 3.7.9
### Linux
#### Docker
Easiest way to build OpenPype on Linux is using [Docker](https://www.docker.com/). Just run:
```sh
sudo ./tools/docker_build.sh
```
If all is successful, you'll find built OpenPype in `./build/` folder.
#### Manual build
You will need [Python 3.7](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads). You'll also need [curl](https://curl.se) on systems that doesn't have one preinstalled.
To build Python related stuff, you need Python header files installed (`python3-dev` on Ubuntu for example).
@ -133,7 +143,6 @@ To build Python related stuff, you need Python header files installed (`python3-
You'll need also other tools to build
some OpenPype dependencies like [CMake](https://cmake.org/). Python 3 should be part of all modern distributions. You can use your package manager to install **git** and **cmake**.
<details>
<summary>Details for Ubuntu</summary>
Install git, cmake and curl

View file

@ -47,7 +47,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
"subset": subset,
"label": scene_file,
"family": family,
"families": [family, "ftrack"],
"families": [family],
"representations": list()
})

View file

@ -26,6 +26,12 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
def install():
from openpype.settings import get_project_settings
project_settings = get_project_settings(os.getenv("AVALON_PROJECT"))
# process path mapping
process_dirmap(project_settings)
pyblish.register_plugin_path(PUBLISH_PATH)
avalon.register_plugin_path(avalon.Loader, LOAD_PATH)
avalon.register_plugin_path(avalon.Creator, CREATE_PATH)
@ -53,6 +59,40 @@ def install():
avalon.data["familiesStateToggled"] = ["imagesequence"]
def process_dirmap(project_settings):
# type: (dict) -> None
"""Go through all paths in Settings and set them using `dirmap`.
Args:
project_settings (dict): Settings for current project.
"""
if not project_settings["maya"].get("maya-dirmap"):
return
mapping = project_settings["maya"]["maya-dirmap"]["paths"] or {}
mapping_enabled = project_settings["maya"]["maya-dirmap"]["enabled"]
if not mapping or not mapping_enabled:
return
if mapping.get("source-path") and mapping_enabled is True:
log.info("Processing directory mapping ...")
cmds.dirmap(en=True)
for k, sp in enumerate(mapping["source-path"]):
try:
print("{} -> {}".format(sp, mapping["destination-path"][k]))
cmds.dirmap(m=(sp, mapping["destination-path"][k]))
cmds.dirmap(m=(mapping["destination-path"][k], sp))
except IndexError:
# missing corresponding destination path
log.error(("invalid dirmap mapping, missing corresponding"
" destination directory."))
break
except RuntimeError:
log.error("invalid path {} -> {}, mapping not registered".format(
sp, mapping["destination-path"][k]
))
continue
def uninstall():
pyblish.deregister_plugin_path(PUBLISH_PATH)
avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH)

View file

@ -51,7 +51,6 @@ class ExtractReviewDataLut(openpype.api.Extractor):
if "render.farm" in families:
instance.data["families"].remove("review")
instance.data["families"].remove("ftrack")
self.log.debug(
"_ lutPath: {}".format(instance.data["lutPath"]))

View file

@ -45,7 +45,6 @@ class ExtractReviewDataMov(openpype.api.Extractor):
if "render.farm" in families:
instance.data["families"].remove("review")
instance.data["families"].remove("ftrack")
data = exporter.generate_mov(farm=True)
self.log.debug(

View file

@ -199,7 +199,7 @@ def get_renderer_variables(renderlayer, root):
if extension is None:
extension = "png"
if extension == "exr (multichannel)" or extension == "exr (deep)":
if extension in ["exr (multichannel)", "exr (deep)"]:
extension = "exr"
prefix_attr = "vraySettings.fileNamePrefix"
@ -295,57 +295,70 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
instance.data["toBeRenderedOn"] = "deadline"
filepath = None
patches = (
context.data["project_settings"].get(
"deadline", {}).get(
"publish", {}).get(
"MayaSubmitDeadline", {}).get(
"scene_patches", {})
)
# Handle render/export from published scene or not ------------------
if self.use_published:
patched_files = []
for i in context:
if "workfile" in i.data["families"]:
assert i.data["publish"] is True, (
"Workfile (scene) must be published along")
template_data = i.data.get("anatomyData")
rep = i.data.get("representations")[0].get("name")
template_data["representation"] = rep
template_data["ext"] = rep
template_data["comment"] = None
anatomy_filled = anatomy.format(template_data)
template_filled = anatomy_filled["publish"]["path"]
filepath = os.path.normpath(template_filled)
self.log.info("Using published scene for render {}".format(
filepath))
if "workfile" not in i.data["families"]:
continue
assert i.data["publish"] is True, (
"Workfile (scene) must be published along")
template_data = i.data.get("anatomyData")
rep = i.data.get("representations")[0].get("name")
template_data["representation"] = rep
template_data["ext"] = rep
template_data["comment"] = None
anatomy_filled = anatomy.format(template_data)
template_filled = anatomy_filled["publish"]["path"]
filepath = os.path.normpath(template_filled)
self.log.info("Using published scene for render {}".format(
filepath))
if not os.path.exists(filepath):
self.log.error("published scene does not exist!")
raise
# now we need to switch scene in expected files
# because <scene> token will now point to published
# scene file and that might differ from current one
new_scene = os.path.splitext(
os.path.basename(filepath))[0]
orig_scene = os.path.splitext(
os.path.basename(context.data["currentFile"]))[0]
exp = instance.data.get("expectedFiles")
if not os.path.exists(filepath):
self.log.error("published scene does not exist!")
raise
# now we need to switch scene in expected files
# because <scene> token will now point to published
# scene file and that might differ from current one
new_scene = os.path.splitext(
os.path.basename(filepath))[0]
orig_scene = os.path.splitext(
os.path.basename(context.data["currentFile"]))[0]
exp = instance.data.get("expectedFiles")
if isinstance(exp[0], dict):
# we have aovs and we need to iterate over them
new_exp = {}
for aov, files in exp[0].items():
replaced_files = []
for f in files:
replaced_files.append(
f.replace(orig_scene, new_scene)
)
new_exp[aov] = replaced_files
instance.data["expectedFiles"] = [new_exp]
else:
new_exp = []
for f in exp:
new_exp.append(
if isinstance(exp[0], dict):
# we have aovs and we need to iterate over them
new_exp = {}
for aov, files in exp[0].items():
replaced_files = []
for f in files:
replaced_files.append(
f.replace(orig_scene, new_scene)
)
instance.data["expectedFiles"] = [new_exp]
self.log.info("Scene name was switched {} -> {}".format(
orig_scene, new_scene
))
new_exp[aov] = replaced_files
instance.data["expectedFiles"] = [new_exp]
else:
new_exp = []
for f in exp:
new_exp.append(
f.replace(orig_scene, new_scene)
)
instance.data["expectedFiles"] = [new_exp]
self.log.info("Scene name was switched {} -> {}".format(
orig_scene, new_scene
))
# patch workfile is needed
if filepath not in patched_files:
patched_file = self._patch_workfile(filepath, patches)
patched_files.append(patched_file)
all_instances = []
for result in context.data["results"]:
@ -868,10 +881,11 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
payload["JobInfo"].update(job_info_ext)
payload["PluginInfo"].update(plugin_info_ext)
envs = []
for k, v in payload["JobInfo"].items():
if k.startswith("EnvironmentKeyValue"):
envs.append(v)
envs = [
v
for k, v in payload["JobInfo"].items()
if k.startswith("EnvironmentKeyValue")
]
# add app name to environment
envs.append(
@ -892,11 +906,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
envs.append(
"OPENPYPE_ASS_EXPORT_STEP={}".format(1))
i = 0
for e in envs:
for i, e in enumerate(envs):
payload["JobInfo"]["EnvironmentKeyValue{}".format(i)] = e
i += 1
return payload
def _get_vray_render_payload(self, data):
@ -1003,7 +1014,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
"""
if 'verify' not in kwargs:
kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa
kwargs['verify'] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True)
# add 10sec timeout before bailing out
kwargs['timeout'] = 10
return requests.post(*args, **kwargs)
@ -1022,7 +1033,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
"""
if 'verify' not in kwargs:
kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa
kwargs['verify'] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True)
# add 10sec timeout before bailing out
kwargs['timeout'] = 10
return requests.get(*args, **kwargs)
@ -1069,3 +1080,43 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin):
result = filename_zero.replace("\\", "/")
return result
def _patch_workfile(self, file, patches):
# type: (str, dict) -> [str, None]
"""Patch Maya scene.
This will take list of patches (lines to add) and apply them to
*published* Maya scene file (that is used later for rendering).
Patches are dict with following structure::
{
"name": "Name of patch",
"regex": "regex of line before patch",
"line": "line to insert"
}
Args:
file (str): File to patch.
patches (dict): Dictionary defining patches.
Returns:
str: Patched file path or None
"""
if os.path.splitext(file)[1].lower() != ".ma" or not patches:
return None
compiled_regex = [re.compile(p["regex"]) for p in patches]
with open(file, "r+") as pf:
scene_data = pf.readlines()
for ln, line in enumerate(scene_data):
for i, r in enumerate(compiled_regex):
if re.match(r, line):
scene_data.insert(ln + 1, patches[i]["line"])
pf.seek(0)
pf.writelines(scene_data)
pf.truncate()
self.log.info(
"Applied {} patch to scene.".format(
patches[i]["name"]))
return file

View file

@ -181,6 +181,10 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin):
"""Returns set of file names from metadata.json"""
expected_files = set()
for file_name in repre["files"]:
files = repre["files"]
if not isinstance(files, list):
files = [files]
for file_name in files:
expected_files.add(file_name)
return expected_files

View file

@ -63,8 +63,9 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin):
self.log.debug("Adding ftrack family for '{}'".
format(instance.data.get("family")))
if families and "ftrack" not in families:
instance.data["families"].append("ftrack")
if families:
if "ftrack" not in families:
instance.data["families"].append("ftrack")
else:
instance.data["families"] = ["ftrack"]
else:

View file

@ -16,6 +16,7 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin):
def process(self, context):
asset_and_parents = self.get_parents(context)
self.log.debug("__ asset_and_parents: {}".format(asset_and_parents))
if not io.Session:
io.install()
@ -25,7 +26,8 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin):
self.log.debug("__ db_assets: {}".format(db_assets))
asset_db_docs = {
str(e["name"]): e["data"]["parents"] for e in db_assets}
str(e["name"]): e["data"]["parents"]
for e in db_assets}
self.log.debug("__ project_entities: {}".format(
pformat(asset_db_docs)))
@ -107,6 +109,7 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin):
parents = instance.data["parents"]
return_dict.update({
asset: [p["entity_name"] for p in parents]
asset: [p["entity_name"] for p in parents
if p["entity_type"].lower() != "project"]
})
return return_dict

View file

@ -45,7 +45,8 @@
"group": "none",
"limit": [],
"jobInfo": {},
"pluginInfo": {}
"pluginInfo": {},
"scene_patches": []
},
"NukeSubmitDeadline": {
"enabled": true,

View file

@ -304,7 +304,8 @@
"aftereffects"
],
"families": [
"render"
"render",
"workfile"
],
"tasks": [],
"add_ftrack_family": true,

View file

@ -7,6 +7,19 @@
"workfile": "ma",
"yetiRig": "ma"
},
"maya-dirmap": {
"enabled": true,
"paths": {
"source-path": [
"foo1",
"foo2"
],
"destination-path": [
"bar1",
"bar2"
]
}
},
"scriptsmenu": {
"name": "OpenPype Tools",
"definition": [

View file

@ -1,6 +1,5 @@
{
"project_setup": {
"dev_mode": true,
"install_unreal_python_engine": false
"dev_mode": true
}
}

View file

@ -174,6 +174,14 @@ class BaseItemEntity(BaseEntity):
roles = [roles]
self.roles = roles
@abstractmethod
def collect_static_entities_by_path(self):
"""Collect all paths of all static path entities.
Static path is entity which is not dynamic or under dynamic entity.
"""
pass
@property
def require_restart_on_change(self):
return self._require_restart_on_change

View file

@ -141,6 +141,7 @@ class DictConditionalEntity(ItemEntity):
self.enum_key = self.schema_data.get("enum_key")
self.enum_label = self.schema_data.get("enum_label")
self.enum_children = self.schema_data.get("enum_children")
self.enum_default = self.schema_data.get("enum_default")
self.enum_entity = None
@ -277,15 +278,22 @@ class DictConditionalEntity(ItemEntity):
if isinstance(item, dict) and "key" in item:
valid_enum_items.append(item)
enum_keys = []
enum_items = []
for item in valid_enum_items:
item_key = item["key"]
enum_keys.append(item_key)
item_label = item.get("label") or item_key
enum_items.append({item_key: item_label})
if not enum_items:
return
if self.enum_default in enum_keys:
default_key = self.enum_default
else:
default_key = enum_keys[0]
# Create Enum child first
enum_key = self.enum_key or "invalid"
enum_schema = {
@ -293,7 +301,8 @@ class DictConditionalEntity(ItemEntity):
"multiselection": False,
"enum_items": enum_items,
"key": enum_key,
"label": self.enum_label
"label": self.enum_label,
"default": default_key
}
enum_entity = self.create_schema_object(enum_schema, self)
@ -318,6 +327,11 @@ class DictConditionalEntity(ItemEntity):
self.non_gui_children[item_key][child_obj.key] = child_obj
def collect_static_entities_by_path(self):
if self.is_dynamic_item or self.is_in_dynamic_item:
return {}
return {self.path: self}
def get_child_path(self, child_obj):
"""Get hierarchical path of child entity.

View file

@ -203,6 +203,18 @@ class DictImmutableKeysEntity(ItemEntity):
)
self.show_borders = self.schema_data.get("show_borders", True)
def collect_static_entities_by_path(self):
output = {}
if self.is_dynamic_item or self.is_in_dynamic_item:
return output
output[self.path] = self
for children in self.non_gui_children.values():
result = children.collect_static_entities_by_path()
if result:
output.update(result)
return output
def get_child_path(self, child_obj):
"""Get hierarchical path of child entity.

View file

@ -73,21 +73,41 @@ class EnumEntity(BaseEnumEntity):
def _item_initalization(self):
self.multiselection = self.schema_data.get("multiselection", False)
self.enum_items = self.schema_data.get("enum_items")
# Default is optional and non breaking attribute
enum_default = self.schema_data.get("default")
valid_keys = set()
all_keys = []
for item in self.enum_items or []:
valid_keys.add(tuple(item.keys())[0])
key = tuple(item.keys())[0]
all_keys.append(key)
self.valid_keys = valid_keys
self.valid_keys = set(all_keys)
if self.multiselection:
self.valid_value_types = (list, )
self.value_on_not_set = []
value_on_not_set = []
if enum_default:
if not isinstance(enum_default, list):
enum_default = [enum_default]
for item in enum_default:
if item in all_keys:
value_on_not_set.append(item)
self.value_on_not_set = value_on_not_set
else:
for key in valid_keys:
if self.value_on_not_set is NOT_SET:
self.value_on_not_set = key
break
if isinstance(enum_default, list) and enum_default:
enum_default = enum_default[0]
if enum_default in self.valid_keys:
self.value_on_not_set = enum_default
else:
for key in all_keys:
if self.value_on_not_set is NOT_SET:
self.value_on_not_set = key
break
self.valid_value_types = (STRING_TYPE, )

View file

@ -53,6 +53,11 @@ class EndpointEntity(ItemEntity):
def _settings_value(self):
pass
def collect_static_entities_by_path(self):
if self.is_dynamic_item or self.is_in_dynamic_item:
return {}
return {self.path: self}
def settings_value(self):
if self._override_state is OverrideState.NOT_DEFINED:
return NOT_SET

View file

@ -106,6 +106,9 @@ class PathEntity(ItemEntity):
self.valid_value_types = valid_value_types
self.child_obj = self.create_schema_object(item_schema, self)
def collect_static_entities_by_path(self):
return self.child_obj.collect_static_entities_by_path()
def get_child_path(self, _child_obj):
return self.path
@ -192,6 +195,24 @@ class PathEntity(ItemEntity):
class ListStrictEntity(ItemEntity):
schema_types = ["list-strict"]
def __getitem__(self, idx):
if not isinstance(idx, int):
idx = int(idx)
return self.children[idx]
def __setitem__(self, idx, value):
if not isinstance(idx, int):
idx = int(idx)
self.children[idx].set(value)
def get(self, idx, default=None):
if not isinstance(idx, int):
idx = int(idx)
if idx < len(self.children):
return self.children[idx]
return default
def _item_initalization(self):
self.valid_value_types = (list, )
self.require_key = True
@ -222,6 +243,18 @@ class ListStrictEntity(ItemEntity):
super(ListStrictEntity, self).schema_validations()
def collect_static_entities_by_path(self):
output = {}
if self.is_dynamic_item or self.is_in_dynamic_item:
return output
output[self.path] = self
for child_obj in self.children:
result = child_obj.collect_static_entities_by_path()
if result:
output.update(result)
return output
def get_child_path(self, child_obj):
result_idx = None
for idx, _child_obj in enumerate(self.children):

View file

@ -45,6 +45,24 @@ class ListEntity(EndpointEntity):
return True
return False
def __getitem__(self, idx):
if not isinstance(idx, int):
idx = int(idx)
return self.children[idx]
def __setitem__(self, idx, value):
if not isinstance(idx, int):
idx = int(idx)
self.children[idx].set(value)
def get(self, idx, default=None):
if not isinstance(idx, int):
idx = int(idx)
if idx < len(self.children):
return self.children[idx]
return default
def index(self, item):
if isinstance(item, BaseEntity):
for idx, child_entity in enumerate(self.children):

View file

@ -242,6 +242,14 @@ class RootEntity(BaseItemEntity):
"""Whan any children has changed."""
self.on_change()
def collect_static_entities_by_path(self):
output = {}
for child_obj in self.non_gui_children.values():
result = child_obj.collect_static_entities_by_path()
if result:
output.update(result)
return output
def get_child_path(self, child_entity):
"""Return path of children entity"""
for key, _child_entity in self.non_gui_children.items():

View file

@ -195,6 +195,7 @@
- all items in `enum_children` must have at least `key` key which represents value stored under `enum_key`
- items can define `label` for UI purposes
- most important part is that item can define `children` key where are definitions of it's children (`children` value works the same way as in `dict`)
- to set default value for `enum_key` set it with `enum_default`
- entity must have defined `"label"` if is not used as widget
- is set as group if any parent is not group
- if `"label"` is entetered there which will be shown in GUI
@ -359,6 +360,9 @@ How output of the schema could look like on save:
- values are defined under value of key `"enum_items"` as list
- each item in list is simple dictionary where value is label and key is value which will be stored
- should be possible to enter single dictionary if order of items doesn't matter
- it is possible to set default selected value/s with `default` attribute
- it is recommended to use this option only in single selection mode
- at the end this option is used only when defying default settings value or in dynamic items
```
{
@ -371,7 +375,7 @@ How output of the schema could look like on save:
{"ftrackreview": "Add to Ftrack"},
{"delete": "Delete output"},
{"slate-frame": "Add slate frame"},
{"no-hnadles": "Skip handle frames"}
{"no-handles": "Skip handle frames"}
]
}
```

View file

@ -151,7 +151,7 @@
"type": "dict",
"collapsible": true,
"key": "MayaSubmitDeadline",
"label": "Submit maya job to deadline",
"label": "Submit Maya job to Deadline",
"checkbox_key": "enabled",
"children": [
{
@ -213,6 +213,31 @@
"type": "raw-json",
"key": "pluginInfo",
"label": "Additional PluginInfo data"
},
{
"type": "list",
"key": "scene_patches",
"label": "Scene patches",
"required_keys": ["name", "regex", "line"],
"object_type": {
"type": "dict",
"children": [
{
"key": "name",
"label": "Patch name",
"type": "text"
}, {
"key": "regex",
"label": "Patch regex",
"type": "text"
}, {
"key": "line",
"label": "Patch line",
"type": "text"
}
]
}
}
]
},

View file

@ -14,6 +14,39 @@
"type": "text"
}
},
{
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"key": "maya-dirmap",
"label": "Maya Directory Mapping",
"is_group": true,
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "dict",
"key": "paths",
"children": [
{
"type": "list",
"object_type": "text",
"key": "source-path",
"label": "Source Path"
},
{
"type": "list",
"object_type": "text",
"key": "destination-path",
"label": "Destination Path"
}
]
}
]
},
{
"type": "schema",
"name": "schema_maya_scriptsmenu"

View file

@ -25,6 +25,38 @@ class BaseWidget(QtWidgets.QWidget):
self.label_widget = None
self.create_ui()
def scroll_to(self, widget):
self.category_widget.scroll_to(widget)
def set_path(self, path):
self.category_widget.set_path(path)
def set_focus(self, scroll_to=False):
"""Set focus of a widget.
Args:
scroll_to(bool): Also scroll to widget in category widget.
"""
if scroll_to:
self.scroll_to(self)
self.setFocus()
def make_sure_is_visible(self, path, scroll_to):
"""Make a widget of entity visible by it's path.
Args:
path(str): Path to entity.
scroll_to(bool): Should be scrolled to entity.
Returns:
bool: Entity with path was found.
"""
raise NotImplementedError(
"{} not implemented `make_sure_is_visible`".format(
self.__class__.__name__
)
)
def trigger_hierarchical_style_update(self):
self.category_widget.hierarchical_style_update()
@ -277,11 +309,23 @@ class BaseWidget(QtWidgets.QWidget):
if to_run:
to_run()
def focused_in(self):
if self.entity is not None:
self.set_path(self.entity.path)
def mouseReleaseEvent(self, event):
if self.allow_actions and event.button() == QtCore.Qt.RightButton:
return self.show_actions_menu()
return super(BaseWidget, self).mouseReleaseEvent(event)
focused_in = False
if event.button() == QtCore.Qt.LeftButton:
focused_in = True
self.focused_in()
result = super(BaseWidget, self).mouseReleaseEvent(event)
if focused_in and not event.isAccepted():
event.accept()
return result
class InputWidget(BaseWidget):
@ -337,6 +381,14 @@ class InputWidget(BaseWidget):
)
)
def make_sure_is_visible(self, path, scroll_to):
if path:
entity_path = self.entity.path
if entity_path == path:
self.set_focus(scroll_to)
return True
return False
def update_style(self):
has_unsaved_changes = self.entity.has_unsaved_changes
if not has_unsaved_changes and self.entity.group_item:
@ -422,11 +474,20 @@ class GUIWidget(BaseWidget):
layout.addWidget(splitter_item)
def set_entity_value(self):
return
pass
def hierarchical_style_update(self):
pass
def make_sure_is_visible(self, *args, **kwargs):
return False
def focused_in(self):
pass
def set_path(self, *args, **kwargs):
pass
def get_invalid(self):
return []

View file

@ -0,0 +1,492 @@
from Qt import QtWidgets, QtGui, QtCore
PREFIX_ROLE = QtCore.Qt.UserRole + 1
LAST_SEGMENT_ROLE = QtCore.Qt.UserRole + 2
class BreadcrumbItem(QtGui.QStandardItem):
def __init__(self, *args, **kwargs):
self._display_value = None
self._edit_value = None
super(BreadcrumbItem, self).__init__(*args, **kwargs)
def data(self, role=None):
if role == QtCore.Qt.DisplayRole:
return self._display_value
if role == QtCore.Qt.EditRole:
return self._edit_value
if role is None:
args = tuple()
else:
args = (role, )
return super(BreadcrumbItem, self).data(*args)
def setData(self, value, role):
if role == QtCore.Qt.DisplayRole:
self._display_value = value
return True
if role == QtCore.Qt.EditRole:
self._edit_value = value
return True
if role is None:
args = (value, )
else:
args = (value, role)
return super(BreadcrumbItem, self).setData(*args)
class BreadcrumbsModel(QtGui.QStandardItemModel):
def __init__(self):
super(BreadcrumbsModel, self).__init__()
self.current_path = ""
self.reset()
def reset(self):
return
class SettingsBreadcrumbs(BreadcrumbsModel):
def __init__(self):
self.entity = None
self.entities_by_path = {}
self.dynamic_paths = set()
super(SettingsBreadcrumbs, self).__init__()
def set_entity(self, entity):
self.entities_by_path = {}
self.dynamic_paths = set()
self.entity = entity
self.reset()
def has_children(self, path):
for key in self.entities_by_path.keys():
if key.startswith(path):
return True
return False
def is_valid_path(self, path):
if not path:
return True
path_items = path.split("/")
try:
entity = self.entity
for item in path_items:
entity = entity[item]
except Exception:
return False
return True
class SystemSettingsBreadcrumbs(SettingsBreadcrumbs):
def reset(self):
root_item = self.invisibleRootItem()
rows = root_item.rowCount()
if rows > 0:
root_item.removeRows(0, rows)
if self.entity is None:
return
entities_by_path = self.entity.collect_static_entities_by_path()
self.entities_by_path = entities_by_path
items = []
for path in entities_by_path.keys():
if not path:
continue
path_items = path.split("/")
value = path
label = path_items.pop(-1)
prefix = "/".join(path_items)
if prefix:
prefix += "/"
item = QtGui.QStandardItem(value)
item.setData(label, LAST_SEGMENT_ROLE)
item.setData(prefix, PREFIX_ROLE)
items.append(item)
root_item.appendRows(items)
class ProjectSettingsBreadcrumbs(SettingsBreadcrumbs):
def reset(self):
root_item = self.invisibleRootItem()
rows = root_item.rowCount()
if rows > 0:
root_item.removeRows(0, rows)
if self.entity is None:
return
entities_by_path = self.entity.collect_static_entities_by_path()
self.entities_by_path = entities_by_path
items = []
for path in entities_by_path.keys():
if not path:
continue
path_items = path.split("/")
value = path
label = path_items.pop(-1)
prefix = "/".join(path_items)
if prefix:
prefix += "/"
item = QtGui.QStandardItem(value)
item.setData(label, LAST_SEGMENT_ROLE)
item.setData(prefix, PREFIX_ROLE)
items.append(item)
root_item.appendRows(items)
class BreadcrumbsProxy(QtCore.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
super(BreadcrumbsProxy, self).__init__(*args, **kwargs)
self._current_path = ""
def set_path_prefix(self, prefix):
path = prefix
if not prefix.endswith("/"):
path_items = path.split("/")
if len(path_items) == 1:
path = ""
else:
path_items.pop(-1)
path = "/".join(path_items) + "/"
if path == self._current_path:
return
self._current_path = prefix
self.invalidateFilter()
def filterAcceptsRow(self, row, parent):
index = self.sourceModel().index(row, 0, parent)
prefix_path = index.data(PREFIX_ROLE)
return prefix_path == self._current_path
class BreadcrumbsHintMenu(QtWidgets.QMenu):
def __init__(self, model, path_prefix, parent):
super(BreadcrumbsHintMenu, self).__init__(parent)
self._path_prefix = path_prefix
self._model = model
def showEvent(self, event):
self.clear()
self._model.set_path_prefix(self._path_prefix)
row_count = self._model.rowCount()
if row_count == 0:
action = self.addAction("* Nothing")
action.setData(".")
else:
for row in range(self._model.rowCount()):
index = self._model.index(row, 0)
label = index.data(LAST_SEGMENT_ROLE)
value = index.data(QtCore.Qt.EditRole)
action = self.addAction(label)
action.setData(value)
super(BreadcrumbsHintMenu, self).showEvent(event)
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 BreadcrumbsPathInput(QtWidgets.QLineEdit):
cancelled = QtCore.Signal()
confirmed = QtCore.Signal()
def __init__(self, model, proxy_model, parent):
super(BreadcrumbsPathInput, self).__init__(parent)
self.setObjectName("BreadcrumbsPathInput")
self.setFrame(False)
completer = QtWidgets.QCompleter(self)
completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
completer.setModel(proxy_model)
popup = completer.popup()
popup.setUniformItemSizes(True)
popup.setLayoutMode(QtWidgets.QListView.Batched)
self.setCompleter(completer)
completer.activated.connect(self._on_completer_activated)
self.textEdited.connect(self._on_text_change)
self._completer = completer
self._model = model
self._proxy_model = proxy_model
self._context_menu_visible = False
def set_model(self, model):
self._model = model
def event(self, event):
if (
event.type() == QtCore.QEvent.KeyPress
and event.key() == QtCore.Qt.Key_Tab
):
if self._model:
find_value = self.text() + "/"
if self._model.has_children(find_value):
self.insert("/")
else:
self._completer.popup().hide()
event.accept()
return True
return super(BreadcrumbsPathInput, self).event(event)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Escape:
self.cancelled.emit()
return
if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
self.confirmed.emit()
return
super(BreadcrumbsPathInput, self).keyPressEvent(event)
def focusOutEvent(self, event):
if not self._context_menu_visible:
self.cancelled.emit()
self._context_menu_visible = False
super(BreadcrumbsPathInput, self).focusOutEvent(event)
def contextMenuEvent(self, event):
self._context_menu_visible = True
super(BreadcrumbsPathInput, self).contextMenuEvent(event)
def _on_completer_activated(self, path):
self.confirmed.emit()
def _on_text_change(self, path):
self._proxy_model.set_path_prefix(path)
class BreadcrumbsButton(QtWidgets.QToolButton):
path_selected = QtCore.Signal(str)
def __init__(self, path, model, parent):
super(BreadcrumbsButton, self).__init__(parent)
self.setObjectName("BreadcrumbsButton")
path_prefix = path
if path:
path_prefix += "/"
self.setAutoRaise(True)
self.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
self.setMouseTracking(True)
if path:
self.setText(path.split("/")[-1])
else:
self.setProperty("empty", "1")
menu = BreadcrumbsHintMenu(model, path_prefix, self)
self.setMenu(menu)
# fixed size breadcrumbs
self.setMinimumSize(self.minimumSizeHint())
size_policy = self.sizePolicy()
size_policy.setVerticalPolicy(size_policy.Minimum)
self.setSizePolicy(size_policy)
menu.triggered.connect(self._on_menu_click)
self.clicked.connect(self._on_click)
self._path = path
self._path_prefix = path_prefix
self._model = model
self._menu = menu
def _on_click(self):
self.path_selected.emit(self._path)
def _on_menu_click(self, action):
item = action.data()
self.path_selected.emit(item)
class BreadcrumbsAddressBar(QtWidgets.QFrame):
"Windows Explorer-like address bar"
path_changed = QtCore.Signal(str)
path_edited = QtCore.Signal(str)
def __init__(self, parent=None):
super(BreadcrumbsAddressBar, self).__init__(parent)
self.setAutoFillBackground(True)
self.setFrameShape(self.StyledPanel)
# Edit presented path textually
proxy_model = BreadcrumbsProxy()
path_input = BreadcrumbsPathInput(None, proxy_model, self)
path_input.setVisible(False)
path_input.cancelled.connect(self._on_input_cancel)
path_input.confirmed.connect(self._on_input_confirm)
# Container for `crumbs_panel`
crumbs_container = QtWidgets.QWidget(self)
# Container for breadcrumbs
crumbs_panel = QtWidgets.QWidget(crumbs_container)
crumbs_panel.setObjectName("BreadcrumbsPanel")
crumbs_layout = QtWidgets.QHBoxLayout()
crumbs_layout.setContentsMargins(0, 0, 0, 0)
crumbs_layout.setSpacing(0)
crumbs_cont_layout = QtWidgets.QHBoxLayout(crumbs_container)
crumbs_cont_layout.setContentsMargins(0, 0, 0, 0)
crumbs_cont_layout.setSpacing(0)
crumbs_cont_layout.addWidget(crumbs_panel)
# Clicking on empty space to the right puts the bar into edit mode
switch_space = ClickableWidget(self)
crumb_panel_layout = QtWidgets.QHBoxLayout(crumbs_panel)
crumb_panel_layout.setContentsMargins(0, 0, 0, 0)
crumb_panel_layout.setSpacing(0)
crumb_panel_layout.addLayout(crumbs_layout, 0)
crumb_panel_layout.addWidget(switch_space, 1)
switch_space.clicked.connect(self.switch_space_mouse_up)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(path_input)
layout.addWidget(crumbs_container)
self.setMaximumHeight(path_input.height())
self.crumbs_layout = crumbs_layout
self.crumbs_panel = crumbs_panel
self.switch_space = switch_space
self.path_input = path_input
self.crumbs_container = crumbs_container
self._model = None
self._proxy_model = proxy_model
self._current_path = None
def set_model(self, model):
self._model = model
self.path_input.set_model(model)
self._proxy_model.setSourceModel(model)
def _on_input_confirm(self):
self.change_path(self.path_input.text())
def _on_input_cancel(self):
self._cancel_edit()
def _clear_crumbs(self):
while self.crumbs_layout.count():
widget = self.crumbs_layout.takeAt(0).widget()
if widget:
widget.deleteLater()
def _insert_crumb(self, path):
btn = BreadcrumbsButton(path, self._proxy_model, self.crumbs_panel)
self.crumbs_layout.insertWidget(0, btn)
btn.path_selected.connect(self._on_crumb_clicked)
def _on_crumb_clicked(self, path):
"Breadcrumb was clicked"
self.change_path(path)
def change_path(self, path):
if self._model and not self._model.is_valid_path(path):
self._show_address_field()
else:
self.set_path(path)
self.path_edited.emit(path)
def set_path(self, path):
if path is None or path == ".":
path = self._current_path
# exit edit mode
self._cancel_edit()
self._clear_crumbs()
self._current_path = path
self.path_input.setText(path)
path_items = [
item
for item in path.split("/")
if item
]
while path_items:
item = "/".join(path_items)
self._insert_crumb(item)
path_items.pop(-1)
self._insert_crumb("")
self.path_changed.emit(self._current_path)
def _cancel_edit(self):
"Set edit line text back to current path and switch to view mode"
# revert path
self.path_input.setText(self.path())
# switch back to breadcrumbs view
self._show_address_field(False)
def path(self):
"Get path displayed in this BreadcrumbsAddressBar"
return self._current_path
def switch_space_mouse_up(self):
"EVENT: switch_space mouse clicked"
self._show_address_field(True)
def _show_address_field(self, show=True):
"Show text address field"
self.crumbs_container.setVisible(not show)
self.path_input.setVisible(show)
if show:
self.path_input.setFocus()
self.path_input.selectAll()
def minimumSizeHint(self):
result = super(BreadcrumbsAddressBar, self).minimumSizeHint()
result.setHeight(self.path_input.minimumSizeHint().height())
return result

View file

@ -31,6 +31,11 @@ from openpype.settings.entities import (
from openpype.settings import SaveWarningExc
from .widgets import ProjectListWidget
from .breadcrumbs_widget import (
BreadcrumbsAddressBar,
SystemSettingsBreadcrumbs,
ProjectSettingsBreadcrumbs
)
from .base import GUIWidget
from .list_item_widget import ListWidget
@ -175,6 +180,16 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
scroll_widget = QtWidgets.QScrollArea(self)
scroll_widget.setObjectName("GroupWidget")
content_widget = QtWidgets.QWidget(scroll_widget)
breadcrumbs_label = QtWidgets.QLabel("Path:", content_widget)
breadcrumbs_widget = BreadcrumbsAddressBar(content_widget)
breadcrumbs_layout = QtWidgets.QHBoxLayout()
breadcrumbs_layout.setContentsMargins(5, 5, 5, 5)
breadcrumbs_layout.setSpacing(5)
breadcrumbs_layout.addWidget(breadcrumbs_label)
breadcrumbs_layout.addWidget(breadcrumbs_widget)
content_layout = QtWidgets.QVBoxLayout(content_widget)
content_layout.setContentsMargins(3, 3, 3, 3)
content_layout.setSpacing(5)
@ -183,40 +198,43 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
scroll_widget.setWidgetResizable(True)
scroll_widget.setWidget(content_widget)
configurations_widget = QtWidgets.QWidget(self)
footer_widget = QtWidgets.QWidget(configurations_widget)
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
refresh_icon = qtawesome.icon("fa.refresh", color="white")
refresh_btn = QtWidgets.QPushButton(footer_widget)
refresh_btn = QtWidgets.QPushButton(self)
refresh_btn.setIcon(refresh_icon)
footer_layout.addWidget(refresh_btn, 0)
footer_layout = QtWidgets.QHBoxLayout()
if self.user_role == "developer":
self._add_developer_ui(footer_layout)
save_btn = QtWidgets.QPushButton("Save", footer_widget)
require_restart_label = QtWidgets.QLabel(footer_widget)
save_btn = QtWidgets.QPushButton("Save", self)
require_restart_label = QtWidgets.QLabel(self)
require_restart_label.setAlignment(QtCore.Qt.AlignCenter)
footer_layout.addWidget(refresh_btn, 0)
footer_layout.addWidget(require_restart_label, 1)
footer_layout.addWidget(save_btn, 0)
configurations_layout = QtWidgets.QVBoxLayout(configurations_widget)
configurations_layout = QtWidgets.QVBoxLayout()
configurations_layout.setContentsMargins(0, 0, 0, 0)
configurations_layout.setSpacing(0)
configurations_layout.addWidget(scroll_widget, 1)
configurations_layout.addWidget(footer_widget, 0)
configurations_layout.addLayout(footer_layout, 0)
main_layout = QtWidgets.QHBoxLayout(self)
conf_wrapper_layout = QtWidgets.QHBoxLayout()
conf_wrapper_layout.setContentsMargins(0, 0, 0, 0)
conf_wrapper_layout.setSpacing(0)
conf_wrapper_layout.addLayout(configurations_layout, 1)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
main_layout.addWidget(configurations_widget, 1)
main_layout.addLayout(breadcrumbs_layout, 0)
main_layout.addLayout(conf_wrapper_layout, 1)
save_btn.clicked.connect(self._save)
refresh_btn.clicked.connect(self._on_refresh)
breadcrumbs_widget.path_edited.connect(self._on_path_edit)
self.save_btn = save_btn
self.refresh_btn = refresh_btn
@ -224,7 +242,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
self.scroll_widget = scroll_widget
self.content_layout = content_layout
self.content_widget = content_widget
self.configurations_widget = configurations_widget
self.breadcrumbs_widget = breadcrumbs_widget
self.breadcrumbs_model = None
self.conf_wrapper_layout = conf_wrapper_layout
self.main_layout = main_layout
self.ui_tweaks()
@ -232,6 +252,23 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
def ui_tweaks(self):
return
def _on_path_edit(self, path):
for input_field in self.input_fields:
if input_field.make_sure_is_visible(path, True):
break
def scroll_to(self, widget):
if widget:
# Process events which happened before ensurence
# - that is because some widgets could be not visible before
# this method was called and have incorrect size
QtWidgets.QApplication.processEvents()
# Scroll to widget
self.scroll_widget.ensureWidgetVisible(widget)
def set_path(self, path):
self.breadcrumbs_widget.set_path(path)
def _add_developer_ui(self, footer_layout):
modify_defaults_widget = QtWidgets.QWidget()
modify_defaults_checkbox = QtWidgets.QCheckBox(modify_defaults_widget)
@ -427,10 +464,19 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
def _on_reset_crash(self):
self.save_btn.setEnabled(False)
if self.breadcrumbs_model is not None:
self.breadcrumbs_model.set_entity(None)
def _on_reset_success(self):
if not self.save_btn.isEnabled():
self.save_btn.setEnabled(True)
if self.breadcrumbs_model is not None:
path = self.breadcrumbs_widget.path()
self.breadcrumbs_widget.set_path("")
self.breadcrumbs_model.set_entity(self.entity)
self.breadcrumbs_widget.change_path(path)
def add_children_gui(self):
for child_obj in self.entity.children:
item = self.create_ui_for_entity(self, child_obj, self)
@ -521,6 +567,10 @@ class SystemWidget(SettingsCategoryWidget):
self.modify_defaults_checkbox.setChecked(True)
self.modify_defaults_checkbox.setEnabled(False)
def ui_tweaks(self):
self.breadcrumbs_model = SystemSettingsBreadcrumbs()
self.breadcrumbs_widget.set_model(self.breadcrumbs_model)
def _on_modify_defaults(self):
if self.modify_defaults_checkbox.isChecked():
if not self.entity.is_in_defaults_state():
@ -535,9 +585,12 @@ class ProjectWidget(SettingsCategoryWidget):
self.project_name = None
def ui_tweaks(self):
self.breadcrumbs_model = ProjectSettingsBreadcrumbs()
self.breadcrumbs_widget.set_model(self.breadcrumbs_model)
project_list_widget = ProjectListWidget(self)
self.main_layout.insertWidget(0, project_list_widget, 0)
self.conf_wrapper_layout.insertWidget(0, project_list_widget, 0)
project_list_widget.project_changed.connect(self._on_project_change)

View file

@ -213,6 +213,26 @@ class DictConditionalWidget(BaseWidget):
else:
body_widget.hide_toolbox(hide_content=False)
def make_sure_is_visible(self, path, scroll_to):
if not path:
return False
entity_path = self.entity.path
if entity_path == path:
self.set_focus(scroll_to)
return True
if not path.startswith(entity_path):
return False
if self.body_widget and not self.body_widget.is_expanded():
self.body_widget.toggle_content(True)
for input_field in self.input_fields:
if input_field.make_sure_is_visible(path, scroll_to):
return True
return False
def add_widget_to_layout(self, widget, label=None):
if not widget.entity:
map_id = widget.id

View file

@ -1,12 +1,11 @@
from uuid import uuid4
from Qt import QtWidgets, QtCore
from Qt import QtWidgets, QtCore, QtGui
from .base import BaseWidget
from .widgets import (
ExpandingWidget,
IconButton,
SpacerWidget
IconButton
)
from openpype.tools.settings import (
BTN_FIXED_SIZE,
@ -15,6 +14,69 @@ from openpype.tools.settings import (
from openpype.settings.constants import KEY_REGEX
KEY_INPUT_TOOLTIP = (
"Keys can't be duplicated and may contain alphabetical character (a-Z)"
"\nnumerical characters (0-9) dash (\"-\") or underscore (\"_\")."
)
class PaintHelper:
cached_icons = {}
@classmethod
def _draw_image(cls, width, height, brush):
image = QtGui.QPixmap(width, height)
image.fill(QtCore.Qt.transparent)
icon_path_stroker = QtGui.QPainterPathStroker()
icon_path_stroker.setCapStyle(QtCore.Qt.RoundCap)
icon_path_stroker.setJoinStyle(QtCore.Qt.RoundJoin)
icon_path_stroker.setWidth(height / 5)
painter = QtGui.QPainter(image)
painter.setPen(QtCore.Qt.transparent)
painter.setBrush(brush)
rect = QtCore.QRect(0, 0, image.width(), image.height())
fifteenth = rect.height() / 15
# Left point
p1 = QtCore.QPoint(
rect.x() + (5 * fifteenth),
rect.y() + (9 * fifteenth)
)
# Middle bottom point
p2 = QtCore.QPoint(
rect.center().x(),
rect.y() + (11 * fifteenth)
)
# Top right point
p3 = QtCore.QPoint(
rect.x() + (10 * fifteenth),
rect.y() + (5 * fifteenth)
)
path = QtGui.QPainterPath(p1)
path.lineTo(p2)
path.lineTo(p3)
stroked_path = icon_path_stroker.createStroke(path)
painter.drawPath(stroked_path)
painter.end()
return image
@classmethod
def get_confirm_icon(cls, width, height):
key = "{}x{}-confirm_image".format(width, height)
icon = cls.cached_icons.get(key)
if icon is None:
image = cls._draw_image(width, height, QtCore.Qt.white)
icon = QtGui.QIcon(image)
cls.cached_icons[key] = icon
return icon
def create_add_btn(parent):
add_btn = QtWidgets.QPushButton("+", parent)
add_btn.setFocusPolicy(QtCore.Qt.ClickFocus)
@ -31,6 +93,19 @@ def create_remove_btn(parent):
return remove_btn
def create_confirm_btn(parent):
confirm_btn = QtWidgets.QPushButton(parent)
icon = PaintHelper.get_confirm_icon(
BTN_FIXED_SIZE, BTN_FIXED_SIZE
)
confirm_btn.setIcon(icon)
confirm_btn.setFocusPolicy(QtCore.Qt.ClickFocus)
confirm_btn.setProperty("btn-type", "tool-item")
confirm_btn.setFixedSize(BTN_FIXED_SIZE, BTN_FIXED_SIZE)
return confirm_btn
class ModifiableDictEmptyItem(QtWidgets.QWidget):
def __init__(self, entity_widget, store_as_list, parent):
super(ModifiableDictEmptyItem, self).__init__(parent)
@ -42,6 +117,8 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget):
self.is_duplicated = False
self.key_is_valid = store_as_list
self.confirm_btn = None
if self.collapsible_key:
self.create_collapsible_ui()
else:
@ -61,7 +138,6 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget):
def create_addible_ui(self):
add_btn = create_add_btn(self)
remove_btn = create_remove_btn(self)
spacer_widget = SpacerWidget(self)
remove_btn.setEnabled(False)
@ -70,13 +146,12 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget):
layout.setSpacing(3)
layout.addWidget(add_btn, 0)
layout.addWidget(remove_btn, 0)
layout.addWidget(spacer_widget, 1)
layout.addStretch(1)
add_btn.clicked.connect(self._on_add_clicked)
self.add_btn = add_btn
self.remove_btn = remove_btn
self.spacer_widget = spacer_widget
def _on_focus_lose(self):
if self.key_input.hasFocus() or self.key_label_input.hasFocus():
@ -111,7 +186,16 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget):
self.is_duplicated = self.entity_widget.is_key_duplicated(key)
key_input_state = ""
# Collapsible key and empty key are not invalid
if self.collapsible_key and self.key_input.text() == "":
key_value = self.key_input.text()
if self.confirm_btn is not None:
conf_disabled = (
key_value == ""
or not self.key_is_valid
or self.is_duplicated
)
self.confirm_btn.setEnabled(not conf_disabled)
if self.collapsible_key and key_value == "":
pass
elif self.is_duplicated or not self.key_is_valid:
key_input_state = "invalid"
@ -124,6 +208,7 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget):
def create_collapsible_ui(self):
key_input = QtWidgets.QLineEdit(self)
key_input.setObjectName("DictKey")
key_input.setToolTip(KEY_INPUT_TOOLTIP)
key_label_input = QtWidgets.QLineEdit(self)
@ -141,11 +226,15 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget):
key_input_label_widget = QtWidgets.QLabel("Key:", self)
key_label_input_label_widget = QtWidgets.QLabel("Label:", self)
confirm_btn = create_confirm_btn(self)
confirm_btn.setEnabled(False)
wrapper_widget = ExpandingWidget("", self)
wrapper_widget.add_widget_after_label(key_input_label_widget)
wrapper_widget.add_widget_after_label(key_input)
wrapper_widget.add_widget_after_label(key_label_input_label_widget)
wrapper_widget.add_widget_after_label(key_label_input)
wrapper_widget.add_widget_after_label(confirm_btn)
wrapper_widget.hide_toolbox()
layout = QtWidgets.QVBoxLayout(self)
@ -157,9 +246,12 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget):
key_input.returnPressed.connect(self._on_enter_press)
key_label_input.returnPressed.connect(self._on_enter_press)
confirm_btn.clicked.connect(self._on_enter_press)
self.key_input = key_input
self.key_label_input = key_label_input
self.wrapper_widget = wrapper_widget
self.confirm_btn = confirm_btn
class ModifiableDictItem(QtWidgets.QWidget):
@ -190,10 +282,14 @@ class ModifiableDictItem(QtWidgets.QWidget):
self.key_label_input = None
self.confirm_btn = None
if collapsible_key:
self.create_collapsible_ui()
else:
self.create_addible_ui()
self.key_input.setToolTip(KEY_INPUT_TOOLTIP)
self.update_style()
@property
@ -277,6 +373,9 @@ class ModifiableDictItem(QtWidgets.QWidget):
edit_btn.setProperty("btn-type", "tool-item-icon")
edit_btn.setFixedHeight(BTN_FIXED_SIZE)
confirm_btn = create_confirm_btn(self)
confirm_btn.setVisible(False)
remove_btn = create_remove_btn(self)
key_input_label_widget = QtWidgets.QLabel("Key:")
@ -286,6 +385,7 @@ class ModifiableDictItem(QtWidgets.QWidget):
wrapper_widget.add_widget_after_label(key_input)
wrapper_widget.add_widget_after_label(key_label_input_label_widget)
wrapper_widget.add_widget_after_label(key_label_input)
wrapper_widget.add_widget_after_label(confirm_btn)
wrapper_widget.add_widget_after_label(remove_btn)
key_input.textChanged.connect(self._on_key_change)
@ -295,6 +395,7 @@ class ModifiableDictItem(QtWidgets.QWidget):
key_label_input.returnPressed.connect(self._on_enter_press)
edit_btn.clicked.connect(self.on_edit_pressed)
confirm_btn.clicked.connect(self._on_enter_press)
remove_btn.clicked.connect(self.on_remove_clicked)
# Hide edit inputs
@ -310,6 +411,7 @@ class ModifiableDictItem(QtWidgets.QWidget):
self.key_label_input_label_widget = key_label_input_label_widget
self.wrapper_widget = wrapper_widget
self.edit_btn = edit_btn
self.confirm_btn = confirm_btn
self.remove_btn = remove_btn
self.content_widget = content_widget
@ -319,6 +421,9 @@ class ModifiableDictItem(QtWidgets.QWidget):
self.category_widget, self.entity, self
)
def make_sure_is_visible(self, *args, **kwargs):
return self.input_field.make_sure_is_visible(*args, **kwargs)
def get_style_state(self):
if self.is_invalid:
return "invalid"
@ -415,6 +520,14 @@ class ModifiableDictItem(QtWidgets.QWidget):
self.temp_key, key, self
)
self.temp_key = key
if self.confirm_btn is not None:
conf_disabled = (
key == ""
or not self.key_is_valid
or is_key_duplicated
)
self.confirm_btn.setEnabled(not conf_disabled)
if is_key_duplicated or not self.key_is_valid:
return
@ -434,7 +547,7 @@ class ModifiableDictItem(QtWidgets.QWidget):
key_value = self.key_input.text()
key_label_value = self.key_label_input.text()
if key_label_value:
label = "{} ({})".format(key_label_value, key_value)
label = "{} ({})".format(key_value, key_label_value)
else:
label = key_value
self.wrapper_widget.label_widget.setText(label)
@ -457,6 +570,7 @@ class ModifiableDictItem(QtWidgets.QWidget):
self.key_input.setVisible(enabled)
self.key_input_label_widget.setVisible(enabled)
self.key_label_input.setVisible(enabled)
self.confirm_btn.setVisible(enabled)
if not self.is_required:
self.remove_btn.setVisible(enabled)
if enabled:
@ -681,10 +795,6 @@ class DictMutableKeysWidget(BaseWidget):
def remove_key(self, widget):
key = self.entity.get_child_key(widget.entity)
self.entity.pop(key)
# Poping of key from entity should remove the entity and input field.
# this is kept for testing purposes.
if widget in self.input_fields:
self.remove_row(widget)
def change_key(self, new_key, widget):
if not new_key or widget.is_key_duplicated:
@ -751,6 +861,11 @@ class DictMutableKeysWidget(BaseWidget):
return input_field
def remove_row(self, widget):
if widget.is_key_duplicated:
new_key = widget.uuid_key
if new_key is None:
new_key = str(uuid4())
self.validate_key_duplication(widget.temp_key, new_key, widget)
self.input_fields.remove(widget)
self.content_layout.removeWidget(widget)
widget.deleteLater()
@ -834,7 +949,10 @@ class DictMutableKeysWidget(BaseWidget):
_input_field.set_entity_value()
else:
if input_field.key_value() != key:
if (
not input_field.is_key_duplicated
and input_field.key_value() != key
):
changed = True
input_field.set_key(key)
@ -846,6 +964,26 @@ class DictMutableKeysWidget(BaseWidget):
if changed:
self.on_shuffle()
def make_sure_is_visible(self, path, scroll_to):
if not path:
return False
entity_path = self.entity.path
if entity_path == path:
self.set_focus(scroll_to)
return True
if not path.startswith(entity_path):
return False
if self.body_widget and not self.body_widget.is_expanded():
self.body_widget.toggle_content(True)
for input_field in self.input_fields:
if input_field.make_sure_is_visible(path, scroll_to):
return True
return False
def set_entity_value(self):
while self.input_fields:
self.remove_row(self.input_fields[0])

View file

@ -6,8 +6,10 @@ from .widgets import (
ExpandingWidget,
NumberSpinBox,
GridLabelWidget,
ComboBox,
NiceCheckbox
SettingsComboBox,
NiceCheckbox,
SettingsPlainTextEdit,
SettingsLineEdit
)
from .multiselection_combobox import MultiSelectionComboBox
from .wrapper_widgets import (
@ -46,6 +48,7 @@ class DictImmutableKeysWidget(BaseWidget):
self._ui_item_base()
label = self.entity.label
self._direct_children_widgets = []
self._parent_widget_by_entity_id = {}
self._added_wrapper_ids = set()
self._prepare_entity_layouts(
@ -154,9 +157,41 @@ class DictImmutableKeysWidget(BaseWidget):
else:
body_widget.hide_toolbox(hide_content=False)
def make_sure_is_visible(self, path, scroll_to):
if not path:
return False
entity_path = self.entity.path
if entity_path == path:
self.set_focus(scroll_to)
return True
if not path.startswith(entity_path):
return False
is_checkbox_child = False
changed = False
for direct_child in self._direct_children_widgets:
if direct_child.make_sure_is_visible(path, scroll_to):
changed = True
if direct_child.entity is self.checkbox_child:
is_checkbox_child = True
break
# Change scroll to this widget
if is_checkbox_child:
self.scroll_to(self)
elif self.body_widget and not self.body_widget.is_expanded():
# Expand widget if is callapsible
self.body_widget.toggle_content(True)
return changed
def add_widget_to_layout(self, widget, label=None):
if self.checkbox_child and widget.entity is self.checkbox_child:
self.body_widget.add_widget_before_label(widget)
self._direct_children_widgets.append(widget)
return
if not widget.entity:
@ -172,6 +207,8 @@ class DictImmutableKeysWidget(BaseWidget):
self._added_wrapper_ids.add(wrapper.id)
return
self._direct_children_widgets.append(widget)
row = self.content_layout.rowCount()
if not label or isinstance(widget, WrapperWidget):
self.content_layout.addWidget(widget, row, 0, 1, 2)
@ -270,11 +307,8 @@ class BoolWidget(InputWidget):
height=checkbox_height, parent=self.content_widget
)
spacer = QtWidgets.QWidget(self.content_widget)
spacer.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.content_layout.addWidget(self.input_field, 0)
self.content_layout.addWidget(spacer, 1)
self.content_layout.addStretch(1)
self.setFocusProxy(self.input_field)
@ -297,9 +331,9 @@ class TextWidget(InputWidget):
def _add_inputs_to_layout(self):
multiline = self.entity.multiline
if multiline:
self.input_field = QtWidgets.QPlainTextEdit(self.content_widget)
self.input_field = SettingsPlainTextEdit(self.content_widget)
else:
self.input_field = QtWidgets.QLineEdit(self.content_widget)
self.input_field = SettingsLineEdit(self.content_widget)
placeholder_text = self.entity.placeholder_text
if placeholder_text:
@ -313,8 +347,12 @@ class TextWidget(InputWidget):
self.content_layout.addWidget(self.input_field, 1, **layout_kwargs)
self.input_field.focused_in.connect(self._on_input_focus)
self.input_field.textChanged.connect(self._on_value_change)
def _on_input_focus(self):
self.focused_in()
def _on_entity_change(self):
if self.entity.value != self.input_value():
self.set_entity_value()
@ -352,6 +390,10 @@ class NumberWidget(InputWidget):
self.content_layout.addWidget(self.input_field, 1)
self.input_field.valueChanged.connect(self._on_value_change)
self.input_field.focused_in.connect(self._on_input_focus)
def _on_input_focus(self):
self.focused_in()
def _on_entity_change(self):
if self.entity.value != self.input_field.value():
@ -366,7 +408,7 @@ class NumberWidget(InputWidget):
self.entity.set(self.input_field.value())
class RawJsonInput(QtWidgets.QPlainTextEdit):
class RawJsonInput(SettingsPlainTextEdit):
tab_length = 4
def __init__(self, valid_type, *args, **kwargs):
@ -428,15 +470,18 @@ class RawJsonWidget(InputWidget):
QtWidgets.QSizePolicy.Minimum,
QtWidgets.QSizePolicy.MinimumExpanding
)
self.setFocusProxy(self.input_field)
self.content_layout.addWidget(
self.input_field, 1, alignment=QtCore.Qt.AlignTop
)
self.input_field.focused_in.connect(self._on_input_focus)
self.input_field.textChanged.connect(self._on_value_change)
def _on_input_focus(self):
self.focused_in()
def set_entity_value(self):
self.input_field.set_value(self.entity.value)
self._is_invalid = self.input_field.has_invalid_value()
@ -470,7 +515,7 @@ class EnumeratorWidget(InputWidget):
)
else:
self.input_field = ComboBox(self.content_widget)
self.input_field = SettingsComboBox(self.content_widget)
for enum_item in self.entity.enum_items:
for value, label in enum_item.items():
@ -480,8 +525,12 @@ class EnumeratorWidget(InputWidget):
self.setFocusProxy(self.input_field)
self.input_field.focused_in.connect(self._on_input_focus)
self.input_field.value_changed.connect(self._on_value_change)
def _on_input_focus(self):
self.focused_in()
def _on_entity_change(self):
if self.entity.value != self.input_field.value():
self.set_entity_value()
@ -562,6 +611,9 @@ class PathWidget(BaseWidget):
def set_entity_value(self):
self.input_field.set_entity_value()
def make_sure_is_visible(self, *args, **kwargs):
return self.input_field.make_sure_is_visible(*args, **kwargs)
def hierarchical_style_update(self):
self.update_style()
self.input_field.hierarchical_style_update()
@ -632,14 +684,19 @@ class PathWidget(BaseWidget):
class PathInputWidget(InputWidget):
def _add_inputs_to_layout(self):
self.input_field = QtWidgets.QLineEdit(self.content_widget)
self.input_field = SettingsLineEdit(self.content_widget)
placeholder = self.entity.placeholder_text
if placeholder:
self.input_field.setPlaceholderText(placeholder)
self.setFocusProxy(self.input_field)
self.content_layout.addWidget(self.input_field)
self.input_field.textChanged.connect(self._on_value_change)
self.input_field.focused_in.connect(self._on_input_focus)
def _on_input_focus(self):
self.focused_in()
def _on_entity_change(self):
if self.entity.value != self.input_value():

View file

@ -18,8 +18,6 @@ class EmptyListItem(QtWidgets.QWidget):
add_btn = QtWidgets.QPushButton("+", self)
remove_btn = QtWidgets.QPushButton("-", self)
spacer_widget = QtWidgets.QWidget(self)
spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
add_btn.setFocusPolicy(QtCore.Qt.ClickFocus)
remove_btn.setEnabled(False)
@ -35,13 +33,12 @@ class EmptyListItem(QtWidgets.QWidget):
layout.setSpacing(3)
layout.addWidget(add_btn, 0)
layout.addWidget(remove_btn, 0)
layout.addWidget(spacer_widget, 1)
layout.addStretch(1)
add_btn.clicked.connect(self._on_add_clicked)
self.add_btn = add_btn
self.remove_btn = remove_btn
self.spacer_widget = spacer_widget
def _on_add_clicked(self):
self.entity_widget.add_new_item()
@ -101,12 +98,6 @@ class ListItem(QtWidgets.QWidget):
self.category_widget, self.entity, self
)
spacer_widget = QtWidgets.QWidget(self)
spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
spacer_widget.setVisible(False)
layout.addWidget(spacer_widget, 1)
layout.addWidget(up_btn, 0)
layout.addWidget(down_btn, 0)
@ -115,8 +106,6 @@ class ListItem(QtWidgets.QWidget):
self.up_btn = up_btn
self.down_btn = down_btn
self.spacer_widget = spacer_widget
self._row = -1
self._is_last = False
@ -129,6 +118,9 @@ class ListItem(QtWidgets.QWidget):
*args, **kwargs
)
def make_sure_is_visible(self, *args, **kwargs):
return self.input_field.make_sure_is_visible(*args, **kwargs)
@property
def is_invalid(self):
return self.input_field.is_invalid
@ -275,6 +267,26 @@ class ListWidget(InputWidget):
invalid.extend(input_field.get_invalid())
return invalid
def make_sure_is_visible(self, path, scroll_to):
if not path:
return False
entity_path = self.entity.path
if entity_path == path:
self.set_focus(scroll_to)
return True
if not path.startswith(entity_path):
return False
if self.body_widget and not self.body_widget.is_expanded():
self.body_widget.toggle_content(True)
for input_field in self.input_fields:
if input_field.make_sure_is_visible(path, scroll_to):
return True
return False
def _on_entity_change(self):
# TODO do less inefficient
childen_order = []

View file

@ -65,6 +65,21 @@ class ListStrictWidget(BaseWidget):
invalid.extend(input_field.get_invalid())
return invalid
def make_sure_is_visible(self, path, scroll_to):
if not path:
return False
entity_path = self.entity.path
if entity_path == path:
self.set_focus(scroll_to)
return True
if path.startswith(entity_path):
for input_field in self.input_fields:
if input_field.make_sure_is_visible(path, scroll_to):
return True
return False
def add_widget_to_layout(self, widget, label=None):
# Horizontally added children
if self.entity.is_horizontal:

View file

@ -21,6 +21,8 @@ class ComboItemDelegate(QtWidgets.QStyledItemDelegate):
class MultiSelectionComboBox(QtWidgets.QComboBox):
value_changed = QtCore.Signal()
focused_in = QtCore.Signal()
ignored_keys = {
QtCore.Qt.Key_Up,
QtCore.Qt.Key_Down,
@ -56,6 +58,10 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
self.lines = {}
self.item_height = None
def focusInEvent(self, event):
self.focused_in.emit()
return super(MultiSelectionComboBox, self).focusInEvent(event)
def mousePressEvent(self, event):
"""Reimplemented."""
self._popup_is_shown = False

View file

@ -388,4 +388,32 @@ QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed {
QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active {
background: #3d8ec9;
}
}
#BreadcrumbsPathInput {
padding: 2px;
font-size: 9pt;
}
#BreadcrumbsButton {
padding-right: 12px;
font-size: 9pt;
}
#BreadcrumbsButton[empty="1"] {
padding-right: 0px;
}
#BreadcrumbsButton::menu-button {
width: 12px;
background: rgba(127, 127, 127, 60);
}
#BreadcrumbsButton::menu-button:hover {
background: rgba(127, 127, 127, 90);
}
#BreadcrumbsPanel {
border: 1px solid #4e5254;
border-radius: 5px;
background: #21252B;;
}

View file

@ -9,6 +9,22 @@ from avalon.mongodb import (
from openpype.settings.lib import get_system_settings
class SettingsLineEdit(QtWidgets.QLineEdit):
focused_in = QtCore.Signal()
def focusInEvent(self, event):
super(SettingsLineEdit, self).focusInEvent(event)
self.focused_in.emit()
class SettingsPlainTextEdit(QtWidgets.QPlainTextEdit):
focused_in = QtCore.Signal()
def focusInEvent(self, event):
super(SettingsPlainTextEdit, self).focusInEvent(event)
self.focused_in.emit()
class ShadowWidget(QtWidgets.QWidget):
def __init__(self, message, parent):
super(ShadowWidget, self).__init__(parent)
@ -70,6 +86,8 @@ class IconButton(QtWidgets.QPushButton):
class NumberSpinBox(QtWidgets.QDoubleSpinBox):
focused_in = QtCore.Signal()
def __init__(self, *args, **kwargs):
min_value = kwargs.pop("minimum", -99999)
max_value = kwargs.pop("maximum", 99999)
@ -80,6 +98,10 @@ class NumberSpinBox(QtWidgets.QDoubleSpinBox):
self.setMinimum(min_value)
self.setMaximum(max_value)
def focusInEvent(self, event):
super(NumberSpinBox, self).focusInEvent(event)
self.focused_in.emit()
def wheelEvent(self, event):
if self.hasFocus():
super(NumberSpinBox, self).wheelEvent(event)
@ -93,18 +115,23 @@ class NumberSpinBox(QtWidgets.QDoubleSpinBox):
return output
class ComboBox(QtWidgets.QComboBox):
class SettingsComboBox(QtWidgets.QComboBox):
value_changed = QtCore.Signal()
focused_in = QtCore.Signal()
def __init__(self, *args, **kwargs):
super(ComboBox, self).__init__(*args, **kwargs)
super(SettingsComboBox, self).__init__(*args, **kwargs)
self.currentIndexChanged.connect(self._on_change)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
def wheelEvent(self, event):
if self.hasFocus():
return super(ComboBox, self).wheelEvent(event)
return super(SettingsComboBox, self).wheelEvent(event)
def focusInEvent(self, event):
self.focused_in.emit()
return super(SettingsComboBox, self).focusInEvent(event)
def _on_change(self, *args, **kwargs):
self.value_changed.emit()
@ -160,15 +187,13 @@ class ExpandingWidget(QtWidgets.QWidget):
after_label_layout = QtWidgets.QHBoxLayout(after_label_widget)
after_label_layout.setContentsMargins(0, 0, 0, 0)
spacer_widget = QtWidgets.QWidget(side_line_widget)
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(before_label_widget)
side_line_layout.addWidget(label_widget)
side_line_layout.addWidget(after_label_widget)
side_line_layout.addWidget(spacer_widget, 1)
side_line_layout.addStretch(1)
top_part_layout = QtWidgets.QHBoxLayout(top_part)
top_part_layout.setContentsMargins(0, 0, 0, 0)
@ -176,7 +201,6 @@ class ExpandingWidget(QtWidgets.QWidget):
before_label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
after_label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
spacer_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
@ -215,6 +239,9 @@ class ExpandingWidget(QtWidgets.QWidget):
self.main_layout.addWidget(content_widget)
self.content_widget = content_widget
def is_expanded(self):
return self.button_toggle.isChecked()
def _btn_clicked(self):
self.toggle_content(self.button_toggle.isChecked())
@ -341,31 +368,21 @@ class GridLabelWidget(QtWidgets.QWidget):
self.properties = {}
label_widget = QtWidgets.QLabel(label, self)
label_proxy_layout = QtWidgets.QHBoxLayout()
label_proxy_layout.setContentsMargins(0, 0, 0, 0)
label_proxy_layout.setSpacing(0)
label_proxy_layout.addWidget(label_widget, 0, QtCore.Qt.AlignRight)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 2, 0, 0)
layout.setSpacing(0)
label_proxy = QtWidgets.QWidget(self)
layout.addLayout(label_proxy_layout, 0)
layout.addStretch(1)
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)
label_proxy.setAttribute(QtCore.Qt.WA_TranslucentBackground)
label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.label_widget = label_widget
@ -380,6 +397,8 @@ class GridLabelWidget(QtWidgets.QWidget):
def mouseReleaseEvent(self, event):
if self.input_field:
if event and event.button() == QtCore.Qt.LeftButton:
self.input_field.focused_in()
return self.input_field.show_actions_menu(event)
return super(GridLabelWidget, self).mouseReleaseEvent(event)

View file

@ -19,6 +19,14 @@ class WrapperWidget(QtWidgets.QWidget):
self.create_ui()
def make_sure_is_visible(self, *args, **kwargs):
changed = False
for input_field in self.input_fields:
if input_field.make_sure_is_visible(*args, **kwargs):
changed = True
break
return changed
def create_ui(self):
raise NotImplementedError(
"{} does not have implemented `create_ui`.".format(
@ -89,6 +97,14 @@ class CollapsibleWrapper(WrapperWidget):
else:
body_widget.hide_toolbox(hide_content=False)
def make_sure_is_visible(self, *args, **kwargs):
result = super(CollapsibleWrapper, self).make_sure_is_visible(
*args, **kwargs
)
if result:
self.body_widget.toggle_content(True)
return result
def add_widget_to_layout(self, widget, label=None):
self.input_fields.append(widget)

View file

@ -50,8 +50,18 @@ function Install-Poetry() {
Write-Host "Installing Poetry ... "
$python = "python"
if (Get-Command "pyenv" -ErrorAction SilentlyContinue) {
if (-not (Test-Path -PathType Leaf -Path "$($openpype_root)\.python-version")) {
$result = & pyenv global
if ($result -eq "no global version configured") {
Write-Host "!!! " -NoNewline -ForegroundColor Red
Write-Host "Using pyenv but having no local or global version of Python set."
Exit-WithCode 1
}
}
$python = & pyenv which python
}
$env:POETRY_HOME="$openpype_root\.poetry"
(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | & $($python) -
}

View file

@ -55,9 +55,9 @@ def inject_openpype_environment(deadlinePlugin):
"AVALON_TASK, AVALON_APP_NAME"
raise RuntimeError(msg)
print("args::{}".format(args))
print("args:::{}".format(args))
exit_code = subprocess.call(args, shell=True)
exit_code = subprocess.call(args, cwd=os.path.dirname(openpype_app))
if exit_code != 0:
raise RuntimeError("Publishing failed, check worker's log")

View file

@ -87,6 +87,26 @@ When you publish your model with top group named like `foo_GRP` it will fail. Bu
All regexes used here are in Python variant.
:::
### Maya > Deadline submitter
This plugin provides connection between Maya and Deadline. It is using [Deadline Webservice](https://docs.thinkboxsoftware.com/products/deadline/10.0/1_User%20Manual/manual/web-service.html) to submit jobs to farm.
![Maya > Deadline Settings](assets/maya-admin_submit_maya_job_to_deadline.png)
You can set various aspects of scene submission to farm with per-project settings in **Setting UI**.
- **Optional** will mark sumission plugin optional
- **Active** will enable/disable plugin
- **Tile Assembler Plugin** will set what should be used to assemble tiles on Deadline. Either **Open Image IO** will be used
or Deadlines **Draft Tile Assembler**.
- **Use Published scene** enable to render from published scene instead of scene in work area. Rendering from published files is much safer.
- **Use Asset dependencies** will mark job pending on farm until asset dependencies are fulfilled - for example Deadline will wait for scene file to be synced to cloud, etc.
- **Group name** use specific Deadline group for the job.
- **Limit Groups** use these Deadline Limit groups for the job.
- **Additional `JobInfo` data** JSON of additional Deadline options that will be embedded in `JobInfo` part of the submission data.
- **Additional `PluginInfo` data** JSON of additional Deadline options that will be embedded in `PluginInfo` part of the submission data.
- **Scene patches** - configure mechanism to add additional lines to published Maya Ascii scene files before they are used for rendering.
This is useful to fix some specific renderer glitches and advanced hacking of Maya Scene files. `Patch name` is label for patch for easier orientation.
`Patch regex` is regex used to find line in file, after `Patch line` string is inserted. Note that you need to add line ending.
## Custom Menu
You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**.
![Custom menu definition](assets/maya-admin_scriptsmenu.png)
@ -94,4 +114,9 @@ You can add your custom tools menu into Maya by extending definitions in **Maya
:::note Work in progress
This is still work in progress. Menu definition will be handled more friendly with widgets and not
raw json.
:::
:::
## Multiplatform path mapping
You can configure path mapping using Maya `dirmap` command. This will add bi-directional mapping between
list of paths specified in **Settings**. You can find it in **Settings -> Project Settings -> Maya -> Maya Directory Mapping**
![Dirmap settings](assets/maya-admin_dirmap_settings.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 582 151" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-654,-2331)">
<g id="pypeclub_black" transform="matrix(0.775972,0,0,0.911492,654.799,1794.12)">
<g transform="matrix(1,0,0,1,-987,-2015)">
<g id="pypeclub_black" transform="matrix(0.775972,0,0,0.911492,987.586,1477.95)">
<rect x="0" y="589.923" width="748.802" height="164.565" style="fill:none;"/>
<g transform="matrix(2.7183,0,0,1.24969,-3.76111,624.796)">
<g id="PYPE" transform="matrix(1,0,0,1,-105.172,0)">
@ -20,7 +20,21 @@
</g>
</g>
<g transform="matrix(0.474085,0,0,0.877899,142.749,-0.167247)">
<text x="2.312px" y="79.899px" style="font-family:'Poppins-Light', 'Poppins';font-weight:300;font-size:100px;">.club</text>
<g transform="matrix(100,0,0,100,2.312,79.8987)">
<path d="M0.092,0.005C0.077,0.005 0.065,-0 0.056,-0.01C0.046,-0.02 0.041,-0.032 0.041,-0.047C0.041,-0.062 0.046,-0.074 0.056,-0.084C0.065,-0.093 0.077,-0.098 0.092,-0.098C0.106,-0.098 0.118,-0.093 0.128,-0.084C0.137,-0.074 0.142,-0.062 0.142,-0.047C0.142,-0.032 0.137,-0.02 0.128,-0.01C0.118,-0 0.106,0.005 0.092,0.005Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(100,0,0,100,20.5107,79.8987)">
<path d="M0.048,-0.273C0.048,-0.33 0.059,-0.379 0.082,-0.422C0.105,-0.464 0.136,-0.497 0.176,-0.52C0.216,-0.543 0.262,-0.554 0.313,-0.554C0.38,-0.554 0.436,-0.537 0.48,-0.504C0.523,-0.471 0.551,-0.425 0.564,-0.368L0.489,-0.368C0.48,-0.407 0.46,-0.438 0.429,-0.461C0.398,-0.483 0.359,-0.494 0.313,-0.494C0.276,-0.494 0.243,-0.486 0.214,-0.469C0.185,-0.452 0.162,-0.428 0.145,-0.395C0.128,-0.362 0.119,-0.321 0.119,-0.273C0.119,-0.225 0.128,-0.184 0.145,-0.151C0.162,-0.118 0.185,-0.093 0.214,-0.076C0.243,-0.059 0.276,-0.051 0.313,-0.051C0.359,-0.051 0.398,-0.062 0.429,-0.085C0.46,-0.107 0.48,-0.138 0.489,-0.178L0.564,-0.178C0.551,-0.122 0.523,-0.077 0.479,-0.043C0.435,-0.009 0.38,0.008 0.313,0.008C0.262,0.008 0.216,-0.004 0.176,-0.027C0.136,-0.05 0.105,-0.082 0.082,-0.125C0.059,-0.167 0.048,-0.216 0.048,-0.273Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(100,0,0,100,81.8095,79.8987)">
<rect x="0.08" y="-0.74" width="0.07" height="0.74" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(100,0,0,100,104.708,79.8987)">
<path d="M0.553,-0.546L0.553,-0L0.483,-0L0.483,-0.096C0.467,-0.062 0.442,-0.036 0.409,-0.018C0.376,-0 0.338,0.009 0.297,0.009C0.232,0.009 0.178,-0.011 0.137,-0.051C0.096,-0.092 0.075,-0.15 0.075,-0.227L0.075,-0.546L0.144,-0.546L0.144,-0.235C0.144,-0.176 0.159,-0.13 0.189,-0.099C0.218,-0.068 0.259,-0.052 0.31,-0.052C0.363,-0.052 0.405,-0.069 0.436,-0.102C0.467,-0.135 0.483,-0.184 0.483,-0.249L0.483,-0.546L0.553,-0.546Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(100,0,0,100,168.007,79.8987)">
<path d="M0.149,-0.425C0.167,-0.463 0.195,-0.494 0.233,-0.518C0.27,-0.542 0.315,-0.554 0.366,-0.554C0.416,-0.554 0.461,-0.543 0.5,-0.52C0.539,-0.497 0.57,-0.464 0.593,-0.422C0.615,-0.379 0.626,-0.33 0.626,-0.274C0.626,-0.218 0.615,-0.169 0.593,-0.126C0.57,-0.083 0.539,-0.05 0.5,-0.027C0.46,-0.004 0.415,0.008 0.366,0.008C0.314,0.008 0.269,-0.004 0.232,-0.028C0.194,-0.051 0.166,-0.082 0.149,-0.12L0.149,-0L0.08,-0L0.08,-0.74L0.149,-0.74L0.149,-0.425ZM0.555,-0.274C0.555,-0.319 0.546,-0.359 0.529,-0.392C0.511,-0.425 0.487,-0.45 0.456,-0.467C0.425,-0.484 0.391,-0.493 0.352,-0.493C0.315,-0.493 0.281,-0.484 0.25,-0.466C0.219,-0.448 0.194,-0.422 0.176,-0.389C0.158,-0.356 0.149,-0.317 0.149,-0.273C0.149,-0.229 0.158,-0.19 0.176,-0.157C0.194,-0.124 0.219,-0.098 0.25,-0.08C0.281,-0.062 0.315,-0.053 0.352,-0.053C0.391,-0.053 0.425,-0.062 0.456,-0.08C0.487,-0.097 0.511,-0.123 0.529,-0.157C0.546,-0.19 0.555,-0.229 0.555,-0.274Z" style="fill-rule:nonzero;"/>
</g>
</g>
</g>
</g>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Before After
Before After

View file

@ -1,26 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 582 151" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-35,-2135)">
<g id="pypeclub_color_white" transform="matrix(0.775972,0,0,0.911492,35.6167,1597.77)">
<g transform="matrix(1,0,0,1,-987,-1797)">
<g id="pypeclub_color_white" transform="matrix(0.775972,0,0,0.911492,987.586,1259.42)">
<rect x="0" y="589.923" width="748.802" height="164.565" style="fill:none;"/>
<g transform="matrix(2.7183,0,0,1.24969,-3.76111,624.796)">
<g id="PYPE" transform="matrix(1,0,0,1,-105.172,0)">
<g transform="matrix(47.4085,0,0,87.7899,124.81,70.8632)">
<path d="M0.629,-0.465C0.629,-0.42 0.619,-0.38 0.598,-0.344C0.577,-0.307 0.547,-0.278 0.506,-0.257C0.466,-0.236 0.417,-0.225 0.36,-0.225L0.272,-0.225L0.272,-0.05C0.272,-0.037 0.267,-0.024 0.257,-0.015C0.248,-0.005 0.235,0 0.222,0L0.1,0C0.072,0 0.05,-0.022 0.05,-0.05L0.05,-0.658C0.05,-0.686 0.072,-0.708 0.1,-0.708L0.36,-0.708C0.447,-0.708 0.513,-0.686 0.56,-0.642C0.606,-0.598 0.629,-0.539 0.629,-0.465ZM0.335,-0.4C0.381,-0.4 0.404,-0.422 0.404,-0.465C0.404,-0.508 0.381,-0.53 0.335,-0.53L0.272,-0.53L0.272,-0.4L0.335,-0.4Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
<path d="M0.629,-0.465C0.629,-0.42 0.619,-0.38 0.598,-0.344C0.577,-0.307 0.547,-0.278 0.506,-0.257C0.466,-0.236 0.417,-0.225 0.36,-0.225L0.272,-0.225L0.272,-0.05C0.272,-0.037 0.267,-0.024 0.257,-0.015C0.248,-0.005 0.235,0 0.222,0L0.1,-0C0.072,0 0.05,-0.022 0.05,-0.05L0.05,-0.658C0.05,-0.686 0.072,-0.708 0.1,-0.708L0.36,-0.708C0.447,-0.708 0.513,-0.686 0.56,-0.642C0.606,-0.598 0.629,-0.539 0.629,-0.465ZM0.335,-0.4C0.381,-0.4 0.404,-0.422 0.404,-0.465C0.404,-0.508 0.381,-0.53 0.335,-0.53L0.272,-0.53L0.272,-0.4L0.335,-0.4Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
</g>
<g transform="matrix(47.4085,0,0,87.7899,155.577,70.8632)">
<path d="M0.648,-0.708C0.665,-0.708 0.682,-0.699 0.691,-0.684C0.7,-0.669 0.7,-0.651 0.692,-0.635C0.622,-0.498 0.476,-0.215 0.476,-0.215L0.476,-0.05C0.476,-0.022 0.454,0 0.426,0L0.304,0C0.276,0 0.254,-0.022 0.254,-0.05L0.254,-0.215C0.254,-0.215 0.108,-0.498 0.038,-0.635C0.03,-0.651 0.03,-0.669 0.039,-0.684C0.048,-0.699 0.065,-0.708 0.082,-0.708L0.222,-0.708C0.241,-0.708 0.259,-0.696 0.267,-0.679C0.297,-0.612 0.367,-0.457 0.367,-0.457C0.367,-0.457 0.437,-0.612 0.467,-0.679C0.475,-0.696 0.493,-0.708 0.512,-0.708L0.648,-0.708Z" style="fill:url(#_Linear2);fill-rule:nonzero;"/>
<path d="M0.648,-0.708C0.665,-0.708 0.682,-0.699 0.691,-0.684C0.7,-0.669 0.7,-0.651 0.692,-0.635C0.622,-0.498 0.476,-0.215 0.476,-0.215L0.476,-0.05C0.476,-0.022 0.454,0 0.426,0L0.304,-0C0.276,0 0.254,-0.022 0.254,-0.05L0.254,-0.215C0.254,-0.215 0.108,-0.498 0.038,-0.635C0.03,-0.651 0.03,-0.669 0.039,-0.684C0.048,-0.699 0.065,-0.708 0.082,-0.708L0.222,-0.708C0.241,-0.708 0.259,-0.696 0.267,-0.679C0.297,-0.612 0.367,-0.457 0.367,-0.457C0.367,-0.457 0.437,-0.612 0.467,-0.679C0.475,-0.696 0.493,-0.708 0.512,-0.708L0.648,-0.708Z" style="fill:url(#_Linear2);fill-rule:nonzero;"/>
</g>
<g transform="matrix(47.4085,0,0,87.7899,190.185,70.8632)">
<path d="M0.629,-0.465C0.629,-0.42 0.619,-0.38 0.598,-0.344C0.577,-0.307 0.547,-0.278 0.506,-0.257C0.466,-0.236 0.417,-0.225 0.36,-0.225L0.272,-0.225L0.272,-0.05C0.272,-0.037 0.267,-0.024 0.257,-0.015C0.248,-0.005 0.235,0 0.222,0L0.1,0C0.072,0 0.05,-0.022 0.05,-0.05L0.05,-0.658C0.05,-0.686 0.072,-0.708 0.1,-0.708L0.36,-0.708C0.447,-0.708 0.513,-0.686 0.56,-0.642C0.606,-0.598 0.629,-0.539 0.629,-0.465ZM0.335,-0.4C0.381,-0.4 0.404,-0.422 0.404,-0.465C0.404,-0.508 0.381,-0.53 0.335,-0.53L0.272,-0.53L0.272,-0.4L0.335,-0.4Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
<path d="M0.629,-0.465C0.629,-0.42 0.619,-0.38 0.598,-0.344C0.577,-0.307 0.547,-0.278 0.506,-0.257C0.466,-0.236 0.417,-0.225 0.36,-0.225L0.272,-0.225L0.272,-0.05C0.272,-0.037 0.267,-0.024 0.257,-0.015C0.248,-0.005 0.235,0 0.222,0L0.1,-0C0.072,0 0.05,-0.022 0.05,-0.05L0.05,-0.658C0.05,-0.686 0.072,-0.708 0.1,-0.708L0.36,-0.708C0.447,-0.708 0.513,-0.686 0.56,-0.642C0.606,-0.598 0.629,-0.539 0.629,-0.465ZM0.335,-0.4C0.381,-0.4 0.404,-0.422 0.404,-0.465C0.404,-0.508 0.381,-0.53 0.335,-0.53L0.272,-0.53L0.272,-0.4L0.335,-0.4Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
</g>
<g transform="matrix(47.4085,0,0,87.7899,220.953,70.8632)">
<path d="M0.272,-0.531L0.272,-0.444L0.492,-0.444L0.492,-0.277L0.272,-0.277L0.272,-0.177L0.472,-0.177C0.485,-0.177 0.498,-0.172 0.507,-0.162C0.517,-0.153 0.522,-0.14 0.522,-0.127L0.522,-0.05C0.522,-0.037 0.517,-0.024 0.507,-0.015C0.498,-0.005 0.485,0 0.472,-0L0.1,0C0.072,0 0.05,-0.022 0.05,-0.05L0.05,-0.658C0.05,-0.686 0.072,-0.708 0.1,-0.708L0.472,-0.708C0.485,-0.708 0.498,-0.703 0.507,-0.693C0.517,-0.684 0.522,-0.671 0.522,-0.658L0.522,-0.581C0.522,-0.568 0.517,-0.555 0.507,-0.546C0.498,-0.536 0.485,-0.531 0.472,-0.531L0.272,-0.531Z" style="fill:url(#_Linear4);fill-rule:nonzero;"/>
</g>
</g>
<g transform="matrix(0.474085,0,0,0.877899,142.749,-0.167247)">
<text x="2.312px" y="79.899px" style="font-family:'Poppins-Light', 'Poppins';font-weight:300;font-size:100px;fill:white;">.club</text>
<g transform="matrix(100,0,0,100,2.312,79.8987)">
<path d="M0.092,0.005C0.077,0.005 0.065,-0 0.056,-0.01C0.046,-0.02 0.041,-0.032 0.041,-0.047C0.041,-0.062 0.046,-0.074 0.056,-0.084C0.065,-0.093 0.077,-0.098 0.092,-0.098C0.106,-0.098 0.118,-0.093 0.128,-0.084C0.137,-0.074 0.142,-0.062 0.142,-0.047C0.142,-0.032 0.137,-0.02 0.128,-0.01C0.118,-0 0.106,0.005 0.092,0.005Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(100,0,0,100,20.5107,79.8987)">
<path d="M0.048,-0.273C0.048,-0.33 0.059,-0.379 0.082,-0.422C0.105,-0.464 0.136,-0.497 0.176,-0.52C0.216,-0.543 0.262,-0.554 0.313,-0.554C0.38,-0.554 0.436,-0.537 0.48,-0.504C0.523,-0.471 0.551,-0.425 0.564,-0.368L0.489,-0.368C0.48,-0.407 0.46,-0.438 0.429,-0.461C0.398,-0.483 0.359,-0.494 0.313,-0.494C0.276,-0.494 0.243,-0.486 0.214,-0.469C0.185,-0.452 0.162,-0.428 0.145,-0.395C0.128,-0.362 0.119,-0.321 0.119,-0.273C0.119,-0.225 0.128,-0.184 0.145,-0.151C0.162,-0.118 0.185,-0.093 0.214,-0.076C0.243,-0.059 0.276,-0.051 0.313,-0.051C0.359,-0.051 0.398,-0.062 0.429,-0.085C0.46,-0.107 0.48,-0.138 0.489,-0.178L0.564,-0.178C0.551,-0.122 0.523,-0.077 0.479,-0.043C0.435,-0.009 0.38,0.008 0.313,0.008C0.262,0.008 0.216,-0.004 0.176,-0.027C0.136,-0.05 0.105,-0.082 0.082,-0.125C0.059,-0.167 0.048,-0.216 0.048,-0.273Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(100,0,0,100,81.8095,79.8987)">
<rect x="0.08" y="-0.74" width="0.07" height="0.74" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(100,0,0,100,104.708,79.8987)">
<path d="M0.553,-0.546L0.553,-0L0.483,-0L0.483,-0.096C0.467,-0.062 0.442,-0.036 0.409,-0.018C0.376,-0 0.338,0.009 0.297,0.009C0.232,0.009 0.178,-0.011 0.137,-0.051C0.096,-0.092 0.075,-0.15 0.075,-0.227L0.075,-0.546L0.144,-0.546L0.144,-0.235C0.144,-0.176 0.159,-0.13 0.189,-0.099C0.218,-0.068 0.259,-0.052 0.31,-0.052C0.363,-0.052 0.405,-0.069 0.436,-0.102C0.467,-0.135 0.483,-0.184 0.483,-0.249L0.483,-0.546L0.553,-0.546Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(100,0,0,100,168.007,79.8987)">
<path d="M0.149,-0.425C0.167,-0.463 0.195,-0.494 0.233,-0.518C0.27,-0.542 0.315,-0.554 0.366,-0.554C0.416,-0.554 0.461,-0.543 0.5,-0.52C0.539,-0.497 0.57,-0.464 0.593,-0.422C0.615,-0.379 0.626,-0.33 0.626,-0.274C0.626,-0.218 0.615,-0.169 0.593,-0.126C0.57,-0.083 0.539,-0.05 0.5,-0.027C0.46,-0.004 0.415,0.008 0.366,0.008C0.314,0.008 0.269,-0.004 0.232,-0.028C0.194,-0.051 0.166,-0.082 0.149,-0.12L0.149,-0L0.08,-0L0.08,-0.74L0.149,-0.74L0.149,-0.425ZM0.555,-0.274C0.555,-0.319 0.546,-0.359 0.529,-0.392C0.511,-0.425 0.487,-0.45 0.456,-0.467C0.425,-0.484 0.391,-0.493 0.352,-0.493C0.315,-0.493 0.281,-0.484 0.25,-0.466C0.219,-0.448 0.194,-0.422 0.176,-0.389C0.158,-0.356 0.149,-0.317 0.149,-0.273C0.149,-0.229 0.158,-0.19 0.176,-0.157C0.194,-0.124 0.219,-0.098 0.25,-0.08C0.281,-0.062 0.315,-0.053 0.352,-0.053C0.391,-0.053 0.425,-0.062 0.456,-0.08C0.487,-0.097 0.511,-0.123 0.529,-0.157C0.546,-0.19 0.555,-0.229 0.555,-0.274Z" style="fill:white;fill-rule:nonzero;"/>
</g>
</g>
</g>
</g>

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Before After
Before After

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 582 151" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-654,-2135)">
<g id="pypeclub_color_white" transform="matrix(0.775972,0,0,0.911492,654.799,1597.77)">
<g transform="matrix(1,0,0,1,-987,-2234)">
<g id="pypeclub_white" transform="matrix(0.775972,0,0,0.911492,987.586,1696.48)">
<rect x="0" y="589.923" width="748.802" height="164.565" style="fill:none;"/>
<g transform="matrix(2.7183,0,0,1.24969,-3.76111,624.796)">
<g id="PYPE" transform="matrix(1,0,0,1,-105.172,0)">
@ -20,7 +20,21 @@
</g>
</g>
<g transform="matrix(0.474085,0,0,0.877899,142.749,-0.167247)">
<text x="2.312px" y="79.899px" style="font-family:'Poppins-Light', 'Poppins';font-weight:300;font-size:100px;fill:white;">.club</text>
<g transform="matrix(100,0,0,100,2.312,79.8987)">
<path d="M0.092,0.005C0.077,0.005 0.065,-0 0.056,-0.01C0.046,-0.02 0.041,-0.032 0.041,-0.047C0.041,-0.062 0.046,-0.074 0.056,-0.084C0.065,-0.093 0.077,-0.098 0.092,-0.098C0.106,-0.098 0.118,-0.093 0.128,-0.084C0.137,-0.074 0.142,-0.062 0.142,-0.047C0.142,-0.032 0.137,-0.02 0.128,-0.01C0.118,-0 0.106,0.005 0.092,0.005Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(100,0,0,100,20.5107,79.8987)">
<path d="M0.048,-0.273C0.048,-0.33 0.059,-0.379 0.082,-0.422C0.105,-0.464 0.136,-0.497 0.176,-0.52C0.216,-0.543 0.262,-0.554 0.313,-0.554C0.38,-0.554 0.436,-0.537 0.48,-0.504C0.523,-0.471 0.551,-0.425 0.564,-0.368L0.489,-0.368C0.48,-0.407 0.46,-0.438 0.429,-0.461C0.398,-0.483 0.359,-0.494 0.313,-0.494C0.276,-0.494 0.243,-0.486 0.214,-0.469C0.185,-0.452 0.162,-0.428 0.145,-0.395C0.128,-0.362 0.119,-0.321 0.119,-0.273C0.119,-0.225 0.128,-0.184 0.145,-0.151C0.162,-0.118 0.185,-0.093 0.214,-0.076C0.243,-0.059 0.276,-0.051 0.313,-0.051C0.359,-0.051 0.398,-0.062 0.429,-0.085C0.46,-0.107 0.48,-0.138 0.489,-0.178L0.564,-0.178C0.551,-0.122 0.523,-0.077 0.479,-0.043C0.435,-0.009 0.38,0.008 0.313,0.008C0.262,0.008 0.216,-0.004 0.176,-0.027C0.136,-0.05 0.105,-0.082 0.082,-0.125C0.059,-0.167 0.048,-0.216 0.048,-0.273Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(100,0,0,100,81.8095,79.8987)">
<rect x="0.08" y="-0.74" width="0.07" height="0.74" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(100,0,0,100,104.708,79.8987)">
<path d="M0.553,-0.546L0.553,-0L0.483,-0L0.483,-0.096C0.467,-0.062 0.442,-0.036 0.409,-0.018C0.376,-0 0.338,0.009 0.297,0.009C0.232,0.009 0.178,-0.011 0.137,-0.051C0.096,-0.092 0.075,-0.15 0.075,-0.227L0.075,-0.546L0.144,-0.546L0.144,-0.235C0.144,-0.176 0.159,-0.13 0.189,-0.099C0.218,-0.068 0.259,-0.052 0.31,-0.052C0.363,-0.052 0.405,-0.069 0.436,-0.102C0.467,-0.135 0.483,-0.184 0.483,-0.249L0.483,-0.546L0.553,-0.546Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(100,0,0,100,168.007,79.8987)">
<path d="M0.149,-0.425C0.167,-0.463 0.195,-0.494 0.233,-0.518C0.27,-0.542 0.315,-0.554 0.366,-0.554C0.416,-0.554 0.461,-0.543 0.5,-0.52C0.539,-0.497 0.57,-0.464 0.593,-0.422C0.615,-0.379 0.626,-0.33 0.626,-0.274C0.626,-0.218 0.615,-0.169 0.593,-0.126C0.57,-0.083 0.539,-0.05 0.5,-0.027C0.46,-0.004 0.415,0.008 0.366,0.008C0.314,0.008 0.269,-0.004 0.232,-0.028C0.194,-0.051 0.166,-0.082 0.149,-0.12L0.149,-0L0.08,-0L0.08,-0.74L0.149,-0.74L0.149,-0.425ZM0.555,-0.274C0.555,-0.319 0.546,-0.359 0.529,-0.392C0.511,-0.425 0.487,-0.45 0.456,-0.467C0.425,-0.484 0.391,-0.493 0.352,-0.493C0.315,-0.493 0.281,-0.484 0.25,-0.466C0.219,-0.448 0.194,-0.422 0.176,-0.389C0.158,-0.356 0.149,-0.317 0.149,-0.273C0.149,-0.229 0.158,-0.19 0.176,-0.157C0.194,-0.124 0.219,-0.098 0.25,-0.08C0.281,-0.062 0.315,-0.053 0.352,-0.053C0.391,-0.053 0.425,-0.062 0.456,-0.08C0.487,-0.097 0.511,-0.123 0.529,-0.157C0.546,-0.19 0.555,-0.229 0.555,-0.274Z" style="fill:white;fill-rule:nonzero;"/>
</g>
</g>
</g>
</g>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 7 KiB

Before After
Before After