diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a3571eca6..02ae689dc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,27 +1,63 @@
# Changelog
-## [3.6.2-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD)
+## [3.7.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD)
-[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.1...HEAD)
+[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.6.3...HEAD)
+
+**🆕 New features**
+
+- OpenPypeV3: add key task type, task shortname and user to path templating construction [\#2157](https://github.com/pypeclub/OpenPype/pull/2157)
**🚀 Enhancements**
-- Tools: SceneInventory in OpenPype [\#2255](https://github.com/pypeclub/OpenPype/pull/2255)
-- Tools: Tasks widget [\#2251](https://github.com/pypeclub/OpenPype/pull/2251)
-- Added endpoint for configured extensions [\#2221](https://github.com/pypeclub/OpenPype/pull/2221)
+- Ftrack: Remove unused clean component plugin [\#2269](https://github.com/pypeclub/OpenPype/pull/2269)
+- Houdini: Add experimental tools action [\#2267](https://github.com/pypeclub/OpenPype/pull/2267)
**🐛 Bug fixes**
+- Nuke: Anatomy fill data use task as dictionary [\#2278](https://github.com/pypeclub/OpenPype/pull/2278)
+- Bug: fix variable name \_asset\_id in workfiles application [\#2274](https://github.com/pypeclub/OpenPype/pull/2274)
+- Version handling fixes [\#2272](https://github.com/pypeclub/OpenPype/pull/2272)
+
+## [3.6.3](https://github.com/pypeclub/OpenPype/tree/3.6.3) (2021-11-19)
+
+[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.3-nightly.1...3.6.3)
+
+**🐛 Bug fixes**
+
+- Deadline: Fix publish targets [\#2280](https://github.com/pypeclub/OpenPype/pull/2280)
+
+## [3.6.2](https://github.com/pypeclub/OpenPype/tree/3.6.2) (2021-11-18)
+
+[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.2-nightly.2...3.6.2)
+
+**🚀 Enhancements**
+
+- Tools: Assets widget [\#2265](https://github.com/pypeclub/OpenPype/pull/2265)
+- SceneInventory: Choose loader in asset switcher [\#2262](https://github.com/pypeclub/OpenPype/pull/2262)
+- Style: New fonts in OpenPype style [\#2256](https://github.com/pypeclub/OpenPype/pull/2256)
+- Tools: SceneInventory in OpenPype [\#2255](https://github.com/pypeclub/OpenPype/pull/2255)
+- Tools: Tasks widget [\#2251](https://github.com/pypeclub/OpenPype/pull/2251)
+- Tools: Creator in OpenPype [\#2244](https://github.com/pypeclub/OpenPype/pull/2244)
+- Usage of tools code [\#2185](https://github.com/pypeclub/OpenPype/pull/2185)
+
+**🐛 Bug fixes**
+
+- Tools: Parenting of tools in Nuke and Hiero [\#2266](https://github.com/pypeclub/OpenPype/pull/2266)
+- limiting validator to specific editorial hosts [\#2264](https://github.com/pypeclub/OpenPype/pull/2264)
+- Tools: Select Context dialog attribute fix [\#2261](https://github.com/pypeclub/OpenPype/pull/2261)
+- Maya: Render publishing fails on linux [\#2260](https://github.com/pypeclub/OpenPype/pull/2260)
+- LookAssigner: Fix tool reopen [\#2259](https://github.com/pypeclub/OpenPype/pull/2259)
+- Standalone: editorial not publishing thumbnails on all subsets [\#2258](https://github.com/pypeclub/OpenPype/pull/2258)
+- Loader doesn't allow changing of version before loading [\#2254](https://github.com/pypeclub/OpenPype/pull/2254)
- Burnins: Support mxf metadata [\#2247](https://github.com/pypeclub/OpenPype/pull/2247)
+- Maya: Support for configurable AOV separator characters [\#2197](https://github.com/pypeclub/OpenPype/pull/2197)
+- Maya: texture colorspace modes in looks [\#2195](https://github.com/pypeclub/OpenPype/pull/2195)
## [3.6.1](https://github.com/pypeclub/OpenPype/tree/3.6.1) (2021-11-16)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.1-nightly.1...3.6.1)
-**🐛 Bug fixes**
-
-- Loader doesn't allow changing of version before loading [\#2254](https://github.com/pypeclub/OpenPype/pull/2254)
-
## [3.6.0](https://github.com/pypeclub/OpenPype/tree/3.6.0) (2021-11-15)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.6.0-nightly.6...3.6.0)
@@ -46,6 +82,7 @@
- Add a "following workfile versioning" option on publish [\#2225](https://github.com/pypeclub/OpenPype/pull/2225)
- Modules: Module can add cli commands [\#2224](https://github.com/pypeclub/OpenPype/pull/2224)
- Webpublisher: Separate webpublisher logic [\#2222](https://github.com/pypeclub/OpenPype/pull/2222)
+- Added endpoint for configured extensions [\#2221](https://github.com/pypeclub/OpenPype/pull/2221)
- Add both side availability on Site Sync sites to Loader [\#2220](https://github.com/pypeclub/OpenPype/pull/2220)
- Tools: Center loader and library loader on show [\#2219](https://github.com/pypeclub/OpenPype/pull/2219)
- Maya : Validate shape zero [\#2212](https://github.com/pypeclub/OpenPype/pull/2212)
@@ -57,7 +94,6 @@
- Dirmap in Nuke [\#2198](https://github.com/pypeclub/OpenPype/pull/2198)
- Delivery: Check 'frame' key in template for sequence delivery [\#2196](https://github.com/pypeclub/OpenPype/pull/2196)
- Settings: Site sync project settings improvement [\#2193](https://github.com/pypeclub/OpenPype/pull/2193)
-- Usage of tools code [\#2185](https://github.com/pypeclub/OpenPype/pull/2185)
- Settings: Dictionary based on project roots [\#2184](https://github.com/pypeclub/OpenPype/pull/2184)
- Subset name: Be able to pass asset document to get subset name [\#2179](https://github.com/pypeclub/OpenPype/pull/2179)
@@ -83,25 +119,6 @@
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.5.0-nightly.8...3.5.0)
-**🆕 New features**
-
-- Added project and task into context change message in Maya [\#2131](https://github.com/pypeclub/OpenPype/pull/2131)
-- Add ExtractBurnin to photoshop review [\#2124](https://github.com/pypeclub/OpenPype/pull/2124)
-- PYPE-1218 - changed namespace to contain subset name in Maya [\#2114](https://github.com/pypeclub/OpenPype/pull/2114)
-
-**🚀 Enhancements**
-
-- Maya: make rig validators configurable in settings [\#2137](https://github.com/pypeclub/OpenPype/pull/2137)
-- Settings: Updated readme for entity types in settings [\#2132](https://github.com/pypeclub/OpenPype/pull/2132)
-- Nuke: unified clip loader [\#2128](https://github.com/pypeclub/OpenPype/pull/2128)
-
-**🐛 Bug fixes**
-
-- Maya: fix model publishing [\#2130](https://github.com/pypeclub/OpenPype/pull/2130)
-- Fix - oiiotool wasn't recognized even if present [\#2129](https://github.com/pypeclub/OpenPype/pull/2129)
-- General: Disk mapping group [\#2120](https://github.com/pypeclub/OpenPype/pull/2120)
-- Hiero: publishing effect first time makes wrong resources path [\#2115](https://github.com/pypeclub/OpenPype/pull/2115)
-
## [3.4.1](https://github.com/pypeclub/OpenPype/tree/3.4.1) (2021-09-23)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.1-nightly.1...3.4.1)
diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py
index f7f35824c8..151597e505 100644
--- a/igniter/bootstrap_repos.py
+++ b/igniter/bootstrap_repos.py
@@ -10,6 +10,7 @@ import tempfile
from pathlib import Path
from typing import Union, Callable, List, Tuple
import hashlib
+import platform
from zipfile import ZipFile, BadZipFile
@@ -196,21 +197,23 @@ class OpenPypeVersion(semver.VersionInfo):
return str(self.finalize_version())
@staticmethod
- def version_in_str(string: str) -> Tuple:
+ def version_in_str(string: str) -> Union[None, OpenPypeVersion]:
"""Find OpenPype version in given string.
Args:
string (str): string to search.
Returns:
- tuple: True/False and OpenPypeVersion if found.
+ OpenPypeVersion: of detected or None.
"""
m = re.search(OpenPypeVersion._VERSION_REGEX, string)
if not m:
- return False, None
+ return None
version = OpenPypeVersion.parse(string[m.start():m.end()])
- return True, version
+ if "staging" in string[m.start():m.end()]:
+ version.staging = True
+ return version
@classmethod
def parse(cls, version):
@@ -531,6 +534,7 @@ class BootstrapRepos:
processed_path = file
self._print(f"- processing {processed_path}")
+
checksums.append(
(
sha256sum(file.as_posix()),
@@ -542,7 +546,10 @@ class BootstrapRepos:
checksums_str = ""
for c in checksums:
- checksums_str += "{}:{}\n".format(c[0], c[1])
+ file_str = c[1]
+ if platform.system().lower() == "windows":
+ file_str = c[1].as_posix().replace("\\", "/")
+ checksums_str += "{}:{}\n".format(c[0], file_str)
zip_file.writestr("checksums", checksums_str)
# test if zip is ok
zip_file.testzip()
@@ -563,6 +570,8 @@ class BootstrapRepos:
and string with reason as second.
"""
+ if os.getenv("OPENPYPE_DONT_VALIDATE_VERSION"):
+ return True, "Disabled validation"
if not path.exists():
return False, "Path doesn't exist"
@@ -589,13 +598,16 @@ class BootstrapRepos:
# calculate and compare checksums in the zip file
for file in checksums:
+ file_name = file[1]
+ if platform.system().lower() == "windows":
+ file_name = file_name.replace("/", "\\")
h = hashlib.sha256()
try:
- h.update(zip_file.read(file[1]))
+ h.update(zip_file.read(file_name))
except FileNotFoundError:
- return False, f"Missing file [ {file[1]} ]"
+ return False, f"Missing file [ {file_name} ]"
if h.hexdigest() != file[0]:
- return False, f"Invalid checksum on {file[1]}"
+ return False, f"Invalid checksum on {file_name}"
# get list of files in zip minus `checksums` file itself
# and turn in to set to compare against list of files
@@ -604,7 +616,7 @@ class BootstrapRepos:
files_in_zip = zip_file.namelist()
files_in_zip.remove("checksums")
files_in_zip = set(files_in_zip)
- files_in_checksum = set([file[1] for file in checksums])
+ files_in_checksum = {file[1] for file in checksums}
diff = files_in_zip.difference(files_in_checksum)
if diff:
return False, f"Missing files {diff}"
@@ -628,16 +640,19 @@ class BootstrapRepos:
]
files_in_dir.remove("checksums")
files_in_dir = set(files_in_dir)
- files_in_checksum = set([file[1] for file in checksums])
+ files_in_checksum = {file[1] for file in checksums}
for file in checksums:
+ file_name = file[1]
+ if platform.system().lower() == "windows":
+ file_name = file_name.replace("/", "\\")
try:
- current = sha256sum((path / file[1]).as_posix())
+ current = sha256sum((path / file_name).as_posix())
except FileNotFoundError:
- return False, f"Missing file [ {file[1]} ]"
+ return False, f"Missing file [ {file_name} ]"
if file[0] != current:
- return False, f"Invalid checksum on {file[1]}"
+ return False, f"Invalid checksum on {file_name}"
diff = files_in_dir.difference(files_in_checksum)
if diff:
return False, f"Missing files {diff}"
@@ -1161,9 +1176,9 @@ class BootstrapRepos:
name = item.name if item.is_dir() else item.stem
result = OpenPypeVersion.version_in_str(name)
- if result[0]:
+ if result:
detected_version: OpenPypeVersion
- detected_version = result[1]
+ detected_version = result
if item.is_dir() and not self._is_openpype_in_dir(
item, detected_version
diff --git a/igniter/version.py b/igniter/version.py
index 56d58f7f60..8e7731f6d6 100644
--- a/igniter/version.py
+++ b/igniter/version.py
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
"""Definition of Igniter version."""
-__version__ = "1.0.1"
+__version__ = "1.0.2"
diff --git a/openpype/api.py b/openpype/api.py
index e4bbb104a3..a6529202ff 100644
--- a/openpype/api.py
+++ b/openpype/api.py
@@ -17,6 +17,7 @@ from .lib import (
version_up,
get_asset,
get_hierarchy,
+ get_workdir_data,
get_version_from_path,
get_last_version_from_path,
get_app_environments_for_context,
diff --git a/openpype/cli.py b/openpype/cli.py
index 3194723d4c..4c4dc1a3c6 100644
--- a/openpype/cli.py
+++ b/openpype/cli.py
@@ -384,3 +384,15 @@ def syncserver(debug, active_site):
if debug:
os.environ['OPENPYPE_DEBUG'] = '3'
PypeCommands().syncserver(active_site)
+
+
+@main.command()
+@click.argument("directory")
+def repack_version(directory):
+ """Repack OpenPype version from directory.
+
+ This command will re-create zip file from specified directory,
+ recalculating file checksums. It will try to use version detected in
+ directory name.
+ """
+ PypeCommands().repack_version(directory)
diff --git a/openpype/hosts/harmony/plugins/publish/collect_farm_render.py b/openpype/hosts/harmony/plugins/publish/collect_farm_render.py
index fc80e7c029..31a249591e 100644
--- a/openpype/hosts/harmony/plugins/publish/collect_farm_render.py
+++ b/openpype/hosts/harmony/plugins/publish/collect_farm_render.py
@@ -126,7 +126,8 @@ class CollectFarmRender(openpype.lib.abstract_collect_render.
# because of using 'renderFarm' as a family, replace 'Farm' with
# capitalized task name - issue of avalon-core Creator app
subset_name = node.split("/")[1]
- task_name = context.data["anatomyData"]["task"].capitalize()
+ task_name = context.data["anatomyData"]["task"][
+ "name"].capitalize()
replace_str = ""
if task_name.lower() not in subset_name.lower():
replace_str = task_name
diff --git a/openpype/hosts/harmony/plugins/publish/collect_palettes.py b/openpype/hosts/harmony/plugins/publish/collect_palettes.py
index b8671badb3..e47cbaf17e 100644
--- a/openpype/hosts/harmony/plugins/publish/collect_palettes.py
+++ b/openpype/hosts/harmony/plugins/publish/collect_palettes.py
@@ -28,7 +28,7 @@ class CollectPalettes(pyblish.api.ContextPlugin):
# skip collecting if not in allowed task
if self.allowed_tasks:
- task_name = context.data["anatomyData"]["task"].lower()
+ task_name = context.data["anatomyData"]["task"]["name"].lower()
if (not any([re.search(pattern, task_name)
for pattern in self.allowed_tasks])):
return
diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py
index 876fae5da9..af58f5b73e 100644
--- a/openpype/hosts/hiero/api/lib.py
+++ b/openpype/hosts/hiero/api/lib.py
@@ -30,6 +30,7 @@ self = sys.modules[__name__]
self._has_been_setup = False
self._has_menu = False
self._registered_gui = None
+self._parent = None
self.pype_tag_name = "openpypeData"
self.default_sequence_name = "openpypeSequence"
self.default_bin_name = "openpypeBin"
@@ -1029,3 +1030,15 @@ def before_project_save(event):
# also mark old versions of loaded containers
check_inventory_versions()
+
+
+def get_main_window():
+ """Acquire Nuke's main window"""
+ if self._parent is None:
+ top_widgets = QtWidgets.QApplication.topLevelWidgets()
+ name = "Foundry::UI::DockMainWindow"
+ main_window = next(widget for widget in top_widgets if
+ widget.inherits("QMainWindow") and
+ widget.metaObject().className() == name)
+ self._parent = main_window
+ return self._parent
diff --git a/openpype/hosts/hiero/api/menu.py b/openpype/hosts/hiero/api/menu.py
index e3de220777..61b515d719 100644
--- a/openpype/hosts/hiero/api/menu.py
+++ b/openpype/hosts/hiero/api/menu.py
@@ -37,12 +37,16 @@ def menu_install():
Installing menu into Hiero
"""
+ from Qt import QtGui
from . import (
publish, launch_workfiles_app, reload_config,
apply_colorspace_project, apply_colorspace_clips
)
+ from .lib import get_main_window
+
+ main_window = get_main_window()
+
# here is the best place to add menu
- from avalon.vendor.Qt import QtGui
menu_name = os.environ['AVALON_LABEL']
@@ -86,15 +90,21 @@ def menu_install():
creator_action = menu.addAction("Create ...")
creator_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
- creator_action.triggered.connect(host_tools.show_creator)
+ creator_action.triggered.connect(
+ lambda: host_tools.show_creator(parent=main_window)
+ )
loader_action = menu.addAction("Load ...")
loader_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
- loader_action.triggered.connect(host_tools.show_loader)
+ loader_action.triggered.connect(
+ lambda: host_tools.show_loader(parent=main_window)
+ )
sceneinventory_action = menu.addAction("Manage ...")
sceneinventory_action.setIcon(QtGui.QIcon("icons:CopyRectangle.png"))
- sceneinventory_action.triggered.connect(host_tools.show_scene_inventory)
+ sceneinventory_action.triggered.connect(
+ lambda: host_tools.show_scene_inventory(parent=main_window)
+ )
menu.addSeparator()
if os.getenv("OPENPYPE_DEVELOP"):
diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py
index 6f6588e1be..d52cb68ba7 100644
--- a/openpype/hosts/hiero/api/pipeline.py
+++ b/openpype/hosts/hiero/api/pipeline.py
@@ -209,9 +209,11 @@ def update_container(track_item, data=None):
def launch_workfiles_app(*args):
''' Wrapping function for workfiles launcher '''
+ from .lib import get_main_window
+ main_window = get_main_window()
# show workfile gui
- host_tools.show_workfiles()
+ host_tools.show_workfiles(parent=main_window)
def publish(parent):
diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml
index 2b556a2e75..c34310cf72 100644
--- a/openpype/hosts/houdini/startup/MainMenuCommon.xml
+++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml
@@ -67,6 +67,16 @@ from avalon.houdini import pipeline
pipeline.reload_pipeline()]]>
+
+
+
+
+
+
diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py
index 4983109d58..e8e4b9aaef 100644
--- a/openpype/hosts/maya/api/lib_renderproducts.py
+++ b/openpype/hosts/maya/api/lib_renderproducts.py
@@ -180,6 +180,7 @@ class ARenderProducts:
self.layer = layer
self.render_instance = render_instance
self.multipart = False
+ self.aov_separator = render_instance.data.get("aovSeparator", "_")
# Initialize
self.layer_data = self._get_layer_data()
@@ -676,7 +677,7 @@ class RenderProductsVray(ARenderProducts):
"""
prefix = super(RenderProductsVray, self).get_renderer_prefix()
- prefix = "{}.".format(prefix)
+ prefix = "{}{}".format(prefix, self.aov_separator)
return prefix
def _get_layer_data(self):
diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py
index 4fd4b9d986..85919d1166 100644
--- a/openpype/hosts/maya/plugins/create/create_render.py
+++ b/openpype/hosts/maya/plugins/create/create_render.py
@@ -21,6 +21,7 @@ from openpype.api import (
from openpype.modules import ModulesManager
from avalon.api import Session
+from avalon.api import CreatorError
class CreateRender(plugin.Creator):
@@ -81,13 +82,21 @@ class CreateRender(plugin.Creator):
}
_image_prefixes = {
- 'mentalray': 'maya///_',
+ 'mentalray': 'maya///{aov_separator}', # noqa
'vray': 'maya///',
- 'arnold': 'maya///_',
- 'renderman': 'maya///_',
- 'redshift': 'maya///_'
+ 'arnold': 'maya///{aov_separator}', # noqa
+ 'renderman': 'maya///{aov_separator}',
+ 'redshift': 'maya///{aov_separator}' # noqa
}
+ _aov_chars = {
+ "dot": ".",
+ "dash": "-",
+ "underscore": "_"
+ }
+
+ _project_settings = None
+
def __init__(self, *args, **kwargs):
"""Constructor."""
super(CreateRender, self).__init__(*args, **kwargs)
@@ -95,12 +104,24 @@ class CreateRender(plugin.Creator):
if not deadline_settings["enabled"]:
self.deadline_servers = {}
return
- project_settings = get_project_settings(Session["AVALON_PROJECT"])
+ self._project_settings = get_project_settings(
+ Session["AVALON_PROJECT"])
+
+ # project_settings/maya/create/CreateRender/aov_separator
+ try:
+ self.aov_separator = self._aov_chars[(
+ self._project_settings["maya"]
+ ["create"]
+ ["CreateRender"]
+ ["aov_separator"]
+ )]
+ except KeyError:
+ self.aov_separator = "_"
+
try:
default_servers = deadline_settings["deadline_urls"]
project_servers = (
- project_settings["deadline"]
- ["deadline_servers"]
+ self._project_settings["deadline"]["deadline_servers"]
)
self.deadline_servers = {
k: default_servers[k]
@@ -409,8 +430,10 @@ class CreateRender(plugin.Creator):
renderer (str): Renderer name.
"""
+ prefix = self._image_prefixes[renderer]
+ prefix = prefix.replace("{aov_separator}", self.aov_separator)
cmds.setAttr(self._image_prefix_nodes[renderer],
- self._image_prefixes[renderer],
+ prefix,
type="string")
asset = get_asset()
@@ -446,37 +469,37 @@ class CreateRender(plugin.Creator):
self._set_global_output_settings()
- @staticmethod
- def _set_renderer_option(renderer_node, arg=None, value=None):
- # type: (str, str, str) -> str
- """Set option on renderer node.
-
- If renderer settings node doesn't exists, it is created first.
-
- Args:
- renderer_node (str): Renderer name.
- arg (str, optional): Argument name.
- value (str, optional): Argument value.
-
- Returns:
- str: Renderer settings node.
-
- """
- settings = cmds.ls(type=renderer_node)
- result = settings[0] if settings else cmds.createNode(renderer_node)
- cmds.setAttr(arg.format(result), value)
- return result
-
def _set_vray_settings(self, asset):
# type: (dict) -> None
"""Sets important settings for Vray."""
- node = self._set_renderer_option(
- "VRaySettingsNode", "{}.fileNameRenderElementSeparator", "_"
- )
+ settings = cmds.ls(type="VRaySettingsNode")
+ node = settings[0] if settings else cmds.createNode("VRaySettingsNode")
+ # set separator
+ # set it in vray menu
+ if cmds.optionMenuGrp("vrayRenderElementSeparator", exists=True,
+ q=True):
+ items = cmds.optionMenuGrp(
+ "vrayRenderElementSeparator", ill=True, query=True)
+
+ separators = [cmds.menuItem(i, label=True, query=True) for i in items] # noqa: E501
+ try:
+ sep_idx = separators.index(self.aov_separator)
+ except ValueError:
+ raise CreatorError(
+ "AOV character {} not in {}".format(
+ self.aov_separator, separators))
+
+ cmds.optionMenuGrp(
+ "vrayRenderElementSeparator", sl=sep_idx + 1, edit=True)
+ cmds.setAttr(
+ "{}.fileNameRenderElementSeparator".format(node),
+ self.aov_separator,
+ type="string"
+ )
# set format to exr
cmds.setAttr(
- "{}.imageFormatStr".format(node), 5)
+ "{}.imageFormatStr".format(node), "exr", type="string")
# animType
cmds.setAttr(
diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py
index d2f277329a..580d459a90 100644
--- a/openpype/hosts/maya/plugins/publish/collect_render.py
+++ b/openpype/hosts/maya/plugins/publish/collect_render.py
@@ -41,6 +41,7 @@ Provides:
import re
import os
+import platform
import json
from maya import cmds
@@ -61,6 +62,12 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
label = "Collect Render Layers"
sync_workfile_version = False
+ _aov_chars = {
+ "dot": ".",
+ "dash": "-",
+ "underscore": "_"
+ }
+
def process(self, context):
"""Entry point to collector."""
render_instance = None
@@ -166,6 +173,18 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
if renderer.startswith("renderman"):
renderer = "renderman"
+ try:
+ aov_separator = self._aov_chars[(
+ context.data["project_settings"]
+ ["create"]
+ ["CreateRender"]
+ ["aov_separator"]
+ )]
+ except KeyError:
+ aov_separator = "_"
+
+ render_instance.data["aovSeparator"] = aov_separator
+
# return all expected files for all cameras and aovs in given
# frame range
layer_render_products = get_layer_render_products(
@@ -255,12 +274,28 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
common_publish_meta_path, part)
if part == expected_layer_name:
break
+
+ # TODO: replace this terrible linux hotfix with real solution :)
+ if platform.system().lower() in ["linux", "darwin"]:
+ common_publish_meta_path = "/" + common_publish_meta_path
+
self.log.info(
"Publish meta path: {}".format(common_publish_meta_path))
self.log.info(full_exp_files)
self.log.info("collecting layer: {}".format(layer_name))
# Get layer specific settings, might be overrides
+
+ try:
+ aov_separator = self._aov_chars[(
+ context.data["project_settings"]
+ ["create"]
+ ["CreateRender"]
+ ["aov_separator"]
+ )]
+ except KeyError:
+ aov_separator = "_"
+
data = {
"subset": expected_layer_name,
"attachTo": attach_to,
@@ -302,7 +337,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
"convertToScanline") or False,
"useReferencedAovs": render_instance.data.get(
"useReferencedAovs") or render_instance.data.get(
- "vrayUseReferencedAovs") or False
+ "vrayUseReferencedAovs") or False,
+ "aovSeparator": aov_separator
}
if deadline_url:
diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py
index 65ddacfc57..6079d34fbe 100644
--- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py
+++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py
@@ -55,13 +55,19 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
ImagePrefixTokens = {
- 'arnold': 'maya///_',
+ 'arnold': 'maya///{aov_separator}', # noqa
'redshift': 'maya///',
'vray': 'maya///',
- 'renderman': '_..'
+ 'renderman': '{aov_separator}..' # noqa
}
- redshift_AOV_prefix = "/_"
+ _aov_chars = {
+ "dot": ".",
+ "dash": "-",
+ "underscore": "_"
+ }
+
+ redshift_AOV_prefix = "/{aov_separator}" # noqa: E501
# WARNING: There is bug? in renderman, translating token
# to something left behind mayas default image prefix. So instead
@@ -107,6 +113,9 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
anim_override = lib.get_attr_in_layer("defaultRenderGlobals.animation",
layer=layer)
+
+ prefix = prefix.replace(
+ "{aov_separator}", instance.data.get("aovSeparator", "_"))
if not anim_override:
invalid = True
cls.log.error("Animation needs to be enabled. Use the same "
@@ -138,12 +147,16 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
else:
node = vray_settings[0]
- if cmds.getAttr(
- "{}.fileNameRenderElementSeparator".format(node)) != "_":
- invalid = False
+ scene_sep = cmds.getAttr(
+ "{}.fileNameRenderElementSeparator".format(node))
+ if scene_sep != instance.data.get("aovSeparator", "_"):
cls.log.error("AOV separator is not set correctly.")
+ invalid = True
if renderer == "redshift":
+ redshift_AOV_prefix = cls.redshift_AOV_prefix.replace(
+ "{aov_separator}", instance.data.get("aovSeparator", "_")
+ )
if re.search(cls.R_AOV_TOKEN, prefix):
invalid = True
cls.log.error(("Do not use AOV token [ {} ] - "
@@ -155,7 +168,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
for aov in rs_aovs:
aov_prefix = cmds.getAttr("{}.filePrefix".format(aov))
# check their image prefix
- if aov_prefix != cls.redshift_AOV_prefix:
+ if aov_prefix != redshift_AOV_prefix:
cls.log.error(("AOV ({}) image prefix is not set "
"correctly {} != {}").format(
cmds.getAttr("{}.name".format(aov)),
@@ -181,7 +194,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
file_prefix = cmds.getAttr("rmanGlobals.imageFileFormat")
dir_prefix = cmds.getAttr("rmanGlobals.imageOutputDir")
- if file_prefix.lower() != cls.ImagePrefixTokens[renderer].lower():
+ if file_prefix.lower() != prefix.lower():
invalid = True
cls.log.error("Wrong image prefix [ {} ]".format(file_prefix))
@@ -198,18 +211,20 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
cls.log.error("Wrong image prefix [ {} ] - "
"You can't use '' token "
"with merge AOVs turned on".format(prefix))
- else:
- if not re.search(cls.R_AOV_TOKEN, prefix):
- invalid = True
- cls.log.error("Wrong image prefix [ {} ] - "
- "doesn't have: '' or "
- "token".format(prefix))
+ elif not re.search(cls.R_AOV_TOKEN, prefix):
+ invalid = True
+ cls.log.error("Wrong image prefix [ {} ] - "
+ "doesn't have: '' or "
+ "token".format(prefix))
# prefix check
- if prefix.lower() != cls.ImagePrefixTokens[renderer].lower():
+ default_prefix = cls.ImagePrefixTokens[renderer]
+ default_prefix = default_prefix.replace(
+ "{aov_separator}", instance.data.get("aovSeparator", "_"))
+ if prefix.lower() != default_prefix.lower():
cls.log.warning("warning: prefix differs from "
"recommended {}".format(
- cls.ImagePrefixTokens[renderer]))
+ default_prefix))
if padding != cls.DEFAULT_PADDING:
invalid = True
@@ -257,9 +272,14 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
@classmethod
def repair(cls, instance):
-
renderer = instance.data['renderer']
layer_node = instance.data['setMembers']
+ redshift_AOV_prefix = cls.redshift_AOV_prefix.replace(
+ "{aov_separator}", instance.data.get("aovSeparator", "_")
+ )
+ default_prefix = cls.ImagePrefixTokens[renderer].replace(
+ "{aov_separator}", instance.data.get("aovSeparator", "_")
+ )
with lib.renderlayer(layer_node):
default = lib.RENDER_ATTRS['default']
@@ -270,7 +290,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
node = render_attrs["node"]
prefix_attr = render_attrs["prefix"]
- fname_prefix = cls.ImagePrefixTokens[renderer]
+ fname_prefix = default_prefix
cmds.setAttr("{}.{}".format(node, prefix_attr),
fname_prefix, type="string")
@@ -281,7 +301,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
else:
# renderman handles stuff differently
cmds.setAttr("rmanGlobals.imageFileFormat",
- cls.ImagePrefixTokens[renderer],
+ default_prefix,
type="string")
cmds.setAttr("rmanGlobals.imageOutputDir",
cls.RendermanDirPrefix,
@@ -294,10 +314,13 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
else:
node = vray_settings[0]
+ cmds.optionMenuGrp("vrayRenderElementSeparator",
+ v=instance.data.get("aovSeparator", "_"))
cmds.setAttr(
"{}.fileNameRenderElementSeparator".format(
node),
- "_"
+ instance.data.get("aovSeparator", "_"),
+ type="string"
)
if renderer == "redshift":
@@ -306,7 +329,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
for aov in rs_aovs:
# fix AOV prefixes
cmds.setAttr(
- "{}.filePrefix".format(aov), cls.redshift_AOV_prefix)
+ "{}.filePrefix".format(aov), redshift_AOV_prefix)
# fix AOV file format
default_ext = cmds.getAttr(
"redshiftOptions.imageFormat", asString=True)
diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py
index 6d593ca588..f4c3a55c2b 100644
--- a/openpype/hosts/nuke/api/lib.py
+++ b/openpype/hosts/nuke/api/lib.py
@@ -18,7 +18,7 @@ from openpype.api import (
BuildWorkfile,
get_version_from_path,
get_anatomy_settings,
- get_hierarchy,
+ get_workdir_data,
get_asset,
get_current_project_settings,
ApplicationManager
@@ -268,15 +268,21 @@ def format_anatomy(data):
if not version:
file = script_name()
data["version"] = get_version_from_path(file)
- project_document = io.find_one({"type": "project"})
+
+ project_doc = io.find_one({"type": "project"})
+ asset_doc = io.find_one({
+ "type": "asset",
+ "name": data["avalon"]["asset"]
+ })
+ task_name = os.environ["AVALON_TASK"]
+ host_name = os.environ["AVALON_APP"]
+ context_data = get_workdir_data(
+ project_doc, asset_doc, task_name, host_name
+ )
+ data.update(context_data)
data.update({
"subset": data["avalon"]["subset"],
- "asset": data["avalon"]["asset"],
- "task": os.environ["AVALON_TASK"],
"family": data["avalon"]["family"],
- "project": {"name": project_document["name"],
- "code": project_document["data"].get("code", '')},
- "hierarchy": get_hierarchy(),
"frame": "#" * padding,
})
return anatomy.format(data)
@@ -1654,6 +1660,8 @@ def launch_workfiles_app():
from openpype.lib import (
env_value_to_bool
)
+ from avalon.nuke.pipeline import get_main_window
+
# get all imortant settings
open_at_start = env_value_to_bool(
env_key="OPENPYPE_WORKFILE_TOOL_ON_START",
@@ -1665,7 +1673,8 @@ def launch_workfiles_app():
if not opnl.workfiles_launched:
opnl.workfiles_launched = True
- host_tools.show_workfiles()
+ main_window = get_main_window()
+ host_tools.show_workfiles(parent=main_window)
def process_workfile_builder():
diff --git a/openpype/hosts/nuke/api/menu.py b/openpype/hosts/nuke/api/menu.py
index 3e74893589..78947a34da 100644
--- a/openpype/hosts/nuke/api/menu.py
+++ b/openpype/hosts/nuke/api/menu.py
@@ -6,11 +6,15 @@ from .lib import WorkfileSettings
from openpype.api import Logger, BuildWorkfile, get_current_project_settings
from openpype.tools.utils import host_tools
+from avalon.nuke.pipeline import get_main_window
+
log = Logger().get_logger(__name__)
menu_label = os.environ["AVALON_LABEL"]
+
def install():
+ main_window = get_main_window()
menubar = nuke.menu("Nuke")
menu = menubar.findItem(menu_label)
@@ -25,7 +29,7 @@ def install():
menu.removeItem(rm_item[1].name())
menu.addCommand(
name,
- host_tools.show_workfiles,
+ lambda: host_tools.show_workfiles(parent=main_window),
index=2
)
menu.addSeparator(index=3)
@@ -88,7 +92,7 @@ def install():
menu.addSeparator()
menu.addCommand(
"Experimental tools...",
- host_tools.show_experimental_tools_dialog
+ lambda: host_tools.show_experimental_tools_dialog(parent=main_window)
)
# adding shortcuts
diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py
index ffa24cfd93..36bacceb1c 100644
--- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py
+++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py
@@ -238,7 +238,7 @@ class CollectInstanceResources(pyblish.api.InstancePlugin):
})
# exception for mp4 preview
- if ".mp4" in _reminding_file:
+ if ext in ["mp4", "mov"]:
frame_start = 0
frame_end = (
(instance_data["frameEnd"] - instance_data["frameStart"])
@@ -255,6 +255,7 @@ class CollectInstanceResources(pyblish.api.InstancePlugin):
"step": 1,
"fps": self.context.data.get("fps"),
"name": "review",
+ "thumbnail": True,
"tags": ["review", "ftrackreview", "delete"],
})
diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py
index a4fed3bc3f..48c36aa067 100644
--- a/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py
+++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_scenes.py
@@ -49,10 +49,22 @@ class CollectHarmonyScenes(pyblish.api.InstancePlugin):
# fix anatomy data
anatomy_data_new = copy.deepcopy(anatomy_data)
+
+ project_entity = context.data["projectEntity"]
+ asset_entity = context.data["assetEntity"]
+
+ task_type = asset_entity["data"]["tasks"].get(task, {}).get("type")
+ project_task_types = project_entity["config"]["tasks"]
+ task_code = project_task_types.get(task_type, {}).get("short_name")
+
# updating hierarchy data
anatomy_data_new.update({
"asset": asset_data["name"],
- "task": task,
+ "task": {
+ "name": task,
+ "type": task_type,
+ "short": task_code,
+ },
"subset": subset_name
})
diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py
index 93eff85486..40a969f8df 100644
--- a/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py
+++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_harmony_zips.py
@@ -27,6 +27,7 @@ class CollectHarmonyZips(pyblish.api.InstancePlugin):
anatomy_data = instance.context.data["anatomyData"]
repres = instance.data["representations"]
files = repres[0]["files"]
+ project_entity = context.data["projectEntity"]
if files.endswith(".zip"):
# A zip file was dropped
@@ -45,14 +46,24 @@ class CollectHarmonyZips(pyblish.api.InstancePlugin):
self.log.info("Copied data: {}".format(new_instance.data))
+ task_type = asset_data["data"]["tasks"].get(task, {}).get("type")
+ project_task_types = project_entity["config"]["tasks"]
+ task_code = project_task_types.get(task_type, {}).get("short_name")
+
# fix anatomy data
anatomy_data_new = copy.deepcopy(anatomy_data)
# updating hierarchy data
- anatomy_data_new.update({
- "asset": asset_data["name"],
- "task": task,
- "subset": subset_name
- })
+ anatomy_data_new.update(
+ {
+ "asset": asset_data["name"],
+ "task": {
+ "name": task,
+ "type": task_type,
+ "short": task_code,
+ },
+ "subset": subset_name
+ }
+ )
new_instance.data["label"] = f"{instance_name}"
new_instance.data["subset"] = subset_name
diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py
deleted file mode 100644
index adbac6ef09..0000000000
--- a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py
+++ /dev/null
@@ -1,415 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Extract Harmony scene from zip file."""
-import glob
-import os
-import shutil
-import six
-import sys
-import tempfile
-import zipfile
-
-import pyblish.api
-from avalon import api, io
-import openpype.api
-from openpype.lib import get_workfile_template_key_from_context
-
-
-class ExtractHarmonyZip(openpype.api.Extractor):
- """Extract Harmony zip."""
-
- # Pyblish settings
- label = "Extract Harmony zip"
- order = pyblish.api.ExtractorOrder + 0.02
- hosts = ["standalonepublisher"]
- families = ["scene"]
-
- # Properties
- session = None
- task_types = None
- task_statuses = None
- assetversion_statuses = None
-
- # Presets
- create_workfile = True
- default_task = "harmonyIngest"
- default_task_type = "Ingest"
- default_task_status = "Ingested"
- assetversion_status = "Ingested"
-
- def process(self, instance):
- """Plugin entry point."""
- context = instance.context
- self.session = context.data["ftrackSession"]
- asset_doc = context.data["assetEntity"]
- # asset_name = instance.data["asset"]
- subset_name = instance.data["subset"]
- instance_name = instance.data["name"]
- family = instance.data["family"]
- task = context.data["anatomyData"]["task"] or self.default_task
- project_entity = instance.context.data["projectEntity"]
- ftrack_id = asset_doc["data"]["ftrackId"]
- repres = instance.data["representations"]
- submitted_staging_dir = repres[0]["stagingDir"]
- submitted_files = repres[0]["files"]
-
- # Get all the ftrack entities needed
-
- # Asset Entity
- query = 'AssetBuild where id is "{}"'.format(ftrack_id)
- asset_entity = self.session.query(query).first()
-
- # Project Entity
- query = 'Project where full_name is "{}"'.format(
- project_entity["name"]
- )
- project_entity = self.session.query(query).one()
-
- # Get Task types and Statuses for creation if needed
- self.task_types = self._get_all_task_types(project_entity)
- self.task_statuses = self._get_all_task_statuses(project_entity)
-
- # Get Statuses of AssetVersions
- self.assetversion_statuses = self._get_all_assetversion_statuses(
- project_entity
- )
-
- # Setup the status that we want for the AssetVersion
- if self.assetversion_status:
- instance.data["assetversion_status"] = self.assetversion_status
-
- # Create the default_task if it does not exist
- if task == self.default_task:
- existing_tasks = []
- entity_children = asset_entity.get('children', [])
- for child in entity_children:
- if child.entity_type.lower() == 'task':
- existing_tasks.append(child['name'].lower())
-
- if task.lower() in existing_tasks:
- print("Task {} already exists".format(task))
-
- else:
- self.create_task(
- name=task,
- task_type=self.default_task_type,
- task_status=self.default_task_status,
- parent=asset_entity,
- )
-
- # Find latest version
- latest_version = self._find_last_version(subset_name, asset_doc)
- version_number = 1
- if latest_version is not None:
- version_number += latest_version
-
- self.log.info(
- "Next version of instance \"{}\" will be {}".format(
- instance_name, version_number
- )
- )
-
- # update instance info
- instance.data["task"] = task
- instance.data["version_name"] = "{}_{}".format(subset_name, task)
- instance.data["family"] = family
- instance.data["subset"] = subset_name
- instance.data["version"] = version_number
- instance.data["latestVersion"] = latest_version
- instance.data["anatomyData"].update({
- "subset": subset_name,
- "family": family,
- "version": version_number
- })
-
- # Copy `families` and check if `family` is not in current families
- families = instance.data.get("families") or list()
- if families:
- families = list(set(families))
-
- instance.data["families"] = families
-
- # Prepare staging dir for new instance and zip + sanitize scene name
- staging_dir = tempfile.mkdtemp(prefix="pyblish_tmp_")
-
- # Handle if the representation is a .zip and not an .xstage
- pre_staged = False
- if submitted_files.endswith(".zip"):
- submitted_zip_file = os.path.join(submitted_staging_dir,
- submitted_files
- ).replace("\\", "/")
-
- pre_staged = self.sanitize_prezipped_project(instance,
- submitted_zip_file,
- staging_dir)
-
- # Get the file to work with
- source_dir = str(repres[0]["stagingDir"])
- source_file = str(repres[0]["files"])
-
- staging_scene_dir = os.path.join(staging_dir, "scene")
- staging_scene = os.path.join(staging_scene_dir, source_file)
-
- # If the file is an .xstage / directory, we must stage it
- if not pre_staged:
- shutil.copytree(source_dir, staging_scene_dir)
-
- # Rename this latest file as 'scene.xstage'
- # This is is determined in the collector from the latest scene in a
- # submitted directory / directory the submitted .xstage is in.
- # In the case of a zip file being submitted, this is determined within
- # the self.sanitize_project() method in this extractor.
- os.rename(staging_scene,
- os.path.join(staging_scene_dir, "scene.xstage")
- )
-
- # Required to set the current directory where the zip will end up
- os.chdir(staging_dir)
-
- # Create the zip file
- zip_filepath = shutil.make_archive(os.path.basename(source_dir),
- "zip",
- staging_scene_dir
- )
-
- zip_filename = os.path.basename(zip_filepath)
-
- self.log.info("Zip file: {}".format(zip_filepath))
-
- # Setup representation
- new_repre = {
- "name": "zip",
- "ext": "zip",
- "files": zip_filename,
- "stagingDir": staging_dir
- }
-
- self.log.debug(
- "Creating new representation: {}".format(new_repre)
- )
- instance.data["representations"] = [new_repre]
-
- self.log.debug("Completed prep of zipped Harmony scene: {}"
- .format(zip_filepath)
- )
-
- # If this extractor is setup to also extract a workfile...
- if self.create_workfile:
- workfile_path = self.extract_workfile(instance,
- staging_scene
- )
-
- self.log.debug("Extracted Workfile to: {}".format(workfile_path))
-
- def extract_workfile(self, instance, staging_scene):
- """Extract a valid workfile for this corresponding publish.
-
- Args:
- instance (:class:`pyblish.api.Instance`): Instance data.
- staging_scene (str): path of staging scene.
-
- Returns:
- str: Path to workdir.
-
- """
- # Since the staging scene was renamed to "scene.xstage" for publish
- # rename the staging scene in the temp stagingdir
- staging_scene = os.path.join(os.path.dirname(staging_scene),
- "scene.xstage")
-
- # Setup the data needed to form a valid work path filename
- anatomy = openpype.api.Anatomy()
- project_entity = instance.context.data["projectEntity"]
-
- data = {
- "root": api.registered_root(),
- "project": {
- "name": project_entity["name"],
- "code": project_entity["data"].get("code", '')
- },
- "asset": instance.data["asset"],
- "hierarchy": openpype.api.get_hierarchy(instance.data["asset"]),
- "family": instance.data["family"],
- "task": instance.data.get("task"),
- "subset": instance.data["subset"],
- "version": 1,
- "ext": "zip",
- }
- host_name = "harmony"
- template_name = get_workfile_template_key_from_context(
- instance.data["asset"],
- instance.data.get("task"),
- host_name,
- project_name=project_entity["name"],
- dbcon=io
- )
-
- # Get a valid work filename first with version 1
- file_template = anatomy.templates[template_name]["file"]
- anatomy_filled = anatomy.format(data)
- work_path = anatomy_filled[template_name]["path"]
-
- # Get the final work filename with the proper version
- data["version"] = api.last_workfile_with_version(
- os.path.dirname(work_path),
- file_template,
- data,
- api.HOST_WORKFILE_EXTENSIONS[host_name]
- )[1]
-
- base_name = os.path.splitext(os.path.basename(work_path))[0]
-
- staging_work_path = os.path.join(os.path.dirname(staging_scene),
- base_name + ".xstage"
- )
-
- # Rename this latest file after the workfile path filename
- os.rename(staging_scene, staging_work_path)
-
- # Required to set the current directory where the zip will end up
- os.chdir(os.path.dirname(os.path.dirname(staging_scene)))
-
- # Create the zip file
- zip_filepath = shutil.make_archive(base_name,
- "zip",
- os.path.dirname(staging_scene)
- )
- self.log.info(staging_scene)
- self.log.info(work_path)
- self.log.info(staging_work_path)
- self.log.info(os.path.dirname(os.path.dirname(staging_scene)))
- self.log.info(base_name)
- self.log.info(zip_filepath)
-
- # Create the work path on disk if it does not exist
- os.makedirs(os.path.dirname(work_path), exist_ok=True)
- shutil.copy(zip_filepath, work_path)
-
- return work_path
-
- def sanitize_prezipped_project(
- self, instance, zip_filepath, staging_dir):
- """Fix when a zip contains a folder.
-
- Handle zip file root contains folder instead of the project.
-
- Args:
- instance (:class:`pyblish.api.Instance`): Instance data.
- zip_filepath (str): Path to zip.
- staging_dir (str): Path to staging directory.
-
- """
- zip = zipfile.ZipFile(zip_filepath)
- zip_contents = zipfile.ZipFile.namelist(zip)
-
- # Determine if any xstage file is in root of zip
- project_in_root = [pth for pth in zip_contents
- if "/" not in pth and pth.endswith(".xstage")]
-
- staging_scene_dir = os.path.join(staging_dir, "scene")
-
- # The project is nested, so we must extract and move it
- if not project_in_root:
-
- staging_tmp_dir = os.path.join(staging_dir, "tmp")
-
- with zipfile.ZipFile(zip_filepath, "r") as zip_ref:
- zip_ref.extractall(staging_tmp_dir)
-
- nested_project_folder = os.path.join(staging_tmp_dir,
- zip_contents[0]
- )
-
- shutil.copytree(nested_project_folder, staging_scene_dir)
-
- else:
- # The project is not nested, so we just extract to scene folder
- with zipfile.ZipFile(zip_filepath, "r") as zip_ref:
- zip_ref.extractall(staging_scene_dir)
-
- latest_file = max(glob.iglob(staging_scene_dir + "/*.xstage"),
- key=os.path.getctime).replace("\\", "/")
-
- instance.data["representations"][0]["stagingDir"] = staging_scene_dir
- instance.data["representations"][0]["files"] = os.path.basename(
- latest_file)
-
- # We have staged the scene already so return True
- return True
-
- def _find_last_version(self, subset_name, asset_doc):
- """Find last version of subset."""
- subset_doc = io.find_one({
- "type": "subset",
- "name": subset_name,
- "parent": asset_doc["_id"]
- })
-
- if subset_doc is None:
- self.log.debug("Subset entity does not exist yet.")
- else:
- version_doc = io.find_one(
- {
- "type": "version",
- "parent": subset_doc["_id"]
- },
- sort=[("name", -1)]
- )
- if version_doc:
- return int(version_doc["name"])
- return None
-
- def _get_all_task_types(self, project):
- """Get all task types."""
- tasks = {}
- proj_template = project['project_schema']
- temp_task_types = proj_template['_task_type_schema']['types']
-
- for type in temp_task_types:
- if type['name'] not in tasks:
- tasks[type['name']] = type
-
- return tasks
-
- def _get_all_task_statuses(self, project):
- """Get all statuses of tasks."""
- statuses = {}
- proj_template = project['project_schema']
- temp_task_statuses = proj_template.get_statuses("Task")
-
- for status in temp_task_statuses:
- if status['name'] not in statuses:
- statuses[status['name']] = status
-
- return statuses
-
- def _get_all_assetversion_statuses(self, project):
- """Get statuses of all asset versions."""
- statuses = {}
- proj_template = project['project_schema']
- temp_task_statuses = proj_template.get_statuses("AssetVersion")
-
- for status in temp_task_statuses:
- if status['name'] not in statuses:
- statuses[status['name']] = status
-
- return statuses
-
- def _create_task(self, name, task_type, parent, task_status):
- """Create task."""
- task_data = {
- 'name': name,
- 'parent': parent,
- }
- self.log.info(task_type)
- task_data['type'] = self.task_types[task_type]
- task_data['status'] = self.task_statuses[task_status]
- self.log.info(task_data)
- task = self.session.create('Task', task_data)
- try:
- self.session.commit()
- except Exception:
- tp, value, tb = sys.exc_info()
- self.session.rollback()
- six.reraise(tp, value, tb)
-
- return task
diff --git a/openpype/hosts/tvpaint/plugins/load/load_workfile.py b/openpype/hosts/tvpaint/plugins/load/load_workfile.py
new file mode 100644
index 0000000000..f410a1ab9d
--- /dev/null
+++ b/openpype/hosts/tvpaint/plugins/load/load_workfile.py
@@ -0,0 +1,102 @@
+import getpass
+import os
+
+from avalon.tvpaint import lib, pipeline, get_current_workfile_context
+from avalon import api, io
+from openpype.lib import (
+ get_workfile_template_key_from_context,
+ get_workdir_data
+)
+from openpype.api import Anatomy
+
+
+class LoadWorkfile(pipeline.Loader):
+ """Load workfile."""
+
+ families = ["workfile"]
+ representations = ["tvpp"]
+
+ label = "Load Workfile"
+
+ def load(self, context, name, namespace, options):
+ # Load context of current workfile as first thing
+ # - which context and extension has
+ host = api.registered_host()
+ current_file = host.current_file()
+
+ context = get_current_workfile_context()
+
+ filepath = self.fname.replace("\\", "/")
+
+ if not os.path.exists(filepath):
+ raise FileExistsError(
+ "The loaded file does not exist. Try downloading it first."
+ )
+
+ george_script = "tv_LoadProject '\"'\"{}\"'\"'".format(
+ filepath
+ )
+ lib.execute_george_through_file(george_script)
+
+ # Save workfile.
+ host_name = "tvpaint"
+ asset_name = context.get("asset")
+ task_name = context.get("task")
+ # Far cases when there is workfile without context
+ if not asset_name:
+ asset_name = io.Session["AVALON_ASSET"]
+ task_name = io.Session["AVALON_TASK"]
+
+ project_doc = io.find_one({
+ "type": "project"
+ })
+ asset_doc = io.find_one({
+ "type": "asset",
+ "name": asset_name
+ })
+ project_name = project_doc["name"]
+
+ template_key = get_workfile_template_key_from_context(
+ asset_name,
+ task_name,
+ host_name,
+ project_name=project_name,
+ dbcon=io
+ )
+ anatomy = Anatomy(project_name)
+
+ data = get_workdir_data(project_doc, asset_doc, task_name, host_name)
+ data["root"] = anatomy.roots
+ data["user"] = getpass.getuser()
+
+ template = anatomy.templates[template_key]["file"]
+
+ # Define saving file extension
+ if current_file:
+ # Match the extension of current file
+ _, extension = os.path.splitext(current_file)
+ else:
+ # Fall back to the first extension supported for this host.
+ extension = host.file_extensions()[0]
+
+ data["ext"] = extension
+
+ work_root = api.format_template_with_optional_keys(
+ data, anatomy.templates[template_key]["folder"]
+ )
+ version = api.last_workfile_with_version(
+ work_root, template, data, host.file_extensions()
+ )[1]
+
+ if version is None:
+ version = 1
+ else:
+ version += 1
+
+ data["version"] = version
+
+ path = os.path.join(
+ work_root,
+ api.format_template_with_optional_keys(data, template)
+ )
+ host.save_file(path)
diff --git a/openpype/lib/anatomy.py b/openpype/lib/anatomy.py
index 7a4a55363c..aaf10479fd 100644
--- a/openpype/lib/anatomy.py
+++ b/openpype/lib/anatomy.py
@@ -989,6 +989,14 @@ class Templates:
invalid_required = []
missing_required = []
replace_keys = []
+
+ task_data = data.get("task")
+ if (
+ isinstance(task_data, StringType)
+ and "{task[name]}" in orig_template
+ ):
+ data["task"] = {"name": task_data}
+
for group in self.key_pattern.findall(template):
orig_key = group[1:-1]
key = str(orig_key)
@@ -1074,6 +1082,10 @@ class Templates:
output = collections.defaultdict(dict)
for key, orig_value in templates.items():
if isinstance(orig_value, StringType):
+ # Replace {task} by '{task[name]}' for backward compatibility
+ if '{task}' in orig_value:
+ orig_value = orig_value.replace('{task}', '{task[name]}')
+
output[key] = self._format(orig_value, data)
continue
diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py
index b9bcecd3a0..30be92e886 100644
--- a/openpype/lib/applications.py
+++ b/openpype/lib/applications.py
@@ -1280,23 +1280,12 @@ def prepare_context_environments(data):
anatomy = data["anatomy"]
- asset_tasks = asset_doc.get("data", {}).get("tasks") or {}
- task_info = asset_tasks.get(task_name) or {}
- task_type = task_info.get("type")
+ task_type = workdir_data["task"]["type"]
# Temp solution how to pass task type to `_prepare_last_workfile`
data["task_type"] = task_type
- workfile_template_key = get_workfile_template_key(
- task_type,
- app.host_name,
- project_name=project_name,
- project_settings=project_settings
- )
-
try:
- workdir = get_workdir_with_workdir_data(
- workdir_data, anatomy, template_key=workfile_template_key
- )
+ workdir = get_workdir_with_workdir_data(workdir_data, anatomy)
except Exception as exc:
raise ApplicationLaunchFailed(
@@ -1329,10 +1318,10 @@ def prepare_context_environments(data):
)
data["env"].update(context_env)
- _prepare_last_workfile(data, workdir, workfile_template_key)
+ _prepare_last_workfile(data, workdir)
-def _prepare_last_workfile(data, workdir, workfile_template_key):
+def _prepare_last_workfile(data, workdir):
"""last workfile workflow preparation.
Function check if should care about last workfile workflow and tries
@@ -1395,6 +1384,10 @@ def _prepare_last_workfile(data, workdir, workfile_template_key):
anatomy = data["anatomy"]
# Find last workfile
file_template = anatomy.templates["work"]["file"]
+ # Replace {task} by '{task[name]}' for backward compatibility
+ if '{task}' in file_template:
+ file_template = file_template.replace('{task}', '{task[name]}')
+
workdir_data.update({
"version": 1,
"user": get_openpype_username(),
diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py
index b043cbfdb4..372e116f43 100644
--- a/openpype/lib/avalon_context.py
+++ b/openpype/lib/avalon_context.py
@@ -7,6 +7,7 @@ import platform
import logging
import collections
import functools
+import getpass
from openpype.settings import get_project_settings
from .anatomy import Anatomy
@@ -464,6 +465,7 @@ def get_workfile_template_key(
return default
+# TODO rename function as is not just "work" specific
def get_workdir_data(project_doc, asset_doc, task_name, host_name):
"""Prepare data for workdir template filling from entered information.
@@ -479,22 +481,31 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name):
"""
hierarchy = "/".join(asset_doc["data"]["parents"])
+ task_type = asset_doc['data']['tasks'].get(task_name, {}).get('type')
+
+ project_task_types = project_doc["config"]["tasks"]
+ task_code = project_task_types.get(task_type, {}).get("short_name")
+
data = {
"project": {
"name": project_doc["name"],
"code": project_doc["data"].get("code")
},
- "task": task_name,
+ "task": {
+ "name": task_name,
+ "type": task_type,
+ "short": task_code,
+ },
"asset": asset_doc["name"],
"app": host_name,
- "hierarchy": hierarchy
+ "user": getpass.getuser(),
+ "hierarchy": hierarchy,
}
return data
def get_workdir_with_workdir_data(
- workdir_data, anatomy=None, project_name=None,
- template_key=None, dbcon=None
+ workdir_data, anatomy=None, project_name=None, template_key=None
):
"""Fill workdir path from entered data and project's anatomy.
@@ -529,12 +540,10 @@ def get_workdir_with_workdir_data(
anatomy = Anatomy(project_name)
if not template_key:
- template_key = get_workfile_template_key_from_context(
- workdir_data["asset"],
- workdir_data["task"],
+ template_key = get_workfile_template_key(
+ workdir_data["task"]["type"],
workdir_data["app"],
- project_name=workdir_data["project"]["name"],
- dbcon=dbcon
+ project_name=workdir_data["project"]["name"]
)
anatomy_filled = anatomy.format(workdir_data)
@@ -648,7 +657,7 @@ def create_workfile_doc(asset_doc, task_name, filename, workdir, dbcon=None):
anatomy = Anatomy(project_doc["name"])
# Get workdir path (result is anatomy.TemplateResult)
template_workdir = get_workdir_with_workdir_data(
- workdir_data, anatomy, dbcon=dbcon
+ workdir_data, anatomy
)
template_workdir_path = str(template_workdir).replace("\\", "/")
diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py
index 6b07749819..adf6d2d758 100644
--- a/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py
+++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py
@@ -104,7 +104,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
families = ["render.farm", "prerender.farm",
"renderlayer", "imagesequence", "vrayscene"]
- aov_filter = {"maya": [r".*(?:\.|_)*([Bb]eauty)(?:\.|_)*.*"],
+ aov_filter = {"maya": [r".*(?:[\._-])*([Bb]eauty)(?:[\.|_])*.*"],
"aftereffects": [r".*"], # for everything from AE
"harmony": [r".*"], # for everything from AE
"celaction": [r".*"]}
@@ -231,7 +231,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
args = [
'publish',
roothless_metadata_path,
- "--targets {}".format("deadline")
+ "--targets", "deadline",
+ "--targets", "filesequence"
]
# Generate the payload for Deadline submission
diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/integrate_remove_components.py b/openpype/modules/default_modules/ftrack/plugins/publish/integrate_remove_components.py
deleted file mode 100644
index 26cac0f1ae..0000000000
--- a/openpype/modules/default_modules/ftrack/plugins/publish/integrate_remove_components.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import pyblish.api
-import os
-
-
-class IntegrateCleanComponentData(pyblish.api.InstancePlugin):
- """
- Cleaning up thumbnail an mov files after they have been integrated
- """
-
- order = pyblish.api.IntegratorOrder + 0.5
- label = 'Clean component data'
- families = ["ftrack"]
- optional = True
- active = False
-
- def process(self, instance):
-
- for comp in instance.data['representations']:
- self.log.debug('component {}'.format(comp))
-
- if "%" in comp['published_path'] or "#" in comp['published_path']:
- continue
-
- if comp.get('thumbnail') or ("thumbnail" in comp.get('tags', [])):
- os.remove(comp['published_path'])
- self.log.info('Thumbnail image was erased')
-
- elif comp.get('preview') or ("preview" in comp.get('tags', [])):
- os.remove(comp['published_path'])
- self.log.info('Preview mov file was erased')
diff --git a/openpype/modules/default_modules/royal_render/rr_root/plugins/control_job/perjob/m50__openpype_publish_render.py b/openpype/modules/default_modules/royal_render/rr_root/plugins/control_job/perjob/m50__openpype_publish_render.py
index 17e4fb38d1..290f26a44a 100644
--- a/openpype/modules/default_modules/royal_render/rr_root/plugins/control_job/perjob/m50__openpype_publish_render.py
+++ b/openpype/modules/default_modules/royal_render/rr_root/plugins/control_job/perjob/m50__openpype_publish_render.py
@@ -148,12 +148,27 @@ class OpenPypeContextSelector:
for k, v in env.items():
print(" {}: {}".format(k, v))
+ publishing_paths = [os.path.join(self.job.imageDir,
+ os.path.dirname(
+ self.job.imageFileName))]
+
+ # add additional channels
+ channel_idx = 0
+ channel = self.job.channelFileName(channel_idx)
+ while channel:
+ channel_path = os.path.dirname(
+ os.path.join(self.job.imageDir, channel))
+ if channel_path not in publishing_paths:
+ publishing_paths.append(channel_path)
+ channel_idx += 1
+ channel = self.job.channelFileName(channel_idx)
+
args = [os.path.join(self.openpype_root, self.openpype_executable),
- 'publish', '-t', "rr_control", "--gui",
- os.path.join(self.job.imageDir,
- os.path.dirname(self.job.imageFileName))
+ 'publish', '-t', "rr_control", "--gui"
]
+ args += publishing_paths
+
print(">>> running {}".format(" ".join(args)))
orig = os.environ.copy()
orig.update(env)
diff --git a/openpype/modules/default_modules/sync_server/providers/sftp.py b/openpype/modules/default_modules/sync_server/providers/sftp.py
index 4f505ae016..3390cd5d3d 100644
--- a/openpype/modules/default_modules/sync_server/providers/sftp.py
+++ b/openpype/modules/default_modules/sync_server/providers/sftp.py
@@ -192,7 +192,7 @@ class SFTPHandler(AbstractProvider):
Format is importing for usage of python's format ** approach
"""
# roots cannot be locally overridden
- return self.presets['roots']
+ return self.presets['root']
def get_tree(self):
"""
diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py
index 610ef6d8e2..948b719851 100644
--- a/openpype/pipeline/create/__init__.py
+++ b/openpype/pipeline/create/__init__.py
@@ -1,3 +1,6 @@
+from .constants import (
+ SUBSET_NAME_ALLOWED_SYMBOLS
+)
from .creator_plugins import (
CreatorError,
@@ -13,6 +16,8 @@ from .context import (
__all__ = (
+ "SUBSET_NAME_ALLOWED_SYMBOLS",
+
"CreatorError",
"BaseCreator",
diff --git a/openpype/pipeline/create/constants.py b/openpype/pipeline/create/constants.py
new file mode 100644
index 0000000000..bfbbccfd12
--- /dev/null
+++ b/openpype/pipeline/create/constants.py
@@ -0,0 +1,6 @@
+SUBSET_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_."
+
+
+__all__ = (
+ "SUBSET_NAME_ALLOWED_SYMBOLS",
+)
diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py
index ec88d5669d..6b95979b76 100644
--- a/openpype/plugins/publish/collect_anatomy_context_data.py
+++ b/openpype/plugins/publish/collect_anatomy_context_data.py
@@ -54,6 +54,12 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin):
if hierarchy_items:
hierarchy = os.path.join(*hierarchy_items)
+ asset_tasks = asset_entity["data"]["tasks"]
+ task_type = asset_tasks.get(task_name, {}).get("type")
+
+ project_task_types = project_entity["config"]["tasks"]
+ task_code = project_task_types.get(task_type, {}).get("short_name")
+
context_data = {
"project": {
"name": project_entity["name"],
@@ -61,7 +67,11 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin):
},
"asset": asset_entity["name"],
"hierarchy": hierarchy.replace("\\", "/"),
- "task": task_name,
+ "task": {
+ "name": task_name,
+ "type": task_type,
+ "short": task_code,
+ },
"username": context.data["user"],
"app": context.data["hostName"]
}
diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py
index e0eb1618b5..da6a2195ee 100644
--- a/openpype/plugins/publish/collect_anatomy_instance_data.py
+++ b/openpype/plugins/publish/collect_anatomy_instance_data.py
@@ -214,6 +214,8 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
project_doc = context.data["projectEntity"]
context_asset_doc = context.data["assetEntity"]
+ project_task_types = project_doc["config"]["tasks"]
+
for instance in context:
if self.follow_workfile_version:
version_number = context.data('version')
@@ -245,7 +247,18 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
# Task
task_name = instance.data.get("task")
if task_name:
- anatomy_updates["task"] = task_name
+ asset_tasks = asset_doc["data"]["tasks"]
+ task_type = asset_tasks.get(task_name, {}).get("type")
+ task_code = (
+ project_task_types
+ .get(task_type, {})
+ .get("short_name")
+ )
+ anatomy_updates["task"] = {
+ "name": task_name,
+ "type": task_type,
+ "short": task_code
+ }
# Additional data
resolution_width = instance.data.get("resolutionWidth")
diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py
index 06eb85c593..cbebed927a 100644
--- a/openpype/plugins/publish/extract_burnin.py
+++ b/openpype/plugins/publish/extract_burnin.py
@@ -184,7 +184,9 @@ class ExtractBurnin(openpype.api.Extractor):
for key in self.positions:
value = burnin_def.get(key)
if value:
- burnin_values[key] = value
+ burnin_values[key] = value.replace(
+ "{task}", "{task[name]}"
+ )
# Remove "delete" tag from new representation
if "delete" in new_repre["tags"]:
diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py
index 7ff7466a2a..1611bd4afd 100644
--- a/openpype/plugins/publish/integrate_new.py
+++ b/openpype/plugins/publish/integrate_new.py
@@ -172,21 +172,26 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
anatomy_data["hierarchy"] = hierarchy
# Make sure task name in anatomy data is same as on instance.data
- task_name = instance.data.get("task")
- if task_name:
- anatomy_data["task"] = task_name
- else:
- # Just set 'task_name' variable to context task
- task_name = anatomy_data["task"]
-
- # Find task type for current task name
- # - this should be already prepared on instance
asset_tasks = (
asset_entity.get("data", {}).get("tasks")
) or {}
- task_info = asset_tasks.get(task_name) or {}
- task_type = task_info.get("type")
- instance.data["task_type"] = task_type
+ task_name = instance.data.get("task")
+ if task_name:
+ task_info = asset_tasks.get(task_name) or {}
+ task_type = task_info.get("type")
+
+ project_task_types = project_entity["config"]["tasks"]
+ task_code = project_task_types.get(task_type, {}).get("short_name")
+ anatomy_data["task"] = {
+ "name": task_name,
+ "type": task_type,
+ "short": task_code
+ }
+
+ else:
+ # Just set 'task_name' variable to context task
+ task_name = anatomy_data["task"]["name"]
+ task_type = anatomy_data["task"]["type"]
# Fill family in anatomy data
anatomy_data["family"] = instance.data.get("family")
@@ -804,11 +809,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
# - is there a chance that task name is not filled in anatomy
# data?
# - should we use context task in that case?
- task_name = (
- instance.data["anatomyData"]["task"]
- or io.Session["AVALON_TASK"]
- )
- task_type = instance.data["task_type"]
+ task_name = instance.data["anatomyData"]["task"]["name"]
+ task_type = instance.data["anatomyData"]["task"]["type"]
filtering_criteria = {
"families": instance.data["family"],
"hosts": instance.context.data["hostName"],
diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py
index eebba61af3..7359ccf360 100644
--- a/openpype/plugins/publish/validate_editorial_asset_name.py
+++ b/openpype/plugins/publish/validate_editorial_asset_name.py
@@ -12,6 +12,12 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin):
order = pyblish.api.ValidatorOrder
label = "Validate Editorial Asset Name"
+ hosts = [
+ "hiero",
+ "standalonepublisher",
+ "resolve",
+ "flame"
+ ]
def process(self, context):
diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py
index f0ba9a997e..519e7c285b 100644
--- a/openpype/pype_commands.py
+++ b/openpype/pype_commands.py
@@ -392,3 +392,10 @@ class PypeCommands:
import time
while True:
time.sleep(1.0)
+
+ def repack_version(self, directory):
+ """Repacking OpenPype version."""
+ from openpype.tools.repack_version import VersionRepacker
+
+ version_packer = VersionRepacker(directory)
+ version_packer.process()
diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json
index 53abd35ed5..9a03b893bf 100644
--- a/openpype/settings/defaults/project_anatomy/templates.json
+++ b/openpype/settings/defaults/project_anatomy/templates.json
@@ -6,8 +6,8 @@
"frame": "{frame:0>{@frame_padding}}"
},
"work": {
- "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task}",
- "file": "{project[code]}_{asset}_{task}_{@version}<_{comment}>.{ext}",
+ "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}",
+ "file": "{project[code]}_{asset}_{task[name]}_{@version}<_{comment}>.{ext}",
"path": "{@folder}/{@file}"
},
"render": {
diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json
index 689d6418ba..73c75ef3ee 100644
--- a/openpype/settings/defaults/project_settings/maya.json
+++ b/openpype/settings/defaults/project_settings/maya.json
@@ -42,7 +42,8 @@
"enabled": true,
"defaults": [
"Main"
- ]
+ ],
+ "aov_separator": "underscore"
},
"CreateAnimation": {
"enabled": true,
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_templates.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_templates.json
index a8534e7e29..e208069e6f 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_templates.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_templates.json
@@ -11,6 +11,10 @@
"type": "dict",
"key": "defaults",
"children": [
+ {
+ "type": "label",
+ "label": "The list of existing placeholders is available here:
https://openpype.io/docs/admin_settings_project_anatomy/#available-template-keys "
+ },
{
"type": "number",
"key": "version_padding",
diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json
index 44a35af7c1..e50357cc40 100644
--- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json
+++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json
@@ -46,6 +46,18 @@
"key": "defaults",
"label": "Default Subsets",
"object_type": "text"
+ },
+ {
+ "key": "aov_separator",
+ "label": "AOV Separator character",
+ "type": "enum",
+ "multiselection": false,
+ "default": "underscore",
+ "enum_items": [
+ {"dash": "- (dash)"},
+ {"underscore": "_ (underscore)"},
+ {"dot": ". (dot)"}
+ ]
}
]
},
diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py
index 60ed54bd4a..ff75562413 100644
--- a/openpype/settings/lib.py
+++ b/openpype/settings/lib.py
@@ -856,6 +856,7 @@ def get_anatomy_settings(
apply_local_settings_on_anatomy_settings(
result, local_settings, project_name, site_name
)
+
return result
diff --git a/openpype/style/__init__.py b/openpype/style/__init__.py
index fd39e93b5d..cb0595d522 100644
--- a/openpype/style/__init__.py
+++ b/openpype/style/__init__.py
@@ -128,9 +128,13 @@ def _load_font():
_FONT_IDS = []
fonts_dirpath = os.path.join(current_dir, "fonts")
font_dirs = []
- font_dirs.append(os.path.join(fonts_dirpath, "Montserrat"))
- font_dirs.append(os.path.join(fonts_dirpath, "Spartan"))
- font_dirs.append(os.path.join(fonts_dirpath, "RobotoMono", "static"))
+ font_dirs.append(os.path.join(fonts_dirpath, "Noto_Sans"))
+ font_dirs.append(os.path.join(
+ fonts_dirpath,
+ "Noto_Sans_Mono",
+ "static",
+ "NotoSansMono"
+ ))
loaded_fonts = []
for font_dir in font_dirs:
diff --git a/openpype/style/fonts/Montserrat/Montserrat-Black.ttf b/openpype/style/fonts/Montserrat/Montserrat-Black.ttf
deleted file mode 100644
index 437b1157cb..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-Black.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-BlackItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-BlackItalic.ttf
deleted file mode 100644
index 52348354c2..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-BlackItalic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-Bold.ttf b/openpype/style/fonts/Montserrat/Montserrat-Bold.ttf
deleted file mode 100644
index 221819bca0..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-Bold.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-BoldItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-BoldItalic.ttf
deleted file mode 100644
index 9ae2bd240f..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-BoldItalic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-ExtraBold.ttf b/openpype/style/fonts/Montserrat/Montserrat-ExtraBold.ttf
deleted file mode 100644
index 80ea8061b0..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-ExtraBold.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-ExtraBoldItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-ExtraBoldItalic.ttf
deleted file mode 100644
index 6c961e1cc9..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-ExtraBoldItalic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-ExtraLight.ttf b/openpype/style/fonts/Montserrat/Montserrat-ExtraLight.ttf
deleted file mode 100644
index ca0bbb6569..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-ExtraLight.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-ExtraLightItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-ExtraLightItalic.ttf
deleted file mode 100644
index f3c1559ec7..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-ExtraLightItalic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-Italic.ttf b/openpype/style/fonts/Montserrat/Montserrat-Italic.ttf
deleted file mode 100644
index eb4232a0c2..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-Italic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-Light.ttf b/openpype/style/fonts/Montserrat/Montserrat-Light.ttf
deleted file mode 100644
index 990857de8e..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-Light.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-LightItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-LightItalic.ttf
deleted file mode 100644
index 209604046b..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-LightItalic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-Medium.ttf b/openpype/style/fonts/Montserrat/Montserrat-Medium.ttf
deleted file mode 100644
index 6e079f6984..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-Medium.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-MediumItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-MediumItalic.ttf
deleted file mode 100644
index 0dc3ac9c29..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-MediumItalic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-Regular.ttf b/openpype/style/fonts/Montserrat/Montserrat-Regular.ttf
deleted file mode 100644
index 8d443d5d56..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-Regular.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-SemiBold.ttf b/openpype/style/fonts/Montserrat/Montserrat-SemiBold.ttf
deleted file mode 100644
index f8a43f2b20..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-SemiBold.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-SemiBoldItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-SemiBoldItalic.ttf
deleted file mode 100644
index 336c56ec0c..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-SemiBoldItalic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-Thin.ttf b/openpype/style/fonts/Montserrat/Montserrat-Thin.ttf
deleted file mode 100644
index b9858757eb..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-Thin.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Montserrat/Montserrat-ThinItalic.ttf b/openpype/style/fonts/Montserrat/Montserrat-ThinItalic.ttf
deleted file mode 100644
index e488998ec7..0000000000
Binary files a/openpype/style/fonts/Montserrat/Montserrat-ThinItalic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Noto_Sans/NotoSans-Bold.ttf b/openpype/style/fonts/Noto_Sans/NotoSans-Bold.ttf
new file mode 100644
index 0000000000..54ad879b41
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans/NotoSans-Bold.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans/NotoSans-BoldItalic.ttf b/openpype/style/fonts/Noto_Sans/NotoSans-BoldItalic.ttf
new file mode 100644
index 0000000000..530a82835d
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans/NotoSans-BoldItalic.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans/NotoSans-Italic.ttf b/openpype/style/fonts/Noto_Sans/NotoSans-Italic.ttf
new file mode 100644
index 0000000000..27ff1ed60a
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans/NotoSans-Italic.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans/NotoSans-Regular.ttf b/openpype/style/fonts/Noto_Sans/NotoSans-Regular.ttf
new file mode 100644
index 0000000000..10589e277e
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans/NotoSans-Regular.ttf differ
diff --git a/openpype/style/fonts/Spartan/OFL.txt b/openpype/style/fonts/Noto_Sans/OFL.txt
similarity index 98%
rename from openpype/style/fonts/Spartan/OFL.txt
rename to openpype/style/fonts/Noto_Sans/OFL.txt
index 808b610ffd..c9857270cc 100644
--- a/openpype/style/fonts/Spartan/OFL.txt
+++ b/openpype/style/fonts/Noto_Sans/OFL.txt
@@ -1,4 +1,4 @@
-Copyright 2020 The Spartan Project Authors (https://github.com/bghryct/Spartan-MB)
+Copyright 2012 Google Inc. All Rights Reserved.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
diff --git a/openpype/style/fonts/Noto_Sans_Mono/NotoSansMono-VariableFont_wdth,wght.ttf b/openpype/style/fonts/Noto_Sans_Mono/NotoSansMono-VariableFont_wdth,wght.ttf
new file mode 100644
index 0000000000..9dabd9e7a4
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/NotoSansMono-VariableFont_wdth,wght.ttf differ
diff --git a/openpype/style/fonts/Montserrat/OFL.txt b/openpype/style/fonts/Noto_Sans_Mono/OFL.txt
similarity index 97%
rename from openpype/style/fonts/Montserrat/OFL.txt
rename to openpype/style/fonts/Noto_Sans_Mono/OFL.txt
index f435ed8b5e..c9857270cc 100644
--- a/openpype/style/fonts/Montserrat/OFL.txt
+++ b/openpype/style/fonts/Noto_Sans_Mono/OFL.txt
@@ -1,4 +1,4 @@
-Copyright 2011 The Montserrat Project Authors (https://github.com/JulietaUla/Montserrat)
+Copyright 2012 Google Inc. All Rights Reserved.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
diff --git a/openpype/style/fonts/Noto_Sans_Mono/README.txt b/openpype/style/fonts/Noto_Sans_Mono/README.txt
new file mode 100644
index 0000000000..b8a8fdb965
--- /dev/null
+++ b/openpype/style/fonts/Noto_Sans_Mono/README.txt
@@ -0,0 +1,99 @@
+Noto Sans Mono Variable Font
+============================
+
+This download contains Noto Sans Mono as both a variable font and static fonts.
+
+Noto Sans Mono is a variable font with these axes:
+ wdth
+ wght
+
+This means all the styles are contained in a single file:
+ NotoSansMono-VariableFont_wdth,wght.ttf
+
+If your app fully supports variable fonts, you can now pick intermediate styles
+that aren’t available as static fonts. Not all apps support variable fonts, and
+in those cases you can use the static font files for Noto Sans Mono:
+ static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Thin.ttf
+ static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-ExtraLight.ttf
+ static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Light.ttf
+ static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Regular.ttf
+ static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Medium.ttf
+ static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-SemiBold.ttf
+ static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Bold.ttf
+ static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-ExtraBold.ttf
+ static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Black.ttf
+ static/NotoSansMono_Condensed/NotoSansMono_Condensed-Thin.ttf
+ static/NotoSansMono_Condensed/NotoSansMono_Condensed-ExtraLight.ttf
+ static/NotoSansMono_Condensed/NotoSansMono_Condensed-Light.ttf
+ static/NotoSansMono_Condensed/NotoSansMono_Condensed-Regular.ttf
+ static/NotoSansMono_Condensed/NotoSansMono_Condensed-Medium.ttf
+ static/NotoSansMono_Condensed/NotoSansMono_Condensed-SemiBold.ttf
+ static/NotoSansMono_Condensed/NotoSansMono_Condensed-Bold.ttf
+ static/NotoSansMono_Condensed/NotoSansMono_Condensed-ExtraBold.ttf
+ static/NotoSansMono_Condensed/NotoSansMono_Condensed-Black.ttf
+ static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Thin.ttf
+ static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-ExtraLight.ttf
+ static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Light.ttf
+ static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Regular.ttf
+ static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Medium.ttf
+ static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-SemiBold.ttf
+ static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Bold.ttf
+ static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-ExtraBold.ttf
+ static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Black.ttf
+ static/NotoSansMono/NotoSansMono-Thin.ttf
+ static/NotoSansMono/NotoSansMono-ExtraLight.ttf
+ static/NotoSansMono/NotoSansMono-Light.ttf
+ static/NotoSansMono/NotoSansMono-Regular.ttf
+ static/NotoSansMono/NotoSansMono-Medium.ttf
+ static/NotoSansMono/NotoSansMono-SemiBold.ttf
+ static/NotoSansMono/NotoSansMono-Bold.ttf
+ static/NotoSansMono/NotoSansMono-ExtraBold.ttf
+ static/NotoSansMono/NotoSansMono-Black.ttf
+
+Get started
+-----------
+
+1. Install the font files you want to use
+
+2. Use your app's font picker to view the font family and all the
+available styles
+
+Learn more about variable fonts
+-------------------------------
+
+ https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
+ https://variablefonts.typenetwork.com
+ https://medium.com/variable-fonts
+
+In desktop apps
+
+ https://theblog.adobe.com/can-variable-fonts-illustrator-cc
+ https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
+
+Online
+
+ https://developers.google.com/fonts/docs/getting_started
+ https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
+ https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
+
+Installing fonts
+
+ MacOS: https://support.apple.com/en-us/HT201749
+ Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
+ Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
+
+Android Apps
+
+ https://developers.google.com/fonts/docs/android
+ https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
+
+License
+-------
+Please read the full license text (OFL.txt) to understand the permissions,
+restrictions and requirements for usage, redistribution, and modification.
+
+You can use them freely in your products & projects - print or digital,
+commercial or otherwise.
+
+This isn't legal advice, please consider consulting a lawyer and see the full
+license for all details.
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Black.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Black.ttf
new file mode 100644
index 0000000000..75fe4b4fe9
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Black.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Bold.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Bold.ttf
new file mode 100644
index 0000000000..9cefe497da
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Bold.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-ExtraBold.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-ExtraBold.ttf
new file mode 100644
index 0000000000..9961afc716
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-ExtraBold.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-ExtraLight.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-ExtraLight.ttf
new file mode 100644
index 0000000000..03ab3f87f2
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-ExtraLight.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Light.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Light.ttf
new file mode 100644
index 0000000000..19a5af2422
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Light.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Medium.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Medium.ttf
new file mode 100644
index 0000000000..62231544b0
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Medium.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Regular.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Regular.ttf
new file mode 100644
index 0000000000..a850b21ca3
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Regular.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-SemiBold.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-SemiBold.ttf
new file mode 100644
index 0000000000..0f4dffc421
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-SemiBold.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Thin.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Thin.ttf
new file mode 100644
index 0000000000..0ecd83c350
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono/NotoSansMono-Thin.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Black.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Black.ttf
new file mode 100644
index 0000000000..77ef132a1c
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Black.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Bold.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Bold.ttf
new file mode 100644
index 0000000000..41dbc9e543
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Bold.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-ExtraBold.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-ExtraBold.ttf
new file mode 100644
index 0000000000..640ae09cec
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-ExtraBold.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-ExtraLight.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-ExtraLight.ttf
new file mode 100644
index 0000000000..02fe86abbb
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-ExtraLight.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Light.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Light.ttf
new file mode 100644
index 0000000000..a0dfac1f80
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Light.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Medium.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Medium.ttf
new file mode 100644
index 0000000000..72a1fa5a87
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Medium.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Regular.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Regular.ttf
new file mode 100644
index 0000000000..8e8591cd89
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Regular.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-SemiBold.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-SemiBold.ttf
new file mode 100644
index 0000000000..b7843ceb04
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-SemiBold.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Thin.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Thin.ttf
new file mode 100644
index 0000000000..42f4493555
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_Condensed/NotoSansMono_Condensed-Thin.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Black.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Black.ttf
new file mode 100644
index 0000000000..6ad6ad9188
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Black.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Bold.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Bold.ttf
new file mode 100644
index 0000000000..4cdda1512c
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Bold.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-ExtraBold.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-ExtraBold.ttf
new file mode 100644
index 0000000000..0d428829a9
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-ExtraBold.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-ExtraLight.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-ExtraLight.ttf
new file mode 100644
index 0000000000..c3b01f97c4
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-ExtraLight.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Light.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Light.ttf
new file mode 100644
index 0000000000..be5b1209e8
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Light.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Medium.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Medium.ttf
new file mode 100644
index 0000000000..5fbb4d9a55
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Medium.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Regular.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Regular.ttf
new file mode 100644
index 0000000000..eac82bf3b4
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Regular.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-SemiBold.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-SemiBold.ttf
new file mode 100644
index 0000000000..9a75e32feb
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-SemiBold.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Thin.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Thin.ttf
new file mode 100644
index 0000000000..b710820d7e
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_ExtraCondensed/NotoSansMono_ExtraCondensed-Thin.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Black.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Black.ttf
new file mode 100644
index 0000000000..ef0f93add8
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Black.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Bold.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Bold.ttf
new file mode 100644
index 0000000000..bb7091a355
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Bold.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-ExtraBold.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-ExtraBold.ttf
new file mode 100644
index 0000000000..a737a65a72
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-ExtraBold.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-ExtraLight.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-ExtraLight.ttf
new file mode 100644
index 0000000000..2a95000602
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-ExtraLight.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Light.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Light.ttf
new file mode 100644
index 0000000000..07906bdabe
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Light.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Medium.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Medium.ttf
new file mode 100644
index 0000000000..89d75e39f8
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Medium.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Regular.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Regular.ttf
new file mode 100644
index 0000000000..0c654e79b1
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Regular.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-SemiBold.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-SemiBold.ttf
new file mode 100644
index 0000000000..e93689fefd
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-SemiBold.ttf differ
diff --git a/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Thin.ttf b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Thin.ttf
new file mode 100644
index 0000000000..b4f1804a74
Binary files /dev/null and b/openpype/style/fonts/Noto_Sans_Mono/static/NotoSansMono_SemiCondensed/NotoSansMono_SemiCondensed-Thin.ttf differ
diff --git a/openpype/style/fonts/RobotoMono/LICENSE.txt b/openpype/style/fonts/RobotoMono/LICENSE.txt
deleted file mode 100644
index d645695673..0000000000
--- a/openpype/style/fonts/RobotoMono/LICENSE.txt
+++ /dev/null
@@ -1,202 +0,0 @@
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
diff --git a/openpype/style/fonts/RobotoMono/README.txt b/openpype/style/fonts/RobotoMono/README.txt
deleted file mode 100644
index 1bc1b1cfa2..0000000000
--- a/openpype/style/fonts/RobotoMono/README.txt
+++ /dev/null
@@ -1,77 +0,0 @@
-Roboto Mono Variable Font
-=========================
-
-This download contains Roboto Mono as both variable fonts and static fonts.
-
-Roboto Mono is a variable font with this axis:
- wght
-
-This means all the styles are contained in these files:
- RobotoMono-VariableFont_wght.ttf
- RobotoMono-Italic-VariableFont_wght.ttf
-
-If your app fully supports variable fonts, you can now pick intermediate styles
-that aren’t available as static fonts. Not all apps support variable fonts, and
-in those cases you can use the static font files for Roboto Mono:
- static/RobotoMono-Thin.ttf
- static/RobotoMono-ExtraLight.ttf
- static/RobotoMono-Light.ttf
- static/RobotoMono-Regular.ttf
- static/RobotoMono-Medium.ttf
- static/RobotoMono-SemiBold.ttf
- static/RobotoMono-Bold.ttf
- static/RobotoMono-ThinItalic.ttf
- static/RobotoMono-ExtraLightItalic.ttf
- static/RobotoMono-LightItalic.ttf
- static/RobotoMono-Italic.ttf
- static/RobotoMono-MediumItalic.ttf
- static/RobotoMono-SemiBoldItalic.ttf
- static/RobotoMono-BoldItalic.ttf
-
-Get started
------------
-
-1. Install the font files you want to use
-
-2. Use your app's font picker to view the font family and all the
-available styles
-
-Learn more about variable fonts
--------------------------------
-
- https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
- https://variablefonts.typenetwork.com
- https://medium.com/variable-fonts
-
-In desktop apps
-
- https://theblog.adobe.com/can-variable-fonts-illustrator-cc
- https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
-
-Online
-
- https://developers.google.com/fonts/docs/getting_started
- https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
- https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
-
-Installing fonts
-
- MacOS: https://support.apple.com/en-us/HT201749
- Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
- Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
-
-Android Apps
-
- https://developers.google.com/fonts/docs/android
- https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
-
-License
--------
-Please read the full license text (LICENSE.txt) to understand the permissions,
-restrictions and requirements for usage, redistribution, and modification.
-
-You can use them freely in your products & projects - print or digital,
-commercial or otherwise.
-
-This isn't legal advice, please consider consulting a lawyer and see the full
-license for all details.
diff --git a/openpype/style/fonts/RobotoMono/RobotoMono-Italic-VariableFont_wght.ttf b/openpype/style/fonts/RobotoMono/RobotoMono-Italic-VariableFont_wght.ttf
deleted file mode 100644
index d30055a9e8..0000000000
Binary files a/openpype/style/fonts/RobotoMono/RobotoMono-Italic-VariableFont_wght.ttf and /dev/null differ
diff --git a/openpype/style/fonts/RobotoMono/RobotoMono-VariableFont_wght.ttf b/openpype/style/fonts/RobotoMono/RobotoMono-VariableFont_wght.ttf
deleted file mode 100644
index d2b4746196..0000000000
Binary files a/openpype/style/fonts/RobotoMono/RobotoMono-VariableFont_wght.ttf and /dev/null differ
diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-Bold.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-Bold.ttf
deleted file mode 100644
index 900fce6848..0000000000
Binary files a/openpype/style/fonts/RobotoMono/static/RobotoMono-Bold.ttf and /dev/null differ
diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-BoldItalic.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-BoldItalic.ttf
deleted file mode 100644
index 4bfe29ae89..0000000000
Binary files a/openpype/style/fonts/RobotoMono/static/RobotoMono-BoldItalic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-ExtraLight.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-ExtraLight.ttf
deleted file mode 100644
index d535884553..0000000000
Binary files a/openpype/style/fonts/RobotoMono/static/RobotoMono-ExtraLight.ttf and /dev/null differ
diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-ExtraLightItalic.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-ExtraLightItalic.ttf
deleted file mode 100644
index b28960a0ee..0000000000
Binary files a/openpype/style/fonts/RobotoMono/static/RobotoMono-ExtraLightItalic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-Italic.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-Italic.ttf
deleted file mode 100644
index 4ee4dc49b4..0000000000
Binary files a/openpype/style/fonts/RobotoMono/static/RobotoMono-Italic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-Light.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-Light.ttf
deleted file mode 100644
index 276af4c55a..0000000000
Binary files a/openpype/style/fonts/RobotoMono/static/RobotoMono-Light.ttf and /dev/null differ
diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-LightItalic.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-LightItalic.ttf
deleted file mode 100644
index a2801c2168..0000000000
Binary files a/openpype/style/fonts/RobotoMono/static/RobotoMono-LightItalic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-Medium.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-Medium.ttf
deleted file mode 100644
index 8461be77a3..0000000000
Binary files a/openpype/style/fonts/RobotoMono/static/RobotoMono-Medium.ttf and /dev/null differ
diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-MediumItalic.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-MediumItalic.ttf
deleted file mode 100644
index a3bfaa115a..0000000000
Binary files a/openpype/style/fonts/RobotoMono/static/RobotoMono-MediumItalic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-Regular.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-Regular.ttf
deleted file mode 100644
index 7c4ce36a44..0000000000
Binary files a/openpype/style/fonts/RobotoMono/static/RobotoMono-Regular.ttf and /dev/null differ
diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-SemiBold.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-SemiBold.ttf
deleted file mode 100644
index 15ee6c6e40..0000000000
Binary files a/openpype/style/fonts/RobotoMono/static/RobotoMono-SemiBold.ttf and /dev/null differ
diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-SemiBoldItalic.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-SemiBoldItalic.ttf
deleted file mode 100644
index 8e21497793..0000000000
Binary files a/openpype/style/fonts/RobotoMono/static/RobotoMono-SemiBoldItalic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-Thin.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-Thin.ttf
deleted file mode 100644
index ee8a3fd41a..0000000000
Binary files a/openpype/style/fonts/RobotoMono/static/RobotoMono-Thin.ttf and /dev/null differ
diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-ThinItalic.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-ThinItalic.ttf
deleted file mode 100644
index 40b01e40de..0000000000
Binary files a/openpype/style/fonts/RobotoMono/static/RobotoMono-ThinItalic.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Spartan/README.txt b/openpype/style/fonts/Spartan/README.txt
deleted file mode 100644
index 9db64aff0b..0000000000
--- a/openpype/style/fonts/Spartan/README.txt
+++ /dev/null
@@ -1,71 +0,0 @@
-Spartan Variable Font
-=====================
-
-This download contains Spartan as both a variable font and static fonts.
-
-Spartan is a variable font with this axis:
- wght
-
-This means all the styles are contained in a single file:
- Spartan-VariableFont_wght.ttf
-
-If your app fully supports variable fonts, you can now pick intermediate styles
-that aren’t available as static fonts. Not all apps support variable fonts, and
-in those cases you can use the static font files for Spartan:
- static/Spartan-Thin.ttf
- static/Spartan-ExtraLight.ttf
- static/Spartan-Light.ttf
- static/Spartan-Regular.ttf
- static/Spartan-Medium.ttf
- static/Spartan-SemiBold.ttf
- static/Spartan-Bold.ttf
- static/Spartan-ExtraBold.ttf
- static/Spartan-Black.ttf
-
-Get started
------------
-
-1. Install the font files you want to use
-
-2. Use your app's font picker to view the font family and all the
-available styles
-
-Learn more about variable fonts
--------------------------------
-
- https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
- https://variablefonts.typenetwork.com
- https://medium.com/variable-fonts
-
-In desktop apps
-
- https://theblog.adobe.com/can-variable-fonts-illustrator-cc
- https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
-
-Online
-
- https://developers.google.com/fonts/docs/getting_started
- https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
- https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
-
-Installing fonts
-
- MacOS: https://support.apple.com/en-us/HT201749
- Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
- Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
-
-Android Apps
-
- https://developers.google.com/fonts/docs/android
- https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
-
-License
--------
-Please read the full license text (OFL.txt) to understand the permissions,
-restrictions and requirements for usage, redistribution, and modification.
-
-You can use them freely in your products & projects - print or digital,
-commercial or otherwise. However, you can't sell the fonts on their own.
-
-This isn't legal advice, please consider consulting a lawyer and see the full
-license for all details.
diff --git a/openpype/style/fonts/Spartan/Spartan-Black.ttf b/openpype/style/fonts/Spartan/Spartan-Black.ttf
deleted file mode 100644
index 5d3147011e..0000000000
Binary files a/openpype/style/fonts/Spartan/Spartan-Black.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Spartan/Spartan-Bold.ttf b/openpype/style/fonts/Spartan/Spartan-Bold.ttf
deleted file mode 100644
index 5fe4b702b2..0000000000
Binary files a/openpype/style/fonts/Spartan/Spartan-Bold.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Spartan/Spartan-ExtraBold.ttf b/openpype/style/fonts/Spartan/Spartan-ExtraBold.ttf
deleted file mode 100644
index 1030b6dec0..0000000000
Binary files a/openpype/style/fonts/Spartan/Spartan-ExtraBold.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Spartan/Spartan-ExtraLight.ttf b/openpype/style/fonts/Spartan/Spartan-ExtraLight.ttf
deleted file mode 100644
index aced3a9e94..0000000000
Binary files a/openpype/style/fonts/Spartan/Spartan-ExtraLight.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Spartan/Spartan-Light.ttf b/openpype/style/fonts/Spartan/Spartan-Light.ttf
deleted file mode 100644
index 3bb6efa40e..0000000000
Binary files a/openpype/style/fonts/Spartan/Spartan-Light.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Spartan/Spartan-Medium.ttf b/openpype/style/fonts/Spartan/Spartan-Medium.ttf
deleted file mode 100644
index 94b22ecc08..0000000000
Binary files a/openpype/style/fonts/Spartan/Spartan-Medium.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Spartan/Spartan-Regular.ttf b/openpype/style/fonts/Spartan/Spartan-Regular.ttf
deleted file mode 100644
index 7560322e3f..0000000000
Binary files a/openpype/style/fonts/Spartan/Spartan-Regular.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Spartan/Spartan-SemiBold.ttf b/openpype/style/fonts/Spartan/Spartan-SemiBold.ttf
deleted file mode 100644
index 7a5f74adb3..0000000000
Binary files a/openpype/style/fonts/Spartan/Spartan-SemiBold.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Spartan/Spartan-Thin.ttf b/openpype/style/fonts/Spartan/Spartan-Thin.ttf
deleted file mode 100644
index 4caa0b2be9..0000000000
Binary files a/openpype/style/fonts/Spartan/Spartan-Thin.ttf and /dev/null differ
diff --git a/openpype/style/fonts/Spartan/Spartan-VariableFont_wght.ttf b/openpype/style/fonts/Spartan/Spartan-VariableFont_wght.ttf
deleted file mode 100644
index b2dd7c3076..0000000000
Binary files a/openpype/style/fonts/Spartan/Spartan-VariableFont_wght.ttf and /dev/null differ
diff --git a/openpype/style/style.css b/openpype/style/style.css
index 2a2f4e572e..fa5b41cd07 100644
--- a/openpype/style/style.css
+++ b/openpype/style/style.css
@@ -19,8 +19,8 @@ Enabled vs Disabled logic in most of stylesheets
*/
* {
- font-size: 9pt;
- font-family: "Spartan";
+ font-size: 10pt;
+ font-family: "Noto Sans";
font-weight: 450;
outline: none;
}
@@ -713,20 +713,19 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
}
#CompleterView::item {
- padding: 2px 4px 2px 4px;
- border-left: 3px solid {color:bg-view};
+ background: {color:bg-view-hover};
+ color: {color:font};
+ padding-left: 0px;
}
#CompleterView::item:hover {
- border-left-color: {palette:blue-base};
- background: {color:bg-view-selection};
- color: {color:font};
+ background: {color:bg-view-hover};
}
/* Launcher specific stylesheets */
#IconView[mode="icon"] {
/* font size can't be set on items */
- font-size: 8pt;
+ font-size: 9pt;
border: 0px;
padding: 0px;
margin: 0px;
@@ -756,15 +755,45 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
border: 1px solid {color:border};
border-radius: 0.1em;
}
+
/* Subset Manager */
#SubsetManagerDetailsText {}
#SubsetManagerDetailsText[state="invalid"] {
border: 1px solid #ff0000;
}
+/* Creator */
+#CreatorsView::item {
+ padding: 1px 5px;
+}
+
+#CreatorFamilyLabel {
+ font-size: 10pt;
+ font-weight: bold;
+}
+
+/* Scene Inventory */
+#ButtonWithMenu {
+ padding-right: 16px;
+ border: 1px solid #4A4949;
+ border-radius: 2px;
+}
+#ButtonWithMenu::menu-button {
+ border: 1px solid #4A4949;
+ width: 12px;
+ border-top-left-radius: 0px;
+ border-top-right-radius: 2px;
+ border-bottom-right-radius: 2px;
+ border-bottom-left-radius: 0px;
+}
+
+#ButtonWithMenu[state="1"], #ButtonWithMenu[state="1"]::menu-button, #ButtonWithMenu[state="1"]::menu-button:hover {
+ border-color: green;
+}
+
/* Python console interpreter */
#PythonInterpreterOutput, #PythonCodeEditor {
- font-family: "Roboto Mono";
+ font-family: "Noto Sans Mono";
border-radius: 0px;
}
@@ -783,7 +812,7 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
/* New Create/Publish UI */
#PublishLogConsole {
- font-family: "Roboto Mono";
+ font-family: "Noto Sans Mono";
}
#VariantInput[state="new"], #VariantInput[state="new"]:focus, #VariantInput[state="new"]:hover {
diff --git a/openpype/tools/context_dialog/window.py b/openpype/tools/context_dialog/window.py
index 7f3ac75445..5d8a2ad62e 100644
--- a/openpype/tools/context_dialog/window.py
+++ b/openpype/tools/context_dialog/window.py
@@ -6,7 +6,7 @@ from avalon.api import AvalonMongoDB
from openpype import style
from openpype.tools.utils.lib import center_window
-from openpype.tools.utils.widgets import AssetWidget
+from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget
from openpype.tools.utils.constants import (
PROJECT_NAME_ROLE
)
@@ -65,8 +65,8 @@ class ContextDialog(QtWidgets.QDialog):
project_combobox.setModel(project_proxy)
# Assets widget
- assets_widget = AssetWidget(
- dbcon, multiselection=False, parent=left_side_widget
+ assets_widget = SingleSelectAssetsWidget(
+ dbcon, parent=left_side_widget
)
left_side_layout = QtWidgets.QVBoxLayout(left_side_widget)
@@ -309,11 +309,8 @@ class ContextDialog(QtWidgets.QDialog):
def _set_asset_to_tasks_widget(self):
# filter None docs they are silo
- asset_docs = self._assets_widget.get_selected_assets()
- asset_ids = [asset_doc["_id"] for asset_doc in asset_docs]
- asset_id = None
- if asset_ids:
- asset_id = asset_ids[0]
+ asset_id = self._assets_widget.get_selected_asset_id()
+
self._tasks_widget.set_asset_id(asset_id)
def _confirm_values(self):
@@ -334,11 +331,7 @@ class ContextDialog(QtWidgets.QDialog):
def get_selected_asset(self):
"""Currently selected asset in asset widget."""
- asset_name = None
- for asset_doc in self._assets_widget.get_selected_assets():
- asset_name = asset_doc["name"]
- break
- return asset_name
+ return self._assets_widget.get_selected_asset_name()
def get_selected_task(self):
"""Currently selected task."""
diff --git a/openpype/tools/creator/__init__.py b/openpype/tools/creator/__init__.py
new file mode 100644
index 0000000000..585b8bdf80
--- /dev/null
+++ b/openpype/tools/creator/__init__.py
@@ -0,0 +1,9 @@
+from .window import (
+ show,
+ CreatorWindow
+)
+
+__all__ = (
+ "show",
+ "CreatorWindow"
+)
diff --git a/openpype/tools/creator/constants.py b/openpype/tools/creator/constants.py
new file mode 100644
index 0000000000..26a25dc010
--- /dev/null
+++ b/openpype/tools/creator/constants.py
@@ -0,0 +1,8 @@
+from Qt import QtCore
+
+
+FAMILY_ROLE = QtCore.Qt.UserRole + 1
+ITEM_ID_ROLE = QtCore.Qt.UserRole + 2
+
+SEPARATOR = "---"
+SEPARATORS = {"---", "---separator---"}
diff --git a/openpype/tools/creator/model.py b/openpype/tools/creator/model.py
new file mode 100644
index 0000000000..6907e8f0aa
--- /dev/null
+++ b/openpype/tools/creator/model.py
@@ -0,0 +1,55 @@
+import uuid
+from Qt import QtGui, QtCore
+
+from avalon import api
+
+from . constants import (
+ FAMILY_ROLE,
+ ITEM_ID_ROLE
+)
+
+
+class CreatorsModel(QtGui.QStandardItemModel):
+ def __init__(self, *args, **kwargs):
+ super(CreatorsModel, self).__init__(*args, **kwargs)
+
+ self._creators_by_id = {}
+
+ def reset(self):
+ # TODO change to refresh when clearing is not needed
+ self.clear()
+ self._creators_by_id = {}
+
+ items = []
+ creators = api.discover(api.Creator)
+ for creator in creators:
+ item_id = str(uuid.uuid4())
+ self._creators_by_id[item_id] = creator
+
+ label = creator.label or creator.family
+ item = QtGui.QStandardItem(label)
+ item.setEditable(False)
+ item.setData(item_id, ITEM_ID_ROLE)
+ item.setData(creator.family, FAMILY_ROLE)
+ items.append(item)
+
+ if not items:
+ item = QtGui.QStandardItem("No registered families")
+ item.setEnabled(False)
+ item.setData(QtCore.Qt.ItemIsEnabled, False)
+ items.append(item)
+
+ self.invisibleRootItem().appendRows(items)
+
+ def get_creator_by_id(self, item_id):
+ return self._creators_by_id.get(item_id)
+
+ def get_indexes_by_family(self, family):
+ indexes = []
+ for row in range(self.rowCount()):
+ index = self.index(row, 0)
+ item_id = index.data(ITEM_ID_ROLE)
+ creator_plugin = self._creators_by_id.get(item_id)
+ if creator_plugin and creator_plugin.family == family:
+ indexes.append(index)
+ return indexes
diff --git a/openpype/tools/creator/widgets.py b/openpype/tools/creator/widgets.py
new file mode 100644
index 0000000000..89c90cc048
--- /dev/null
+++ b/openpype/tools/creator/widgets.py
@@ -0,0 +1,266 @@
+import re
+import inspect
+
+from Qt import QtWidgets, QtCore, QtGui
+
+from avalon.vendor import qtawesome
+
+from openpype import style
+from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
+
+
+class CreateErrorMessageBox(QtWidgets.QDialog):
+ def __init__(
+ self,
+ family,
+ subset_name,
+ asset_name,
+ exc_msg,
+ formatted_traceback,
+ parent=None
+ ):
+ super(CreateErrorMessageBox, self).__init__(parent)
+ self.setWindowTitle("Creation failed")
+ self.setFocusPolicy(QtCore.Qt.StrongFocus)
+ self.setWindowFlags(
+ self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
+ )
+
+ body_layout = QtWidgets.QVBoxLayout(self)
+
+ main_label = (
+ "Failed to create"
+ )
+ main_label_widget = QtWidgets.QLabel(main_label, self)
+ body_layout.addWidget(main_label_widget)
+
+ item_name_template = (
+ "Family: {}
"
+ "Subset: {}
"
+ "Asset: {}
"
+ )
+ exc_msg_template = "{}"
+
+ line = self._create_line()
+ body_layout.addWidget(line)
+
+ item_name = item_name_template.format(family, subset_name, asset_name)
+ item_name_widget = QtWidgets.QLabel(
+ item_name.replace("\n", "
"), self
+ )
+ body_layout.addWidget(item_name_widget)
+
+ exc_msg = exc_msg_template.format(exc_msg.replace("\n", "
"))
+ message_label_widget = QtWidgets.QLabel(exc_msg, self)
+ body_layout.addWidget(message_label_widget)
+
+ if formatted_traceback:
+ tb_widget = QtWidgets.QLabel(
+ formatted_traceback.replace("\n", "
"), self
+ )
+ tb_widget.setTextInteractionFlags(
+ QtCore.Qt.TextBrowserInteraction
+ )
+ body_layout.addWidget(tb_widget)
+
+ footer_widget = QtWidgets.QWidget(self)
+ footer_layout = QtWidgets.QHBoxLayout(footer_widget)
+ button_box = QtWidgets.QDialogButtonBox(QtCore.Qt.Vertical)
+ button_box.setStandardButtons(
+ QtWidgets.QDialogButtonBox.StandardButton.Ok
+ )
+ button_box.accepted.connect(self._on_accept)
+ footer_layout.addWidget(button_box, alignment=QtCore.Qt.AlignRight)
+ body_layout.addWidget(footer_widget)
+
+ def showEvent(self, event):
+ self.setStyleSheet(style.load_stylesheet())
+ super(CreateErrorMessageBox, self).showEvent(event)
+
+ def _on_accept(self):
+ self.close()
+
+ def _create_line(self):
+ line = QtWidgets.QFrame(self)
+ line.setFixedHeight(2)
+ line.setFrameShape(QtWidgets.QFrame.HLine)
+ line.setFrameShadow(QtWidgets.QFrame.Sunken)
+ return line
+
+
+class SubsetNameValidator(QtGui.QRegExpValidator):
+ invalid = QtCore.Signal(set)
+ pattern = "^[{}]*$".format(SUBSET_NAME_ALLOWED_SYMBOLS)
+
+ def __init__(self):
+ reg = QtCore.QRegExp(self.pattern)
+ super(SubsetNameValidator, self).__init__(reg)
+
+ def validate(self, text, pos):
+ results = super(SubsetNameValidator, self).validate(text, pos)
+ if results[0] == self.Invalid:
+ self.invalid.emit(self.invalid_chars(text))
+ return results
+
+ def invalid_chars(self, text):
+ invalid = set()
+ re_valid = re.compile(self.pattern)
+ for char in text:
+ if char == " ":
+ invalid.add("' '")
+ continue
+ if not re_valid.match(char):
+ invalid.add(char)
+ return invalid
+
+
+class VariantLineEdit(QtWidgets.QLineEdit):
+ report = QtCore.Signal(str)
+ colors = {
+ "empty": (QtGui.QColor("#78879b"), ""),
+ "exists": (QtGui.QColor("#4E76BB"), "border-color: #4E76BB;"),
+ "new": (QtGui.QColor("#7AAB8F"), "border-color: #7AAB8F;"),
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(VariantLineEdit, self).__init__(*args, **kwargs)
+
+ validator = SubsetNameValidator()
+ self.setValidator(validator)
+ self.setToolTip("Only alphanumeric characters (A-Z a-z 0-9), "
+ "'_' and '.' are allowed.")
+
+ self._status_color = self.colors["empty"][0]
+
+ anim = QtCore.QPropertyAnimation()
+ anim.setTargetObject(self)
+ anim.setPropertyName(b"status_color")
+ anim.setEasingCurve(QtCore.QEasingCurve.InCubic)
+ anim.setDuration(300)
+ anim.setStartValue(QtGui.QColor("#C84747")) # `Invalid` status color
+ self.animation = anim
+
+ validator.invalid.connect(self.on_invalid)
+
+ def on_invalid(self, invalid):
+ message = "Invalid character: %s" % ", ".join(invalid)
+ self.report.emit(message)
+ self.animation.stop()
+ self.animation.start()
+
+ def as_empty(self):
+ self._set_border("empty")
+ self.report.emit("Empty subset name ..")
+
+ def as_exists(self):
+ self._set_border("exists")
+ self.report.emit("Existing subset, appending next version.")
+
+ def as_new(self):
+ self._set_border("new")
+ self.report.emit("New subset, creating first version.")
+
+ def _set_border(self, status):
+ qcolor, style = self.colors[status]
+ self.animation.setEndValue(qcolor)
+ self.setStyleSheet(style)
+
+ def _get_status_color(self):
+ return self._status_color
+
+ def _set_status_color(self, color):
+ self._status_color = color
+ self.setStyleSheet("border-color: %s;" % color.name())
+
+ status_color = QtCore.Property(
+ QtGui.QColor, _get_status_color, _set_status_color
+ )
+
+
+class FamilyDescriptionWidget(QtWidgets.QWidget):
+ """A family description widget.
+
+ Shows a family icon, family name and a help description.
+ Used in creator header.
+
+ _________________
+ | ____ |
+ | |icon| FAMILY |
+ | |____| help |
+ |_________________|
+
+ """
+
+ SIZE = 35
+
+ def __init__(self, parent=None):
+ super(FamilyDescriptionWidget, self).__init__(parent=parent)
+
+ icon_label = QtWidgets.QLabel(self)
+ icon_label.setSizePolicy(
+ QtWidgets.QSizePolicy.Maximum,
+ QtWidgets.QSizePolicy.Maximum
+ )
+
+ # Add 4 pixel padding to avoid icon being cut off
+ icon_label.setFixedWidth(self.SIZE + 4)
+ icon_label.setFixedHeight(self.SIZE + 4)
+
+ label_layout = QtWidgets.QVBoxLayout()
+ label_layout.setSpacing(0)
+
+ family_label = QtWidgets.QLabel(self)
+ family_label.setObjectName("CreatorFamilyLabel")
+ family_label.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft)
+
+ help_label = QtWidgets.QLabel(self)
+ help_label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)
+
+ label_layout.addWidget(family_label)
+ label_layout.addWidget(help_label)
+
+ layout = QtWidgets.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(5)
+ layout.addWidget(icon_label)
+ layout.addLayout(label_layout)
+
+ self._help_label = help_label
+ self._family_label = family_label
+ self._icon_label = icon_label
+
+ def set_item(self, creator_plugin):
+ """Update elements to display information of a family item.
+
+ Args:
+ item (dict): A family item as registered with name, help and icon
+
+ Returns:
+ None
+
+ """
+ if not creator_plugin:
+ self._icon_label.setPixmap(None)
+ self._family_label.setText("")
+ self._help_label.setText("")
+ return
+
+ # Support a font-awesome icon
+ icon_name = getattr(creator_plugin, "icon", None) or "info-circle"
+ try:
+ icon = qtawesome.icon("fa.{}".format(icon_name), color="white")
+ pixmap = icon.pixmap(self.SIZE, self.SIZE)
+ except Exception:
+ print("BUG: Couldn't load icon \"fa.{}\"".format(str(icon_name)))
+ # Create transparent pixmap
+ pixmap = QtGui.QPixmap()
+ pixmap.fill(QtCore.Qt.transparent)
+ pixmap = pixmap.scaled(self.SIZE, self.SIZE)
+
+ # Parse a clean line from the Creator's docstring
+ docstring = inspect.getdoc(creator_plugin)
+ creator_help = docstring.splitlines()[0] if docstring else ""
+
+ self._icon_label.setPixmap(pixmap)
+ self._family_label.setText(creator_plugin.family)
+ self._help_label.setText(creator_help)
diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py
new file mode 100644
index 0000000000..dca1735121
--- /dev/null
+++ b/openpype/tools/creator/window.py
@@ -0,0 +1,509 @@
+import sys
+import traceback
+import re
+
+from Qt import QtWidgets, QtCore
+
+from avalon import api, io
+
+from openpype import style
+from openpype.api import get_current_project_settings
+from openpype.tools.utils.lib import qt_app_context
+from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
+
+from .model import CreatorsModel
+from .widgets import (
+ CreateErrorMessageBox,
+ VariantLineEdit,
+ FamilyDescriptionWidget
+)
+from .constants import (
+ ITEM_ID_ROLE,
+ SEPARATOR,
+ SEPARATORS
+)
+
+module = sys.modules[__name__]
+module.window = None
+
+
+class CreatorWindow(QtWidgets.QDialog):
+ def __init__(self, parent=None):
+ super(CreatorWindow, self).__init__(parent)
+ self.setWindowTitle("Instance Creator")
+ self.setFocusPolicy(QtCore.Qt.StrongFocus)
+ if not parent:
+ self.setWindowFlags(
+ self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
+ )
+
+ creator_info = FamilyDescriptionWidget(self)
+
+ creators_model = CreatorsModel()
+
+ creators_proxy = QtCore.QSortFilterProxyModel()
+ creators_proxy.setSourceModel(creators_model)
+
+ creators_view = QtWidgets.QListView(self)
+ creators_view.setObjectName("CreatorsView")
+ creators_view.setModel(creators_proxy)
+
+ asset_name_input = QtWidgets.QLineEdit(self)
+ variant_input = VariantLineEdit(self)
+ subset_name_input = QtWidgets.QLineEdit(self)
+ subset_name_input.setEnabled(False)
+
+ subset_button = QtWidgets.QPushButton()
+ subset_button.setFixedWidth(18)
+ subset_menu = QtWidgets.QMenu(subset_button)
+ subset_button.setMenu(subset_menu)
+
+ name_layout = QtWidgets.QHBoxLayout()
+ name_layout.addWidget(variant_input)
+ name_layout.addWidget(subset_button)
+ name_layout.setSpacing(3)
+ name_layout.setContentsMargins(0, 0, 0, 0)
+
+ body_layout = QtWidgets.QVBoxLayout()
+ body_layout.setContentsMargins(0, 0, 0, 0)
+
+ body_layout.addWidget(creator_info, 0)
+ body_layout.addWidget(QtWidgets.QLabel("Family", self), 0)
+ body_layout.addWidget(creators_view, 1)
+ body_layout.addWidget(QtWidgets.QLabel("Asset", self), 0)
+ body_layout.addWidget(asset_name_input, 0)
+ body_layout.addWidget(QtWidgets.QLabel("Subset", self), 0)
+ body_layout.addLayout(name_layout, 0)
+ body_layout.addWidget(subset_name_input, 0)
+
+ useselection_chk = QtWidgets.QCheckBox("Use selection", self)
+ useselection_chk.setCheckState(QtCore.Qt.Checked)
+
+ create_btn = QtWidgets.QPushButton("Create", self)
+ # Need to store error_msg to prevent garbage collection
+ msg_label = QtWidgets.QLabel(self)
+
+ footer_layout = QtWidgets.QVBoxLayout()
+ footer_layout.addWidget(create_btn, 0)
+ footer_layout.addWidget(msg_label, 0)
+ footer_layout.setContentsMargins(0, 0, 0, 0)
+
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.addLayout(body_layout, 1)
+ layout.addWidget(useselection_chk, 0, QtCore.Qt.AlignLeft)
+ layout.addLayout(footer_layout, 0)
+
+ msg_timer = QtCore.QTimer()
+ msg_timer.setSingleShot(True)
+ msg_timer.setInterval(5000)
+
+ validation_timer = QtCore.QTimer()
+ validation_timer.setSingleShot(True)
+ validation_timer.setInterval(300)
+
+ msg_timer.timeout.connect(self._on_msg_timer)
+ validation_timer.timeout.connect(self._on_validation_timer)
+
+ create_btn.clicked.connect(self._on_create)
+ variant_input.returnPressed.connect(self._on_create)
+ variant_input.textChanged.connect(self._on_data_changed)
+ variant_input.report.connect(self.echo)
+ asset_name_input.textChanged.connect(self._on_data_changed)
+ creators_view.selectionModel().currentChanged.connect(
+ self._on_selection_changed
+ )
+
+ # Store valid states and
+ self._is_valid = False
+ create_btn.setEnabled(self._is_valid)
+
+ self._first_show = True
+
+ # Message dialog when something goes wrong during creation
+ self._message_dialog = None
+
+ self._creator_info = creator_info
+ self._create_btn = create_btn
+ self._useselection_chk = useselection_chk
+ self._variant_input = variant_input
+ self._subset_name_input = subset_name_input
+ self._asset_name_input = asset_name_input
+
+ self._creators_model = creators_model
+ self._creators_proxy = creators_proxy
+ self._creators_view = creators_view
+
+ self._subset_btn = subset_button
+ self._subset_menu = subset_menu
+
+ self._msg_label = msg_label
+
+ self._validation_timer = validation_timer
+ self._msg_timer = msg_timer
+
+ # Defaults
+ self.resize(300, 500)
+ variant_input.setFocus()
+
+ def _set_valid_state(self, valid):
+ if self._is_valid == valid:
+ return
+ self._is_valid = valid
+ self._create_btn.setEnabled(valid)
+
+ def _build_menu(self, default_names=None):
+ """Create optional predefined subset names
+
+ Args:
+ default_names(list): all predefined names
+
+ Returns:
+ None
+ """
+ if not default_names:
+ default_names = []
+
+ menu = self._subset_menu
+ button = self._subset_btn
+
+ # Get and destroy the action group
+ group = button.findChild(QtWidgets.QActionGroup)
+ if group:
+ group.deleteLater()
+
+ state = any(default_names)
+ button.setEnabled(state)
+ if state is False:
+ return
+
+ # Build new action group
+ group = QtWidgets.QActionGroup(button)
+ for name in default_names:
+ if name in SEPARATORS:
+ menu.addSeparator()
+ continue
+ action = group.addAction(name)
+ menu.addAction(action)
+
+ group.triggered.connect(self._on_action_clicked)
+
+ def _on_action_clicked(self, action):
+ self._variant_input.setText(action.text())
+
+ def _on_data_changed(self, *args):
+ # Set invalid state until it's reconfirmed to be valid by the
+ # scheduled callback so any form of creation is held back until
+ # valid again
+ self._set_valid_state(False)
+
+ self._validation_timer.start()
+
+ def _on_validation_timer(self):
+ index = self._creators_view.currentIndex()
+ item_id = index.data(ITEM_ID_ROLE)
+ creator_plugin = self._creators_model.get_creator_by_id(item_id)
+ user_input_text = self._variant_input.text()
+ asset_name = self._asset_name_input.text()
+
+ # Early exit if no asset name
+ if not asset_name.strip():
+ self._build_menu()
+ self.echo("Asset name is required ..")
+ self._set_valid_state(False)
+ return
+
+ asset_doc = None
+ if creator_plugin:
+ # Get the asset from the database which match with the name
+ asset_doc = io.find_one(
+ {"name": asset_name, "type": "asset"},
+ projection={"_id": 1}
+ )
+
+ # Get plugin
+ if not asset_doc or not creator_plugin:
+ subset_name = user_input_text
+ self._build_menu()
+
+ if not creator_plugin:
+ self.echo("No registered families ..")
+ else:
+ self.echo("Asset '%s' not found .." % asset_name)
+ self._set_valid_state(False)
+ return
+
+ project_name = io.Session["AVALON_PROJECT"]
+ asset_id = asset_doc["_id"]
+ task_name = io.Session["AVALON_TASK"]
+
+ # Calculate subset name with Creator plugin
+ subset_name = creator_plugin.get_subset_name(
+ user_input_text, task_name, asset_id, project_name
+ )
+ # Force replacement of prohibited symbols
+ # QUESTION should Creator care about this and here should be only
+ # validated with schema regex?
+
+ # Allow curly brackets in subset name for dynamic keys
+ curly_left = "__cbl__"
+ curly_right = "__cbr__"
+ tmp_subset_name = (
+ subset_name
+ .replace("{", curly_left)
+ .replace("}", curly_right)
+ )
+ # Replace prohibited symbols
+ tmp_subset_name = re.sub(
+ "[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS),
+ "",
+ tmp_subset_name
+ )
+ subset_name = (
+ tmp_subset_name
+ .replace(curly_left, "{")
+ .replace(curly_right, "}")
+ )
+ self._subset_name_input.setText(subset_name)
+
+ # Get all subsets of the current asset
+ subset_docs = io.find(
+ {
+ "type": "subset",
+ "parent": asset_id
+ },
+ {"name": 1}
+ )
+ existing_subset_names = set(subset_docs.distinct("name"))
+ existing_subset_names_low = set(
+ _name.lower()
+ for _name in existing_subset_names
+ )
+
+ # Defaults to dropdown
+ defaults = []
+ # Check if Creator plugin has set defaults
+ if (
+ creator_plugin.defaults
+ and isinstance(creator_plugin.defaults, (list, tuple, set))
+ ):
+ defaults = list(creator_plugin.defaults)
+
+ # Replace
+ compare_regex = re.compile(re.sub(
+ user_input_text, "(.+)", subset_name, flags=re.IGNORECASE
+ ))
+ subset_hints = set()
+ if user_input_text:
+ for _name in existing_subset_names:
+ _result = compare_regex.search(_name)
+ if _result:
+ subset_hints |= set(_result.groups())
+
+ if subset_hints:
+ if defaults:
+ defaults.append(SEPARATOR)
+ defaults.extend(subset_hints)
+ self._build_menu(defaults)
+
+ # Indicate subset existence
+ if not user_input_text:
+ self._variant_input.as_empty()
+ elif subset_name.lower() in existing_subset_names_low:
+ # validate existence of subset name with lowered text
+ # - "renderMain" vs. "rensermain" mean same path item for
+ # windows
+ self._variant_input.as_exists()
+ else:
+ self._variant_input.as_new()
+
+ # Update the valid state
+ valid = subset_name.strip() != ""
+
+ self._set_valid_state(valid)
+
+ def _on_selection_changed(self, old_idx, new_idx):
+ index = self._creators_view.currentIndex()
+ item_id = index.data(ITEM_ID_ROLE)
+
+ creator_plugin = self._creators_model.get_creator_by_id(item_id)
+
+ self._creator_info.set_item(creator_plugin)
+
+ if creator_plugin is None:
+ return
+
+ default = None
+ if hasattr(creator_plugin, "get_default_variant"):
+ default = creator_plugin.get_default_variant()
+
+ if not default:
+ if (
+ creator_plugin.defaults
+ and isinstance(creator_plugin.defaults, list)
+ ):
+ default = creator_plugin.defaults[0]
+ else:
+ default = "Default"
+
+ self._variant_input.setText(default)
+
+ self._on_data_changed()
+
+ def keyPressEvent(self, event):
+ """Custom keyPressEvent.
+
+ Override keyPressEvent to do nothing so that Maya's panels won't
+ take focus when pressing "SHIFT" whilst mouse is over viewport or
+ outliner. This way users don't accidently perform Maya commands
+ whilst trying to name an instance.
+
+ """
+ pass
+
+ def showEvent(self, event):
+ super(CreatorWindow, self).showEvent(event)
+ if self._first_show:
+ self._first_show = False
+ self.setStyleSheet(style.load_stylesheet())
+
+ def refresh(self):
+ self._asset_name_input.setText(io.Session["AVALON_ASSET"])
+
+ self._creators_model.reset()
+
+ pype_project_setting = (
+ get_current_project_settings()
+ ["global"]
+ ["tools"]
+ ["creator"]
+ ["families_smart_select"]
+ )
+ current_index = None
+ family = None
+ task_name = io.Session.get("AVALON_TASK", None)
+ lowered_task_name = task_name.lower()
+ if task_name:
+ for _family, _task_names in pype_project_setting.items():
+ _low_task_names = {name.lower() for name in _task_names}
+ for _task_name in _low_task_names:
+ if _task_name in lowered_task_name:
+ family = _family
+ break
+ if family:
+ break
+
+ if family:
+ indexes = self._creators_model.get_indexes_by_family(family)
+ if indexes:
+ index = indexes[0]
+ current_index = self._creators_proxy.mapFromSource(index)
+
+ if current_index is None or not current_index.isValid():
+ current_index = self._creators_proxy.index(0, 0)
+
+ self._creators_view.setCurrentIndex(current_index)
+
+ def _on_create(self):
+ # Do not allow creation in an invalid state
+ if not self._is_valid:
+ return
+
+ index = self._creators_view.currentIndex()
+ item_id = index.data(ITEM_ID_ROLE)
+ creator_plugin = self._creators_model.get_creator_by_id(item_id)
+ if creator_plugin is None:
+ return
+
+ subset_name = self._subset_name_input.text()
+ asset_name = self._asset_name_input.text()
+ use_selection = self._useselection_chk.isChecked()
+
+ variant = self._variant_input.text()
+
+ error_info = None
+ try:
+ api.create(
+ creator_plugin,
+ subset_name,
+ asset_name,
+ options={"useSelection": use_selection},
+ data={"variant": variant}
+ )
+
+ except api.CreatorError as exc:
+ self.echo("Creator error: {}".format(str(exc)))
+ error_info = (str(exc), None)
+
+ except Exception as exc:
+ self.echo("Program error: %s" % str(exc))
+
+ exc_type, exc_value, exc_traceback = sys.exc_info()
+ formatted_traceback = "".join(traceback.format_exception(
+ exc_type, exc_value, exc_traceback
+ ))
+ error_info = (str(exc), formatted_traceback)
+
+ if error_info:
+ box = CreateErrorMessageBox(
+ creator_plugin.family, subset_name, asset_name, *error_info
+ )
+ box.show()
+ # Store dialog so is not garbage collected before is shown
+ self._message_dialog = box
+
+ else:
+ self.echo("Created %s .." % subset_name)
+
+ def _on_msg_timer(self):
+ self._msg_label.setText("")
+
+ def echo(self, message):
+ self._msg_label.setText(str(message))
+ self._msg_timer.start()
+
+
+def show(debug=False, parent=None):
+ """Display asset creator GUI
+
+ Arguments:
+ debug (bool, optional): Run loader in debug-mode,
+ defaults to False
+ parent (QtCore.QObject, optional): When provided parent the interface
+ to this QObject.
+
+ """
+
+ try:
+ module.window.close()
+ del(module.window)
+ except (AttributeError, RuntimeError):
+ pass
+
+ if debug:
+ from avalon import mock
+ for creator in mock.creators:
+ api.register_plugin(api.Creator, creator)
+
+ import traceback
+ sys.excepthook = lambda typ, val, tb: traceback.print_last()
+
+ io.install()
+
+ any_project = next(
+ project for project in io.projects()
+ if project.get("active", True) is not False
+ )
+
+ api.Session["AVALON_PROJECT"] = any_project["name"]
+ module.project = any_project["name"]
+
+ with qt_app_context():
+ window = CreatorWindow(parent)
+ window.refresh()
+ window.show()
+
+ module.window = window
+
+ # Pull window to the front.
+ module.window.raise_()
+ module.window.activateWindow()
diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py
index 8d6b609282..c8acbe77c2 100644
--- a/openpype/tools/launcher/window.py
+++ b/openpype/tools/launcher/window.py
@@ -8,7 +8,7 @@ from avalon.api import AvalonMongoDB
from openpype import style
from openpype.api import resources
-from openpype.tools.utils.widgets import AssetWidget
+from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget
from openpype.tools.utils.tasks_widget import TasksWidget
from avalon.vendor import qtawesome
@@ -138,14 +138,9 @@ class AssetsPanel(QtWidgets.QWidget):
project_bar_layout.addWidget(project_bar)
# Assets widget
- assets_widget = AssetWidget(dbcon=self.dbcon, parent=self)
-
+ assets_widget = SingleSelectAssetsWidget(dbcon=self.dbcon, parent=self)
# Make assets view flickable
- flick = FlickCharm(parent=self)
- flick.activateOn(assets_widget.view)
- assets_widget.view.setVerticalScrollMode(
- assets_widget.view.ScrollPerPixel
- )
+ assets_widget.activate_flick_charm()
# Tasks widget
tasks_widget = TasksWidget(self.dbcon, self)
@@ -183,6 +178,9 @@ class AssetsPanel(QtWidgets.QWidget):
self._tasks_widget = tasks_widget
self._btn_back = btn_back
+ def select_asset(self, asset_name):
+ self.assets_widget.select_asset_by_name(asset_name)
+
def showEvent(self, event):
super(AssetsPanel, self).showEvent(event)
@@ -206,35 +204,15 @@ class AssetsPanel(QtWidgets.QWidget):
This updates the task view.
"""
- asset_name = None
- asset_silo = None
-
# Check asset on current index and selected assets
- asset_doc = self.assets_widget.get_active_asset_document()
- selected_asset_docs = self.assets_widget.get_selected_assets()
- # If there are not asset selected docs then active asset is not
- # selected
- if not selected_asset_docs:
- asset_doc = None
- elif asset_doc:
- # If selected asset doc and current asset are not same than
- # something bad happened
- if selected_asset_docs[0]["_id"] != asset_doc["_id"]:
- asset_doc = None
-
- if asset_doc:
- asset_name = asset_doc["name"]
- asset_silo = asset_doc.get("silo")
+ asset_id = self.assets_widget.get_selected_asset_id()
+ asset_name = self.assets_widget.get_selected_asset_name()
self.dbcon.Session["AVALON_TASK"] = None
self.dbcon.Session["AVALON_ASSET"] = asset_name
- self.dbcon.Session["AVALON_SILO"] = asset_silo
self.session_changed.emit()
- asset_id = None
- if asset_doc:
- asset_id = asset_doc["_id"]
self._tasks_widget.set_asset_id(asset_id)
def _on_task_change(self):
@@ -431,7 +409,6 @@ class LauncherWindow(QtWidgets.QDialog):
def set_session(self, session):
project_name = session.get("AVALON_PROJECT")
- silo = session.get("AVALON_SILO")
asset_name = session.get("AVALON_ASSET")
task_name = session.get("AVALON_TASK")
@@ -446,11 +423,8 @@ class LauncherWindow(QtWidgets.QDialog):
index
)
- if silo:
- self.asset_panel.assets_widget.set_silo(silo)
-
if asset_name:
- self.asset_panel.assets_widget.select_assets([asset_name])
+ self.asset_panel.select_asset(asset_name)
if task_name:
# requires a forced refresh first
diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py
index 710e25bd76..d0d07a316c 100644
--- a/openpype/tools/libraryloader/app.py
+++ b/openpype/tools/libraryloader/app.py
@@ -11,7 +11,7 @@ from openpype.tools.loader.widgets import (
FamilyListView,
RepresentationWidget
)
-from openpype.tools.utils.widgets import AssetWidget
+from openpype.tools.utils.assets_widget import MultiSelectAssetsWidget
from openpype.modules import ModulesManager
@@ -76,8 +76,8 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
projects_combobox.setItemDelegate(combobox_delegate)
# Assets widget
- assets_widget = AssetWidget(
- dbcon, multiselection=True, parent=left_side_splitter
+ assets_widget = MultiSelectAssetsWidget(
+ dbcon, parent=left_side_splitter
)
# Families widget
@@ -165,7 +165,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
)
assets_widget.selection_changed.connect(self.on_assetschanged)
assets_widget.refresh_triggered.connect(self.on_assetschanged)
- assets_widget.view.clicked.connect(self.on_assetview_click)
subsets_widget.active_changed.connect(self.on_subsetschanged)
subsets_widget.version_changed.connect(self.on_versionschanged)
subsets_widget.refreshed.connect(self._on_subset_refresh)
@@ -204,11 +203,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
self._initial_refresh = True
self.refresh()
- def on_assetview_click(self, *args):
- selection_model = self._subsets_widget.view.selectionModel()
- if selection_model.selectedIndexes():
- selection_model.clearSelection()
-
def _set_projects(self):
# Store current project
old_project_name = self.current_project
@@ -348,25 +342,14 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
self._families_filter_view.set_enabled_families(set())
self._families_filter_view.refresh()
- self._assets_widget.model.stop_fetch_thread()
+ self._assets_widget.stop_refresh()
self._assets_widget.refresh()
self._assets_widget.setFocus()
def clear_assets_underlines(self):
last_asset_ids = self.data["state"]["assetIds"]
- if not last_asset_ids:
- return
-
- assets_model = self._assets_widget.model
- id_role = assets_model.ObjectIdRole
-
- for index in tools_lib.iter_model_rows(assets_model, 0):
- if index.data(id_role) not in last_asset_ids:
- continue
-
- assets_model.setData(
- index, [], assets_model.subsetColorsRole
- )
+ if last_asset_ids:
+ self._assets_widget.clear_underlines()
def _assetschanged(self):
"""Selected assets have changed"""
@@ -382,12 +365,8 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
)
return
- # filter None docs they are silo
- asset_docs = self._assets_widget.get_selected_assets()
- if len(asset_docs) == 0:
- return
+ asset_ids = self._assets_widget.get_selected_asset_ids()
- asset_ids = [asset_doc["_id"] for asset_doc in asset_docs]
# Start loading
self._subsets_widget.set_loading_state(
loading=bool(asset_ids),
@@ -402,7 +381,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
# Clear the version information on asset change
self._version_info_widget.set_version(None)
- self._thumbnail_widget.set_thumbnail(asset_docs)
+ self._thumbnail_widget.set_thumbnail(asset_ids)
self.data["state"]["assetIds"] = asset_ids
@@ -421,7 +400,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
_merged=True, _other=False
)
- asset_models = {}
+ asset_colors = {}
asset_ids = []
for subset_node in selected_subsets:
asset_ids.extend(subset_node.get("assetIds", []))
@@ -429,30 +408,17 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
for subset_node in selected_subsets:
for asset_id in asset_ids:
- if asset_id not in asset_models:
- asset_models[asset_id] = []
+ if asset_id not in asset_colors:
+ asset_colors[asset_id] = []
color = None
if asset_id in subset_node.get("assetIds", []):
color = subset_node["subsetColor"]
- asset_models[asset_id].append(color)
+ asset_colors[asset_id].append(color)
- self.clear_assets_underlines()
+ self._assets_widget.set_underline_colors(asset_colors)
- indexes = self._assets_widget.view.selectionModel().selectedRows()
-
- assets_model = self._assets_widget.model
- for index in indexes:
- id = index.data(assets_model.ObjectIdRole)
- if id not in asset_models:
- continue
-
- assets_model.setData(
- index, asset_models[id], assets_model.subsetColorsRole
- )
- # Trigger repaint
- self._assets_widget.view.updateGeometries()
# Set version in Version Widget
self._versionschanged()
@@ -489,13 +455,14 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
self._version_info_widget.set_version(version_doc)
- thumbnail_docs = version_docs
- if not thumbnail_docs:
- asset_docs = self._assets_widget.get_selected_assets()
- if len(asset_docs) > 0:
- thumbnail_docs = asset_docs
+ thumbnail_src_ids = [
+ version_doc["_id"]
+ for version_doc in version_docs
+ ]
+ if not thumbnail_src_ids:
+ thumbnail_src_ids = self._assets_widget.get_selected_asset_ids()
- self._thumbnail_widget.set_thumbnail(thumbnail_docs)
+ self._thumbnail_widget.set_thumbnail(thumbnail_src_ids)
version_ids = [doc["_id"] for doc in version_docs or []]
if self._repres_widget:
@@ -514,8 +481,8 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
None
"""
- asset = context.get("asset", None)
- if asset is None:
+ asset_name = context.get("asset", None)
+ if asset_name is None:
return
if refresh:
@@ -527,7 +494,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
# scheduled refresh and the silo tabs are not shown.
self._refresh_assets()
- self._assets_widget.select_assets(asset)
+ self._assets_widget.select_asset_by_name(asset_name)
def _on_message_timeout(self):
self._message_label.setText("")
diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py
index 9a4f2f1984..0c7844c4fb 100644
--- a/openpype/tools/loader/app.py
+++ b/openpype/tools/loader/app.py
@@ -4,8 +4,8 @@ from Qt import QtWidgets, QtCore
from avalon import api, io, pipeline
from openpype import style
-from openpype.tools.utils.widgets import AssetWidget
from openpype.tools.utils import lib
+from openpype.tools.utils.assets_widget import MultiSelectAssetsWidget
from .widgets import (
SubsetWidget,
@@ -65,8 +65,8 @@ class LoaderWindow(QtWidgets.QDialog):
left_side_splitter.setOrientation(QtCore.Qt.Vertical)
# Assets widget
- assets_widget = AssetWidget(
- io, multiselection=True, parent=left_side_splitter
+ assets_widget = MultiSelectAssetsWidget(
+ io, parent=left_side_splitter
)
assets_widget.set_current_asset_btn_visibility(True)
@@ -156,8 +156,6 @@ class LoaderWindow(QtWidgets.QDialog):
)
assets_widget.selection_changed.connect(self.on_assetschanged)
assets_widget.refresh_triggered.connect(self.on_assetschanged)
- # TODO do not touch view in asset widget
- assets_widget.view.clicked.connect(self.on_assetview_click)
subsets_widget.active_changed.connect(self.on_subsetschanged)
subsets_widget.version_changed.connect(self.on_versionschanged)
subsets_widget.refreshed.connect(self._on_subset_refresh)
@@ -216,12 +214,6 @@ class LoaderWindow(QtWidgets.QDialog):
# Delay calling blocking methods
# -------------------------------
- def on_assetview_click(self, *args):
- # TODO do not touch inner attributes of subset widget
- selection_model = self._subsets_widget.view.selectionModel()
- if selection_model.selectedIndexes():
- selection_model.clearSelection()
-
def refresh(self):
self.echo("Fetching results..")
lib.schedule(self._refresh, 50, channel="mongo")
@@ -271,7 +263,7 @@ class LoaderWindow(QtWidgets.QDialog):
# Refresh families config
self._families_filter_view.refresh()
# Change to context asset on context change
- self._assets_widget.select_assets(io.Session["AVALON_ASSET"])
+ self._assets_widget.select_asset_by_name(io.Session["AVALON_ASSET"])
def _refresh(self):
"""Load assets from database"""
@@ -292,20 +284,9 @@ class LoaderWindow(QtWidgets.QDialog):
on selection change so they match current selection.
"""
# TODO do not touch inner attributes of asset widget
- last_asset_ids = self.data["state"]["assetIds"] or []
- if not last_asset_ids:
- return
-
- assets_widget = self._assets_widget
- id_role = assets_widget.model.ObjectIdRole
-
- for index in lib.iter_model_rows(assets_widget.model, 0):
- if index.data(id_role) not in last_asset_ids:
- continue
-
- assets_widget.model.setData(
- index, [], assets_widget.model.subsetColorsRole
- )
+ last_asset_ids = self.data["state"]["assetIds"]
+ if last_asset_ids:
+ self._assets_widget.clear_underlines()
def _assetschanged(self):
"""Selected assets have changed"""
@@ -317,9 +298,7 @@ class LoaderWindow(QtWidgets.QDialog):
self.clear_assets_underlines()
# filter None docs they are silo
- asset_docs = self._assets_widget.get_selected_assets()
-
- asset_ids = [asset_doc["_id"] for asset_doc in asset_docs]
+ asset_ids = self._assets_widget.get_selected_asset_ids()
# Start loading
subsets_widget.set_loading_state(
loading=bool(asset_ids),
@@ -333,7 +312,7 @@ class LoaderWindow(QtWidgets.QDialog):
)
# Clear the version information on asset change
- self._thumbnail_widget.set_thumbnail(asset_docs)
+ self._thumbnail_widget.set_thumbnail(asset_ids)
self._version_info_widget.set_version(None)
self.data["state"]["assetIds"] = asset_ids
@@ -353,7 +332,7 @@ class LoaderWindow(QtWidgets.QDialog):
_merged=True, _other=False
)
- asset_models = {}
+ asset_colors = {}
asset_ids = []
for subset_node in selected_subsets:
asset_ids.extend(subset_node.get("assetIds", []))
@@ -361,31 +340,17 @@ class LoaderWindow(QtWidgets.QDialog):
for subset_node in selected_subsets:
for asset_id in asset_ids:
- if asset_id not in asset_models:
- asset_models[asset_id] = []
+ if asset_id not in asset_colors:
+ asset_colors[asset_id] = []
color = None
if asset_id in subset_node.get("assetIds", []):
color = subset_node["subsetColor"]
- asset_models[asset_id].append(color)
+ asset_colors[asset_id].append(color)
- self.clear_assets_underlines()
+ self._assets_widget.set_underline_colors(asset_colors)
- # TODO do not use inner attributes of asset widget
- assets_widget = self._assets_widget
- indexes = assets_widget.view.selectionModel().selectedRows()
-
- for index in indexes:
- id = index.data(assets_widget.model.ObjectIdRole)
- if id not in asset_models:
- continue
-
- assets_widget.model.setData(
- index, asset_models[id], assets_widget.model.subsetColorsRole
- )
- # Trigger repaint
- assets_widget.view.updateGeometries()
# Set version in Version Widget
self._versionschanged()
@@ -424,13 +389,14 @@ class LoaderWindow(QtWidgets.QDialog):
self._version_info_widget.set_version(version_doc)
- thumbnail_docs = version_docs
- asset_docs = self._assets_widget.get_selected_assets()
- if not thumbnail_docs:
- if len(asset_docs) > 0:
- thumbnail_docs = asset_docs
+ thumbnail_src_ids = [
+ version_doc["_id"]
+ for version_doc in version_docs
+ ]
+ if not thumbnail_src_ids:
+ thumbnail_src_ids = self._assets_widget.get_selected_asset_ids()
- self._thumbnail_widget.set_thumbnail(thumbnail_docs)
+ self._thumbnail_widget.set_thumbnail(thumbnail_src_ids)
if self._repres_widget is not None:
version_ids = [doc["_id"] for doc in version_docs or []]
@@ -472,7 +438,7 @@ class LoaderWindow(QtWidgets.QDialog):
# scheduled refresh and the silo tabs are not shown.
self._refresh()
- self._assets_widget.select_assets(asset)
+ self._assets_widget.select_asset_by_name(asset)
def _on_message_timeout(self):
self._message_label.setText("")
diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py
index 08b58eebbe..f14f58dfb4 100644
--- a/openpype/tools/loader/widgets.py
+++ b/openpype/tools/loader/widgets.py
@@ -794,19 +794,23 @@ class ThumbnailWidget(QtWidgets.QLabel):
QtCore.Qt.SmoothTransformation
)
- def set_thumbnail(self, entity=None):
- if not entity:
+ def set_thumbnail(self, doc_id=None):
+ if not doc_id:
self.set_pixmap()
return
- if isinstance(entity, (list, tuple)):
- if len(entity) == 1:
- entity = entity[0]
- else:
+ if isinstance(doc_id, (list, tuple)):
+ if len(doc_id) < 1:
self.set_pixmap()
return
+ doc_id = doc_id[0]
- thumbnail_id = entity.get("data", {}).get("thumbnail_id")
+ doc = self.dbcon.find_one(
+ {"_id": doc_id},
+ {"data.thumbnail_id"}
+ )
+
+ thumbnail_id = doc.get("data", {}).get("thumbnail_id")
if thumbnail_id == self.current_thumb_id:
if self.current_thumbnail is None:
self.set_pixmap()
diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py
index cf0850bde8..dc44aade45 100644
--- a/openpype/tools/publisher/constants.py
+++ b/openpype/tools/publisher/constants.py
@@ -6,7 +6,6 @@ CONTEXT_LABEL = "Options"
# Allowed symbols for subset name (and variant)
# - characters, numbers, unsercore and dash
-SUBSET_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_."
VARIANT_TOOLTIP = (
"Variant may contain alphabetical characters (a-Z)"
"\nnumerical characters (0-9) dot (\".\") or underscore (\"_\")."
@@ -23,7 +22,6 @@ FAMILY_ROLE = QtCore.Qt.UserRole + 5
__all__ = (
"CONTEXT_ID",
- "SUBSET_NAME_ALLOWED_SYMBOLS",
"VARIANT_TOOLTIP",
"INSTANCE_ID_ROLE",
diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py
index 0206f038fb..84fc6d4e97 100644
--- a/openpype/tools/publisher/widgets/create_dialog.py
+++ b/openpype/tools/publisher/widgets/create_dialog.py
@@ -9,11 +9,13 @@ except Exception:
commonmark = None
from Qt import QtWidgets, QtCore, QtGui
-from openpype.pipeline.create import CreatorError
+from openpype.pipeline.create import (
+ CreatorError,
+ SUBSET_NAME_ALLOWED_SYMBOLS
+)
from .widgets import IconValuePixmapLabel
from ..constants import (
- SUBSET_NAME_ALLOWED_SYMBOLS,
VARIANT_TOOLTIP,
CREATOR_IDENTIFIER_ROLE,
FAMILY_ROLE
diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py
index 606985c058..fe00ee78d3 100644
--- a/openpype/tools/publisher/widgets/widgets.py
+++ b/openpype/tools/publisher/widgets/widgets.py
@@ -9,7 +9,7 @@ from avalon.vendor import qtawesome
from openpype.widgets.attribute_defs import create_widget_for_attr_def
from openpype.tools.flickcharm import FlickCharm
-
+from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
from .models import (
AssetsHierarchyModel,
TasksModel,
@@ -21,7 +21,6 @@ from .icons import (
)
from ..constants import (
- SUBSET_NAME_ALLOWED_SYMBOLS,
VARIANT_TOOLTIP
)
diff --git a/openpype/tools/repack_version.py b/openpype/tools/repack_version.py
new file mode 100644
index 0000000000..0172264c79
--- /dev/null
+++ b/openpype/tools/repack_version.py
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+"""Script to rehash and repack current version."""
+
+import enlighten
+import blessed
+from pathlib import Path
+import platform
+from zipfile import ZipFile
+from typing import List
+import hashlib
+import sys
+from igniter.bootstrap_repos import OpenPypeVersion
+
+
+class VersionRepacker:
+
+ def __init__(self, directory: str):
+ self._term = blessed.Terminal()
+ self._manager = enlighten.get_manager()
+ self._last_increment = 0
+ self.version_path = Path(directory)
+ self.zip_path = self.version_path.parent
+ _version = {}
+ with open(self.version_path / "openpype" / "version.py") as fp:
+ exec(fp.read(), _version)
+ self._version_py = _version["__version__"]
+ del _version
+
+ def _print(self, msg: str, message_type: int = 0) -> None:
+ """Print message to console.
+
+ Args:
+ msg (str): message to print
+ message_type (int): type of message (0 info, 1 error, 2 note)
+
+ """
+ if message_type == 0:
+ header = self._term.aquamarine3(">>> ")
+ elif message_type == 1:
+ header = self._term.orangered2("!!! ")
+ elif message_type == 2:
+ header = self._term.tan1("... ")
+ else:
+ header = self._term.darkolivegreen3("--- ")
+
+ print("{}{}".format(header, msg))
+
+ @staticmethod
+ def sha256sum(filename):
+ """Calculate sha256 for content of the file.
+
+ Args:
+ filename (str): Path to file.
+
+ Returns:
+ str: hex encoded sha256
+
+ """
+ h = hashlib.sha256()
+ b = bytearray(128 * 1024)
+ mv = memoryview(b)
+ with open(filename, 'rb', buffering=0) as f:
+ for n in iter(lambda: f.readinto(mv), 0):
+ h.update(mv[:n])
+ return h.hexdigest()
+
+ @staticmethod
+ def _filter_dir(path: Path, path_filter: List) -> List[Path]:
+ """Recursively crawl over path and filter."""
+ result = []
+ for item in path.iterdir():
+ if item.name in path_filter:
+ continue
+ if item.name.startswith('.'):
+ continue
+ if item.is_dir():
+ result.extend(VersionRepacker._filter_dir(item, path_filter))
+ else:
+ result.append(item)
+ return result
+
+ def process(self):
+ if (self.version_path / "pyproject.toml").exists():
+ self._print(
+ ("This cannot run on OpenPype sources. "
+ "Please run it on extracted version."), 1)
+ return
+ self._print(f"Rehashing and zipping {self.version_path}")
+ version = OpenPypeVersion.version_in_str(self.version_path.name)
+ if not version:
+ self._print("Cannot get version from directory", 1)
+ return
+
+ self._print(f"Detected version is {version}")
+ # replace version in version.py
+ self._replace_version(version, self.version_path)
+ self._print("Recalculating checksums ...", 2)
+
+ checksums = []
+
+ file_list = VersionRepacker._filter_dir(self.version_path, [])
+ progress_bar = enlighten.Counter(
+ total=len(file_list), desc="Calculating checksums",
+ nits="%", color="green")
+ for file in file_list:
+ checksums.append((
+ VersionRepacker.sha256sum(file.as_posix()),
+ file.resolve().relative_to(self.version_path),
+ file
+ ))
+ progress_bar.update()
+ progress_bar.close()
+
+ progress_bar = enlighten.Counter(
+ total=len(checksums), desc="Zipping directory",
+ nits="%", color=(56, 211, 159))
+
+ zip_filename = self.zip_path / f"openpype-v{version}.zip"
+ with ZipFile(zip_filename, "w") as zip_file:
+
+ for item in checksums:
+ if item[1].as_posix() == "checksums":
+ progress_bar.update()
+ continue
+ zip_file.write(item[2], item[1])
+ progress_bar.update()
+
+ checksums_str = ""
+ for c in checksums:
+ file_str = c[1]
+ if platform.system().lower() == "windows":
+ file_str = c[1].as_posix().replace("\\", "/")
+ checksums_str += "{}:{}\n".format(c[0], file_str)
+ zip_file.writestr("checksums", checksums_str)
+ # test if zip is ok
+ zip_file.testzip()
+ self._print(f"All done, you can find new zip here: {zip_filename}")
+
+ @staticmethod
+ def _replace_version(version: OpenPypeVersion, path: Path):
+ """Replace version in version.py.
+
+ Args:
+ version (OpenPypeVersion): OpenPype version to set
+ path (Path): Path to unzipped version.
+
+ """
+ with open(path / "openpype" / "version.py", "r") as op_version_file:
+ replacement = ""
+
+ for line in op_version_file:
+ stripped_line = line.strip()
+ if stripped_line.strip().startswith("__version__ ="):
+ line = f'__version__ = "{version}"\n'
+ replacement += line
+
+ with open(path / "openpype" / "version.py", "w") as op_version_file:
+ op_version_file.write(replacement)
+
+
+if __name__ == '__main__':
+ print(sys.argv[1])
+ version_packer = VersionRepacker(sys.argv[1])
+ version_packer.process()
diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py
index ecad8eac0a..75e2b6be40 100644
--- a/openpype/tools/sceneinventory/switch_dialog.py
+++ b/openpype/tools/sceneinventory/switch_dialog.py
@@ -2,10 +2,13 @@ import collections
import logging
from Qt import QtWidgets, QtCore
-from avalon import io, api
+from avalon import io, api, pipeline
from avalon.vendor import qtawesome
-from .widgets import SearchComboBox
+from .widgets import (
+ ButtonWithMenu,
+ SearchComboBox
+)
log = logging.getLogger("SwitchAssetDialog")
@@ -55,7 +58,7 @@ class SwitchAssetDialog(QtWidgets.QDialog):
current_asset_btn = QtWidgets.QPushButton("Use current asset")
accept_icon = qtawesome.icon("fa.check", color="white")
- accept_btn = QtWidgets.QPushButton(self)
+ accept_btn = ButtonWithMenu(self)
accept_btn.setIcon(accept_icon)
main_layout = QtWidgets.QGridLayout(self)
@@ -100,6 +103,30 @@ class SwitchAssetDialog(QtWidgets.QDialog):
self._accept_btn = accept_btn
+ self.setMinimumWidth(self.MIN_WIDTH)
+
+ # Set default focus to accept button so you don't directly type in
+ # first asset field, this also allows to see the placeholder value.
+ accept_btn.setFocus()
+
+ self.content_loaders = set()
+ self.content_assets = {}
+ self.content_subsets = {}
+ self.content_versions = {}
+ self.content_repres = {}
+
+ self.hero_version_ids = set()
+
+ self.missing_assets = []
+ self.missing_versions = []
+ self.missing_subsets = []
+ self.missing_repres = []
+ self.missing_docs = False
+
+ self.archived_assets = []
+ self.archived_subsets = []
+ self.archived_repres = []
+
self._init_asset_name = None
self._init_subset_name = None
self._init_repre_name = None
@@ -110,20 +137,16 @@ class SwitchAssetDialog(QtWidgets.QDialog):
self._prepare_content_data()
self.refresh(True)
- self.setMinimumWidth(self.MIN_WIDTH)
-
- # Set default focus to accept button so you don't directly type in
- # first asset field, this also allows to see the placeholder value.
- accept_btn.setFocus()
-
def _prepare_content_data(self):
- repre_ids = [
- io.ObjectId(item["representation"])
- for item in self._items
- ]
+ repre_ids = set()
+ content_loaders = set()
+ for item in self._items:
+ repre_ids.add(io.ObjectId(item["representation"]))
+ content_loaders.add(item["loader"])
+
repres = list(io.find({
"type": {"$in": ["representation", "archived_representation"]},
- "_id": {"$in": repre_ids}
+ "_id": {"$in": list(repre_ids)}
}))
repres_by_id = {repre["_id"]: repre for repre in repres}
@@ -207,6 +230,7 @@ class SwitchAssetDialog(QtWidgets.QDialog):
else:
content_assets[asset_id] = assets_by_id[asset_id]
+ self.content_loaders = content_loaders
self.content_assets = content_assets
self.content_subsets = content_subsets
self.content_versions = content_versions
@@ -260,8 +284,11 @@ class SwitchAssetDialog(QtWidgets.QDialog):
# Fill comboboxes with values
self.set_labels()
+
self.apply_validations(validation_state)
+ self._build_loaders_menu()
+
if init_refresh: # pre select context if possible
self._assets_box.set_valid_value(self._init_asset_name)
self._subsets_box.set_valid_value(self._init_subset_name)
@@ -269,23 +296,89 @@ class SwitchAssetDialog(QtWidgets.QDialog):
self._fill_check = True
- def _get_loaders(self, representations):
- if not representations:
+ def _build_loaders_menu(self):
+ repre_ids = self._get_current_output_repre_ids()
+ loaders = self._get_loaders(repre_ids)
+ # Get and destroy the action group
+ self._accept_btn.clear_actions()
+
+ if not loaders:
+ return
+
+ # Build new action group
+ group = QtWidgets.QActionGroup(self._accept_btn)
+
+ for loader in loaders:
+ # Label
+ label = getattr(loader, "label", None)
+ if label is None:
+ label = loader.__name__
+
+ action = group.addAction(label)
+ # action = QtWidgets.QAction(label)
+ action.setData(loader)
+
+ # Support font-awesome icons using the `.icon` and `.color`
+ # attributes on plug-ins.
+ icon = getattr(loader, "icon", None)
+ if icon is not None:
+ try:
+ key = "fa.{0}".format(icon)
+ color = getattr(loader, "color", "white")
+ action.setIcon(qtawesome.icon(key, color=color))
+
+ except Exception as exc:
+ print("Unable to set icon for loader {}: {}".format(
+ loader, str(exc)
+ ))
+
+ self._accept_btn.add_action(action)
+
+ group.triggered.connect(self._on_action_clicked)
+
+ def _on_action_clicked(self, action):
+ loader_plugin = action.data()
+ self._trigger_switch(loader_plugin)
+
+ def _get_loaders(self, repre_ids):
+ repre_contexts = None
+ if repre_ids:
+ repre_contexts = pipeline.get_repres_contexts(repre_ids)
+
+ if not repre_contexts:
return list()
- available_loaders = filter(
- lambda l: not (hasattr(l, "is_utility") and l.is_utility),
- api.discover(api.Loader)
- )
+ available_loaders = []
+ for loader_plugin in api.discover(api.Loader):
+ # Skip loaders without switch method
+ if not hasattr(loader_plugin, "switch"):
+ continue
- loaders = set()
-
- for representation in representations:
- for loader in api.loaders_from_representation(
- available_loaders,
- representation
+ # Skip utility loaders
+ if (
+ hasattr(loader_plugin, "is_utility")
+ and loader_plugin.is_utility
):
- loaders.add(loader)
+ continue
+ available_loaders.append(loader_plugin)
+
+ loaders = None
+ for repre_context in repre_contexts.values():
+ _loaders = set(pipeline.loaders_from_repre_context(
+ available_loaders, repre_context
+ ))
+ if loaders is None:
+ loaders = _loaders
+ else:
+ loaders = _loaders.intersection(loaders)
+
+ if not loaders:
+ break
+
+ if loaders is None:
+ loaders = []
+ else:
+ loaders = list(loaders)
return loaders
@@ -325,12 +418,11 @@ class SwitchAssetDialog(QtWidgets.QDialog):
def apply_validations(self, validation_state):
error_msg = "*Please select"
error_sheet = "border: 1px solid red;"
- success_sheet = "border: 1px solid green;"
asset_sheet = None
subset_sheet = None
repre_sheet = None
- accept_sheet = None
+ accept_state = ""
if validation_state.asset_ok is False:
asset_sheet = error_sheet
self._asset_label.setText(error_msg)
@@ -342,14 +434,297 @@ class SwitchAssetDialog(QtWidgets.QDialog):
self._repre_label.setText(error_msg)
if validation_state.all_ok:
- accept_sheet = success_sheet
+ accept_state = "1"
self._assets_box.setStyleSheet(asset_sheet or "")
self._subsets_box.setStyleSheet(subset_sheet or "")
self._representations_box.setStyleSheet(repre_sheet or "")
self._accept_btn.setEnabled(validation_state.all_ok)
- self._accept_btn.setStyleSheet(accept_sheet or "")
+ self._set_style_property(self._accept_btn, "state", accept_state)
+
+ def _set_style_property(self, widget, name, value):
+ cur_value = widget.property(name)
+ if cur_value == value:
+ return
+ widget.setProperty(name, value)
+ widget.style().polish(widget)
+
+ def _get_current_output_repre_ids(self):
+ # NOTE hero versions are not used because it is expected that
+ # hero version has same representations as latests
+ selected_asset = self._assets_box.currentText()
+ selected_subset = self._subsets_box.currentText()
+ selected_repre = self._representations_box.currentText()
+
+ # Nothing is selected
+ # [ ] [ ] [ ]
+ if not selected_asset and not selected_subset and not selected_repre:
+ return list(self.content_repres.keys())
+
+ # Prepare asset document if asset is selected
+ asset_doc = None
+ if selected_asset:
+ asset_doc = io.find_one(
+ {"type": "asset", "name": selected_asset},
+ {"_id": True}
+ )
+ if not asset_doc:
+ return []
+
+ # Everything is selected
+ # [x] [x] [x]
+ if selected_asset and selected_subset and selected_repre:
+ return self._get_current_output_repre_ids_xxx(
+ asset_doc, selected_subset, selected_repre
+ )
+
+ # [x] [x] [ ]
+ # If asset and subset is selected
+ if selected_asset and selected_subset:
+ return self._get_current_output_repre_ids_xxo(
+ asset_doc, selected_subset
+ )
+
+ # [x] [ ] [x]
+ # If asset and repre is selected
+ if selected_asset and selected_repre:
+ return self._get_current_output_repre_ids_xox(
+ asset_doc, selected_repre
+ )
+
+ # [x] [ ] [ ]
+ # If asset and subset is selected
+ if selected_asset:
+ return self._get_current_output_repre_ids_xoo(asset_doc)
+
+ # [ ] [x] [x]
+ if selected_subset and selected_repre:
+ return self._get_current_output_repre_ids_oxx(
+ selected_subset, selected_repre
+ )
+
+ # [ ] [x] [ ]
+ if selected_subset:
+ return self._get_current_output_repre_ids_oxo(
+ selected_subset
+ )
+
+ # [ ] [ ] [x]
+ return self._get_current_output_repre_ids_oox(selected_repre)
+
+ def _get_current_output_repre_ids_xxx(
+ self, asset_doc, selected_subset, selected_repre
+ ):
+ subset_doc = io.find_one(
+ {
+ "type": "subset",
+ "name": selected_subset,
+ "parent": asset_doc["_id"]
+ },
+ {"_id": True}
+ )
+ subset_id = subset_doc["_id"]
+ last_versions_by_subset_id = self.find_last_versions([subset_id])
+ version_doc = last_versions_by_subset_id.get(subset_id)
+ if not version_doc:
+ return []
+
+ repre_docs = io.find(
+ {
+ "type": "representation",
+ "parent": version_doc["_id"],
+ "name": selected_repre
+ },
+ {"_id": True}
+ )
+ return [repre_doc["_id"] for repre_doc in repre_docs]
+
+ def _get_current_output_repre_ids_xxo(self, asset_doc, selected_subset):
+ subset_doc = io.find_one(
+ {
+ "type": "subset",
+ "parent": asset_doc["_id"],
+ "name": selected_subset
+ },
+ {"_id": True}
+ )
+ if not subset_doc:
+ return []
+
+ repre_names = set()
+ for repre_doc in self.content_repres.values():
+ repre_names.add(repre_doc["name"])
+
+ repre_docs = io.find(
+ {
+ "type": "rerpesentation",
+ "parent": subset_doc["_id"],
+ "name": {"$in": list(repre_names)}
+ },
+ {"_id": True}
+ )
+ return [repre_doc["_id"] for repre_doc in repre_docs]
+
+ def _get_current_output_repre_ids_xox(self, asset_doc, selected_repre):
+ susbet_names = set()
+ for subset_doc in self.content_subsets.values():
+ susbet_names.add(subset_doc["name"])
+
+ subset_docs = io.find(
+ {
+ "type": "subset",
+ "name": {"$in": list(susbet_names)},
+ "parent": asset_doc["_id"]
+ },
+ {"_id": True}
+ )
+ subset_ids = [subset_doc["_id"] for subset_doc in subset_docs]
+ repre_docs = io.find(
+ {
+ "type": "representation",
+ "parent": {"$in": subset_ids},
+ "name": selected_repre
+ },
+ {"_id": True}
+ )
+ return [repre_doc["_id"] for repre_doc in repre_docs]
+
+ def _get_current_output_repre_ids_xoo(self, asset_doc):
+ repres_by_subset_name = collections.defaultdict(set)
+ for repre_doc in self.content_repres.values():
+ repre_name = repre_doc["name"]
+ version_doc = self.content_versions[repre_doc["parent"]]
+ subset_doc = self.content_subsets[version_doc["parent"]]
+ subset_name = subset_doc["name"]
+ repres_by_subset_name[subset_name].add(repre_name)
+
+ subset_docs = list(io.find(
+ {
+ "type": "subset",
+ "parent": asset_doc["_id"],
+ "name": {"$in": list(repres_by_subset_name.keys())}
+ },
+ {"_id": True, "name": True}
+ ))
+ subset_name_by_id = {
+ subset_doc["_id"]: subset_doc["name"]
+ for subset_doc in subset_docs
+ }
+ subset_ids = list(subset_name_by_id.keys())
+ last_versions_by_subset_id = self.find_last_versions(subset_ids)
+ last_version_id_by_subset_name = {}
+ for subset_id, last_version in last_versions_by_subset_id.items():
+ subset_name = subset_name_by_id[subset_id]
+ last_version_id_by_subset_name[subset_name] = (
+ last_version["_id"]
+ )
+
+ repre_or_query = []
+ for subset_name, repre_names in repres_by_subset_name.items():
+ version_id = last_version_id_by_subset_name.get(subset_name)
+ # This should not happen but why to crash?
+ if version_id is None:
+ continue
+ repre_or_query.append({
+ "parent": version_id,
+ "name": {"$in": list(repre_names)}
+ })
+ repre_docs = io.find(
+ {"$or": repre_or_query},
+ {"_id": True}
+ )
+ return [repre_doc["_id"] for repre_doc in repre_docs]
+
+ def _get_current_output_repre_ids_oxx(
+ self, selected_subset, selected_repre
+ ):
+ subset_docs = list(io.find({
+ "type": "subset",
+ "parent": {"$in": list(self.content_assets.keys())},
+ "name": selected_subset
+ }))
+ subset_ids = [subset_doc["_id"] for subset_doc in subset_docs]
+ last_versions_by_subset_id = self.find_last_versions(subset_ids)
+ last_version_ids = [
+ last_version["_id"]
+ for last_version in last_versions_by_subset_id.values()
+ ]
+ repre_docs = io.find({
+ "type": "representation",
+ "parent": {"$in": last_version_ids},
+ "name": selected_repre
+ })
+
+ return [repre_doc["_id"] for repre_doc in repre_docs]
+
+ def _get_current_output_repre_ids_oxo(self, selected_subset):
+ subset_docs = list(io.find(
+ {
+ "type": "subset",
+ "parent": {"$in": list(self.content_assets.keys())},
+ "name": selected_subset
+ },
+ {"_id": True, "parent": True}
+ ))
+ if not subset_docs:
+ return list()
+
+ subset_docs_by_id = {
+ subset_doc["_id"]: subset_doc
+ for subset_doc in subset_docs
+ }
+ last_versions_by_subset_id = self.find_last_versions(
+ subset_docs_by_id.keys()
+ )
+
+ subset_id_by_version_id = {}
+ for subset_id, last_version in last_versions_by_subset_id.items():
+ version_id = last_version["_id"]
+ subset_id_by_version_id[version_id] = subset_id
+
+ if not subset_id_by_version_id:
+ return list()
+
+ repre_names_by_asset_id = collections.defaultdict(set)
+ for repre_doc in self.content_repres.values():
+ version_doc = self.content_versions[repre_doc["parent"]]
+ subset_doc = self.content_subsets[version_doc["parent"]]
+ asset_doc = self.content_assets[subset_doc["parent"]]
+ repre_name = repre_doc["name"]
+ asset_id = asset_doc["_id"]
+ repre_names_by_asset_id[asset_id].add(repre_name)
+
+ repre_or_query = []
+ for last_version_id, subset_id in subset_id_by_version_id.items():
+ subset_doc = subset_docs_by_id[subset_id]
+ asset_id = subset_doc["parent"]
+ repre_names = repre_names_by_asset_id.get(asset_id)
+ if not repre_names:
+ continue
+ repre_or_query.append({
+ "parent": last_version_id,
+ "name": {"$in": list(repre_names)}
+ })
+ repre_docs = io.find(
+ {
+ "type": "representation",
+ "$or": repre_or_query
+ },
+ {"_id": True}
+ )
+
+ return [repre_doc["_id"] for repre_doc in repre_docs]
+
+ def _get_current_output_repre_ids_oox(self, selected_repre):
+ repre_docs = io.find(
+ {
+ "name": selected_repre,
+ "parent": {"$in": list(self.content_versions.keys())}
+ },
+ {"_id": True}
+ )
+ return [repre_doc["_id"] for repre_doc in repre_docs]
def _get_asset_box_values(self):
asset_docs = io.find(
@@ -852,6 +1227,9 @@ class SwitchAssetDialog(QtWidgets.QDialog):
self._assets_box.setCurrentIndex(index)
def _on_accept(self):
+ self._trigger_switch()
+
+ def _trigger_switch(self, loader=None):
# Use None when not a valid value or when placeholder value
selected_asset = self._assets_box.get_valid_value()
selected_subset = self._subsets_box.get_valid_value()
@@ -974,7 +1352,7 @@ class SwitchAssetDialog(QtWidgets.QDialog):
repre_doc = repres_by_name[container_repre_name]
try:
- api.switch(container, repre_doc)
+ api.switch(container, repre_doc, loader)
except Exception:
msg = (
"Couldn't switch asset."
diff --git a/openpype/tools/sceneinventory/widgets.py b/openpype/tools/sceneinventory/widgets.py
index 6bb74d2d1b..4c4aafad3a 100644
--- a/openpype/tools/sceneinventory/widgets.py
+++ b/openpype/tools/sceneinventory/widgets.py
@@ -1,26 +1,68 @@
from Qt import QtWidgets, QtCore
+from openpype import style
+
+
+class ButtonWithMenu(QtWidgets.QToolButton):
+ def __init__(self, parent=None):
+ super(ButtonWithMenu, self).__init__(parent)
+
+ self.setObjectName("ButtonWithMenu")
+
+ self.setPopupMode(self.MenuButtonPopup)
+ menu = QtWidgets.QMenu(self)
+
+ self.setMenu(menu)
+
+ self._menu = menu
+ self._actions = []
+
+ def menu(self):
+ return self._menu
+
+ def clear_actions(self):
+ if self._menu is not None:
+ self._menu.clear()
+ self._actions = []
+
+ def add_action(self, action):
+ self._actions.append(action)
+ self._menu.addAction(action)
+
+ def _on_action_trigger(self):
+ action = self.sender()
+ if action not in self._actions:
+ return
+ action.trigger()
class SearchComboBox(QtWidgets.QComboBox):
"""Searchable ComboBox with empty placeholder value as first value"""
- def __init__(self, parent=None):
+ def __init__(self, parent):
super(SearchComboBox, self).__init__(parent)
self.setEditable(True)
self.setInsertPolicy(self.NoInsert)
- # Apply completer settings
+ combobox_delegate = QtWidgets.QStyledItemDelegate(self)
+ self.setItemDelegate(combobox_delegate)
+
completer = self.completer()
- completer.setCompletionMode(completer.PopupCompletion)
+ completer.setCompletionMode(
+ QtWidgets.QCompleter.PopupCompletion
+ )
completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
- # Force style sheet on popup menu
- # It won't take the parent stylesheet for some reason
- # todo: better fix for completer popup stylesheet
- # if module.window:
- # popup = completer.popup()
- # popup.setStyleSheet(module.window.styleSheet())
+ completer_view = completer.popup()
+ completer_view.setObjectName("CompleterView")
+ completer_delegate = QtWidgets.QStyledItemDelegate(completer_view)
+ completer_view.setItemDelegate(completer_delegate)
+ completer_view.setStyleSheet(style.load_stylesheet())
+
+ self._combobox_delegate = combobox_delegate
+
+ self._completer_delegate = completer_delegate
+ self._completer = completer
def set_placeholder(self, placeholder):
self.lineEdit().setPlaceholderText(placeholder)
diff --git a/openpype/tools/utils/assets_widget.py b/openpype/tools/utils/assets_widget.py
new file mode 100644
index 0000000000..9ebb62456f
--- /dev/null
+++ b/openpype/tools/utils/assets_widget.py
@@ -0,0 +1,807 @@
+import time
+import collections
+
+import Qt
+from Qt import QtWidgets, QtCore, QtGui
+
+from avalon import style
+from avalon.vendor import qtawesome
+
+from openpype.style import get_objected_colors
+from openpype.tools.flickcharm import FlickCharm
+
+from .views import (
+ TreeViewSpinner,
+ DeselectableTreeView
+)
+from .widgets import PlaceholderLineEdit
+from .models import RecursiveSortFilterProxyModel
+from .lib import DynamicQThread
+
+if Qt.__binding__ == "PySide":
+ from PySide.QtGui import QStyleOptionViewItemV4
+elif Qt.__binding__ == "PyQt4":
+ from PyQt4.QtGui import QStyleOptionViewItemV4
+
+ASSET_ID_ROLE = QtCore.Qt.UserRole + 1
+ASSET_NAME_ROLE = QtCore.Qt.UserRole + 2
+ASSET_LABEL_ROLE = QtCore.Qt.UserRole + 3
+ASSET_UNDERLINE_COLORS_ROLE = QtCore.Qt.UserRole + 4
+
+
+class AssetsView(TreeViewSpinner, DeselectableTreeView):
+ """Asset items view.
+
+ Adds abilities to deselect, show loading spinner and add flick charm
+ (scroll by mouse/touchpad click and move).
+ """
+
+ def __init__(self, parent=None):
+ super(AssetsView, self).__init__(parent)
+ self.setIndentation(15)
+ self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ self.setHeaderHidden(True)
+
+ self._flick_charm_activated = False
+ self._flick_charm = FlickCharm(parent=self)
+ self._before_flick_scroll_mode = None
+
+ def activate_flick_charm(self):
+ if self._flick_charm_activated:
+ return
+ self._flick_charm_activated = True
+ self._before_flick_scroll_mode = self.verticalScrollMode()
+ self._flick_charm.activateOn(self)
+ self.setVerticalScrollMode(self.ScrollPerPixel)
+
+ def deactivate_flick_charm(self):
+ if not self._flick_charm_activated:
+ return
+ self._flick_charm_activated = False
+ self._flick_charm.deactivateFrom(self)
+ if self._before_flick_scroll_mode is not None:
+ self.setVerticalScrollMode(self._before_flick_scroll_mode)
+
+ def mousePressEvent(self, event):
+ index = self.indexAt(event.pos())
+ if not index.isValid():
+ modifiers = QtWidgets.QApplication.keyboardModifiers()
+ if modifiers == QtCore.Qt.ShiftModifier:
+ return
+ elif modifiers == QtCore.Qt.ControlModifier:
+ return
+
+ super(AssetsView, self).mousePressEvent(event)
+
+ def set_loading_state(self, loading, empty):
+ """Change loading state.
+
+ TODO: Separate into 2 individual methods.
+
+ Args:
+ loading(bool): Is loading.
+ empty(bool): Is model empty.
+ """
+ if self.is_loading != loading:
+ if loading:
+ self.spinner.repaintNeeded.connect(
+ self.viewport().update
+ )
+ else:
+ self.spinner.repaintNeeded.disconnect()
+ self.viewport().update()
+
+ self.is_loading = loading
+ self.is_empty = empty
+
+
+class UnderlinesAssetDelegate(QtWidgets.QItemDelegate):
+ """Item delegate drawing bars under asset name.
+
+ This is used in loader and library loader tools. Multiselection of assets
+ may group subsets by name under colored groups. Selected color groups are
+ then propagated back to selected assets as underlines.
+ """
+ bar_height = 3
+
+ def __init__(self, *args, **kwargs):
+ super(UnderlinesAssetDelegate, self).__init__(*args, **kwargs)
+ asset_view_colors = get_objected_colors()["loader"]["asset-view"]
+ self._selected_color = (
+ asset_view_colors["selected"].get_qcolor()
+ )
+ self._hover_color = (
+ asset_view_colors["hover"].get_qcolor()
+ )
+ self._selected_hover_color = (
+ asset_view_colors["selected-hover"].get_qcolor()
+ )
+
+ def sizeHint(self, option, index):
+ """Add bar height to size hint."""
+ result = super(UnderlinesAssetDelegate, self).sizeHint(option, index)
+ height = result.height()
+ result.setHeight(height + self.bar_height)
+
+ return result
+
+ def paint(self, painter, option, index):
+ """Replicate painting of an item and draw color bars if needed."""
+ # Qt4 compat
+ if Qt.__binding__ in ("PySide", "PyQt4"):
+ option = QStyleOptionViewItemV4(option)
+
+ painter.save()
+
+ item_rect = QtCore.QRect(option.rect)
+ item_rect.setHeight(option.rect.height() - self.bar_height)
+
+ subset_colors = index.data(ASSET_UNDERLINE_COLORS_ROLE) or []
+ subset_colors_width = 0
+ if subset_colors:
+ subset_colors_width = option.rect.width() / len(subset_colors)
+
+ subset_rects = []
+ counter = 0
+ for subset_c in subset_colors:
+ new_color = None
+ new_rect = None
+ if subset_c:
+ new_color = QtGui.QColor(*subset_c)
+
+ new_rect = QtCore.QRect(
+ option.rect.left() + (counter * subset_colors_width),
+ option.rect.top() + (
+ option.rect.height() - self.bar_height
+ ),
+ subset_colors_width,
+ self.bar_height
+ )
+ subset_rects.append((new_color, new_rect))
+ counter += 1
+
+ # Background
+ if option.state & QtWidgets.QStyle.State_Selected:
+ if len(subset_colors) == 0:
+ item_rect.setTop(item_rect.top() + (self.bar_height / 2))
+
+ if option.state & QtWidgets.QStyle.State_MouseOver:
+ bg_color = self._selected_hover_color
+ else:
+ bg_color = self._selected_color
+ else:
+ item_rect.setTop(item_rect.top() + (self.bar_height / 2))
+ if option.state & QtWidgets.QStyle.State_MouseOver:
+ bg_color = self._hover_color
+ else:
+ bg_color = QtGui.QColor()
+ bg_color.setAlpha(0)
+
+ # When not needed to do a rounded corners (easier and without
+ # painter restore):
+ painter.fillRect(
+ option.rect,
+ QtGui.QBrush(bg_color)
+ )
+
+ if option.state & QtWidgets.QStyle.State_Selected:
+ for color, subset_rect in subset_rects:
+ if not color or not subset_rect:
+ continue
+ painter.fillRect(subset_rect, QtGui.QBrush(color))
+
+ # Icon
+ icon_index = index.model().index(
+ index.row(), index.column(), index.parent()
+ )
+ # - Default icon_rect if not icon
+ icon_rect = QtCore.QRect(
+ item_rect.left(),
+ item_rect.top(),
+ # To make sure it's same size all the time
+ option.rect.height() - self.bar_height,
+ option.rect.height() - self.bar_height
+ )
+ icon = index.model().data(icon_index, QtCore.Qt.DecorationRole)
+
+ if icon:
+ mode = QtGui.QIcon.Normal
+ if not (option.state & QtWidgets.QStyle.State_Enabled):
+ mode = QtGui.QIcon.Disabled
+ elif option.state & QtWidgets.QStyle.State_Selected:
+ mode = QtGui.QIcon.Selected
+
+ if isinstance(icon, QtGui.QPixmap):
+ icon = QtGui.QIcon(icon)
+ option.decorationSize = icon.size() / icon.devicePixelRatio()
+
+ elif isinstance(icon, QtGui.QColor):
+ pixmap = QtGui.QPixmap(option.decorationSize)
+ pixmap.fill(icon)
+ icon = QtGui.QIcon(pixmap)
+
+ elif isinstance(icon, QtGui.QImage):
+ icon = QtGui.QIcon(QtGui.QPixmap.fromImage(icon))
+ option.decorationSize = icon.size() / icon.devicePixelRatio()
+
+ elif isinstance(icon, QtGui.QIcon):
+ state = QtGui.QIcon.Off
+ if option.state & QtWidgets.QStyle.State_Open:
+ state = QtGui.QIcon.On
+ actual_size = option.icon.actualSize(
+ option.decorationSize, mode, state
+ )
+ option.decorationSize = QtCore.QSize(
+ min(option.decorationSize.width(), actual_size.width()),
+ min(option.decorationSize.height(), actual_size.height())
+ )
+
+ state = QtGui.QIcon.Off
+ if option.state & QtWidgets.QStyle.State_Open:
+ state = QtGui.QIcon.On
+
+ icon.paint(
+ painter, icon_rect,
+ QtCore.Qt.AlignLeft, mode, state
+ )
+
+ # Text
+ text_rect = QtCore.QRect(
+ icon_rect.left() + icon_rect.width() + 2,
+ item_rect.top(),
+ item_rect.width(),
+ item_rect.height()
+ )
+
+ painter.drawText(
+ text_rect, QtCore.Qt.AlignVCenter,
+ index.data(QtCore.Qt.DisplayRole)
+ )
+
+ painter.restore()
+
+
+class AssetModel(QtGui.QStandardItemModel):
+ """A model listing assets in the active project.
+
+ The assets are displayed in a treeview, they are visually parented by
+ a `visualParent` field in the database containing an `_id` to a parent
+ asset.
+
+ Asset document may have defined label, icon or icon color.
+
+ Loading of data for model happens in thread which means that refresh
+ is not sequential. When refresh is triggered it is required to listen for
+ 'refreshed' signal.
+
+ Args:
+ dbcon (AvalonMongoDB): Ready to use connection to mongo with.
+ parent (QObject): Parent Qt object.
+ """
+
+ _doc_fetched = QtCore.Signal()
+ refreshed = QtCore.Signal(bool)
+
+ # Asset document projection
+ _asset_projection = {
+ "name": 1,
+ "parent": 1,
+ "data.visualParent": 1,
+ "data.label": 1,
+ "data.icon": 1,
+ "data.color": 1
+ }
+
+ def __init__(self, dbcon, parent=None):
+ super(AssetModel, self).__init__(parent=parent)
+ self.dbcon = dbcon
+
+ self._refreshing = False
+ self._doc_fetching_thread = None
+ self._doc_fetching_stop = False
+ self._doc_payload = []
+
+ self._doc_fetched.connect(self._on_docs_fetched)
+
+ self._items_with_color_by_id = {}
+ self._items_by_asset_id = {}
+
+ @property
+ def refreshing(self):
+ return self._refreshing
+
+ def get_index_by_asset_id(self, asset_id):
+ item = self._items_by_asset_id.get(asset_id)
+ if item is not None:
+ return item.index()
+ return QtCore.QModelIndex()
+
+ def get_indexes_by_asset_ids(self, asset_ids):
+ return [
+ self.get_index_by_asset_id(asset_id)
+ for asset_id in asset_ids
+ ]
+
+ def get_index_by_asset_name(self, asset_name):
+ indexes = self.get_indexes_by_asset_names([asset_name])
+ for index in indexes:
+ if index.isValid():
+ return index
+ return indexes[0]
+
+ def get_indexes_by_asset_names(self, asset_names):
+ asset_ids_by_name = {
+ asset_name: None
+ for asset_name in asset_names
+ }
+
+ for asset_id, item in self._items_by_asset_id.items():
+ asset_name = item.data(ASSET_NAME_ROLE)
+ if asset_name in asset_ids_by_name:
+ asset_ids_by_name[asset_name] = asset_id
+
+ asset_ids = [
+ asset_ids_by_name[asset_name]
+ for asset_name in asset_names
+ ]
+
+ return self.get_indexes_by_asset_ids(asset_ids)
+
+ def refresh(self, force=False):
+ """Refresh the data for the model."""
+ # Skip fetch if there is already other thread fetching documents
+ if self._refreshing:
+ if not force:
+ return
+ self.stop_refresh()
+
+ # Fetch documents from mongo
+ # Restart payload
+ self._refreshing = True
+ self._doc_payload = []
+ self._doc_fetching_thread = DynamicQThread(self._threaded_fetch)
+ self._doc_fetching_thread.start()
+
+ def stop_refresh(self):
+ self._stop_fetch_thread()
+
+ def clear_underlines(self):
+ for asset_id in tuple(self._items_with_color_by_id.keys()):
+ item = self._items_with_color_by_id.pop(asset_id)
+ item.setData(None, ASSET_UNDERLINE_COLORS_ROLE)
+
+ def set_underline_colors(self, colors_by_asset_id):
+ self.clear_underlines()
+
+ for asset_id, colors in colors_by_asset_id.items():
+ item = self._items_by_asset_id.get(asset_id)
+ if item is None:
+ continue
+ item.setData(colors, ASSET_UNDERLINE_COLORS_ROLE)
+
+ def _on_docs_fetched(self):
+ # Make sure refreshing did not change
+ # - since this line is refreshing sequential and
+ # triggering of new refresh will happen when this method is done
+ if not self._refreshing:
+ root_item = self.invisibleRootItem()
+ root_item.removeRows(0, root_item.rowCount())
+ self._items_by_asset_id = {}
+ self._items_with_color_by_id = {}
+ return
+
+ # Collect asset documents as needed
+ asset_ids = set()
+ asset_docs_by_id = {}
+ asset_ids_by_parents = collections.defaultdict(set)
+ for asset_doc in self._doc_payload:
+ asset_id = asset_doc["_id"]
+ asset_data = asset_doc.get("data") or {}
+ parent_id = asset_data.get("visualParent")
+ asset_ids.add(asset_id)
+ asset_docs_by_id[asset_id] = asset_doc
+ asset_ids_by_parents[parent_id].add(asset_id)
+
+ # Prepare removed asset ids
+ removed_asset_ids = (
+ set(self._items_by_asset_id.keys()) - set(asset_docs_by_id.keys())
+ )
+
+ # Prepare queue for adding new items
+ asset_items_queue = collections.deque()
+
+ # Queue starts with root item and 'visualParent' None
+ root_item = self.invisibleRootItem()
+ asset_items_queue.append((None, root_item))
+
+ while asset_items_queue:
+ # Get item from queue
+ parent_id, parent_item = asset_items_queue.popleft()
+ # Skip if there are no children
+ children_ids = asset_ids_by_parents[parent_id]
+ if not children_ids:
+ continue
+
+ # Go through current children of parent item
+ # - find out items that were deleted and skip creation of already
+ # existing items
+ for row in reversed(range(parent_item.rowCount())):
+ child_item = parent_item.child(row, 0)
+ asset_id = child_item.data(ASSET_ID_ROLE)
+ # Remove item that is not available
+ if asset_id not in children_ids:
+ if asset_id in removed_asset_ids:
+ # Remove and destroy row
+ parent_item.removeRow(row)
+ else:
+ # Just take the row from parent without destroying
+ parent_item.takeRow(row)
+ continue
+
+ # Remove asset id from `children_ids` set
+ # - is used as set for creation of "new items"
+ children_ids.remove(asset_id)
+ # Add existing children to queue
+ asset_items_queue.append((asset_id, child_item))
+
+ new_items = []
+ for asset_id in children_ids:
+ # Look for item in cache (maybe parent changed)
+ item = self._items_by_asset_id.get(asset_id)
+ # Create new item if was not found
+ if item is None:
+ item = QtGui.QStandardItem()
+ item.setEditable(False)
+ item.setData(asset_id, ASSET_ID_ROLE)
+ self._items_by_asset_id[asset_id] = item
+ new_items.append(item)
+ # Add item to queue
+ asset_items_queue.append((asset_id, item))
+
+ if new_items:
+ parent_item.appendRows(new_items)
+
+ # Remove cache of removed items
+ for asset_id in removed_asset_ids:
+ self._items_by_asset_id.pop(asset_id)
+ if asset_id in self._items_with_color_by_id:
+ self._items_with_color_by_id.pop(asset_id)
+
+ # Refresh data
+ # - all items refresh all data except id
+ for asset_id, item in self._items_by_asset_id.items():
+ asset_doc = asset_docs_by_id[asset_id]
+
+ asset_name = asset_doc["name"]
+ if item.data(ASSET_NAME_ROLE) != asset_name:
+ item.setData(asset_name, ASSET_NAME_ROLE)
+
+ asset_data = asset_doc.get("data") or {}
+ asset_label = asset_data.get("label") or asset_name
+ if item.data(ASSET_LABEL_ROLE) != asset_label:
+ item.setData(asset_label, QtCore.Qt.DisplayRole)
+ item.setData(asset_label, ASSET_LABEL_ROLE)
+
+ icon_color = asset_data.get("color") or style.colors.default
+ icon_name = asset_data.get("icon")
+ if not icon_name:
+ # Use default icons if no custom one is specified.
+ # If it has children show a full folder, otherwise
+ # show an open folder
+ if item.rowCount() > 0:
+ icon_name = "folder"
+ else:
+ icon_name = "folder-o"
+
+ try:
+ # font-awesome key
+ full_icon_name = "fa.{0}".format(icon_name)
+ icon = qtawesome.icon(full_icon_name, color=icon_color)
+ item.setData(icon, QtCore.Qt.DecorationRole)
+
+ except Exception:
+ pass
+
+ self.refreshed.emit(bool(self._items_by_asset_id))
+
+ self._stop_fetch_thread()
+
+ def _threaded_fetch(self):
+ asset_docs = self._fetch_asset_docs()
+ if not self._refreshing:
+ return
+
+ self._doc_payload = asset_docs
+
+ # Emit doc fetched only if was not stopped
+ self._doc_fetched.emit()
+
+ def _fetch_asset_docs(self):
+ if not self.dbcon.Session.get("AVALON_PROJECT"):
+ return []
+
+ project_doc = self.dbcon.find_one(
+ {"type": "project"},
+ {"_id": True}
+ )
+ if not project_doc:
+ return []
+
+ # Get all assets sorted by name
+ return list(self.dbcon.find(
+ {"type": "asset"},
+ self._asset_projection
+ ))
+
+ def _stop_fetch_thread(self):
+ self._refreshing = False
+ if self._doc_fetching_thread is not None:
+ while self._doc_fetching_thread.isRunning():
+ time.sleep(0.01)
+ self._doc_fetching_thread = None
+
+
+class AssetsWidget(QtWidgets.QWidget):
+ """Base widget to display a tree of assets with filter.
+
+ Assets have only one column and are sorted by name.
+
+ Refreshing of assets happens in thread so calling 'refresh' method
+ is not sequential. To capture moment when refreshing is finished listen
+ to 'refreshed' signal.
+
+ To capture selection changes listen to 'selection_changed' signal. It won't
+ send any information about new selection as it may be different based on
+ inheritance changes.
+
+ Args:
+ dbcon (AvalonMongoDB): Connection to avalon mongo db.
+ parent (QWidget): Parent Qt widget.
+ """
+
+ # on model refresh
+ refresh_triggered = QtCore.Signal()
+ refreshed = QtCore.Signal()
+ # on view selection change
+ selection_changed = QtCore.Signal()
+
+ def __init__(self, dbcon, parent=None):
+ super(AssetsWidget, self).__init__(parent=parent)
+
+ self.dbcon = dbcon
+
+ # Tree View
+ model = AssetModel(dbcon=self.dbcon, parent=self)
+ proxy = RecursiveSortFilterProxyModel()
+ proxy.setSourceModel(model)
+ proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
+ proxy.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
+
+ view = AssetsView(self)
+ view.setModel(proxy)
+
+ current_asset_icon = qtawesome.icon(
+ "fa.arrow-down", color=style.colors.light
+ )
+ current_asset_btn = QtWidgets.QPushButton(self)
+ current_asset_btn.setIcon(current_asset_icon)
+ current_asset_btn.setToolTip("Go to Asset from current Session")
+ # Hide by default
+ current_asset_btn.setVisible(False)
+
+ refresh_icon = qtawesome.icon("fa.refresh", color=style.colors.light)
+ refresh_btn = QtWidgets.QPushButton(self)
+ refresh_btn.setIcon(refresh_icon)
+ refresh_btn.setToolTip("Refresh items")
+
+ filter_input = PlaceholderLineEdit(self)
+ filter_input.setPlaceholderText("Filter assets..")
+
+ # Header
+ header_layout = QtWidgets.QHBoxLayout()
+ header_layout.addWidget(filter_input)
+ header_layout.addWidget(current_asset_btn)
+ header_layout.addWidget(refresh_btn)
+
+ # Layout
+ layout = QtWidgets.QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(4)
+ layout.addLayout(header_layout)
+ layout.addWidget(view)
+
+ # Signals/Slots
+ filter_input.textChanged.connect(self._on_filter_text_change)
+
+ selection_model = view.selectionModel()
+ selection_model.selectionChanged.connect(self._on_selection_change)
+ refresh_btn.clicked.connect(self.refresh)
+ current_asset_btn.clicked.connect(self.set_current_session_asset)
+ model.refreshed.connect(self._on_model_refresh)
+
+ self._current_asset_btn = current_asset_btn
+ self._model = model
+ self._proxy = proxy
+ self._view = view
+
+ self.model_selection = {}
+
+ @property
+ def refreshing(self):
+ return self._model.refreshing
+
+ def refresh(self):
+ self._refresh_model()
+
+ def stop_refresh(self):
+ self._model.stop_refresh()
+
+ def set_current_session_asset(self):
+ asset_name = self.dbcon.Session.get("AVALON_ASSET")
+ if asset_name:
+ self.select_asset_by_name(asset_name)
+
+ def set_current_asset_btn_visibility(self, visible=None):
+ """Hide set current asset button.
+
+ Not all tools support using of current context asset.
+ """
+ if visible is None:
+ visible = not self._current_asset_btn.isVisible()
+ self._current_asset_btn.setVisible(visible)
+
+ def select_asset(self, asset_id):
+ index = self._model.get_index_by_asset_id(asset_id)
+ new_index = self._proxy.mapFromSource(index)
+ self._select_indexes([new_index])
+
+ def select_asset_by_name(self, asset_name):
+ index = self._model.get_index_by_asset_name(asset_name)
+ new_index = self._proxy.mapFromSource(index)
+ self._select_indexes([new_index])
+
+ def activate_flick_charm(self):
+ self._view.activate_flick_charm()
+
+ def deactivate_flick_charm(self):
+ self._view.deactivate_flick_charm()
+
+ def _on_selection_change(self):
+ self.selection_changed.emit()
+
+ def _on_filter_text_change(self, new_text):
+ self._proxy.setFilterFixedString(new_text)
+
+ def _on_model_refresh(self, has_item):
+ self._proxy.sort(0)
+ self._set_loading_state(loading=False, empty=not has_item)
+ self.refreshed.emit()
+
+ def _refresh_model(self):
+ # Store selection
+ self._set_loading_state(loading=True, empty=True)
+
+ # Trigger signal before refresh is called
+ self.refresh_triggered.emit()
+ # Refresh model
+ self._model.refresh()
+
+ def _set_loading_state(self, loading, empty):
+ self._view.set_loading_state(loading, empty)
+
+ def _select_indexes(self, indexes):
+ valid_indexes = [
+ index
+ for index in indexes
+ if index.isValid()
+ ]
+ if not valid_indexes:
+ return
+
+ selection_model = self._view.selectionModel()
+ selection_model.clearSelection()
+
+ mode = selection_model.Select | selection_model.Rows
+ for index in valid_indexes:
+ self._view.expand(self._proxy.parent(index))
+ selection_model.select(index, mode)
+ self._view.setCurrentIndex(valid_indexes[0])
+
+
+class SingleSelectAssetsWidget(AssetsWidget):
+ """Single selection asset widget.
+
+ Contain single selection specific api methods.
+ """
+ def get_selected_asset_id(self):
+ """Currently selected asset id."""
+ selection_model = self._view.selectionModel()
+ indexes = selection_model.selectedRows()
+ for index in indexes:
+ return index.data(ASSET_ID_ROLE)
+ return None
+
+ def get_selected_asset_name(self):
+ """Currently selected asset name."""
+ selection_model = self._view.selectionModel()
+ indexes = selection_model.selectedRows()
+ for index in indexes:
+ return index.data(ASSET_NAME_ROLE)
+ return None
+
+
+class MultiSelectAssetsWidget(AssetsWidget):
+ """Multiselection asset widget.
+
+ Main purpose is for loader and library loader. If another tool would use
+ multiselection assets this widget should be split and loader's logic
+ separated.
+ """
+ def __init__(self, *args, **kwargs):
+ super(MultiSelectAssetsWidget, self).__init__(*args, **kwargs)
+ self._view.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection)
+
+ delegate = UnderlinesAssetDelegate()
+ self._view.setItemDelegate(delegate)
+ self._delegate = delegate
+
+ def get_selected_asset_ids(self):
+ """Currently selected asset ids."""
+ selection_model = self._view.selectionModel()
+ indexes = selection_model.selectedRows()
+ return [
+ index.data(ASSET_ID_ROLE)
+ for index in indexes
+ ]
+
+ def get_selected_asset_names(self):
+ """Currently selected asset names."""
+ selection_model = self._view.selectionModel()
+ indexes = selection_model.selectedRows()
+ return [
+ index.data(ASSET_NAME_ROLE)
+ for index in indexes
+ ]
+
+ def select_assets(self, asset_ids):
+ """Select assets by their ids.
+
+ Args:
+ asset_ids (list): List of asset ids.
+ """
+ indexes = self._model.get_indexes_by_asset_ids(asset_ids)
+ new_indexes = [
+ self._proxy.mapFromSource(index)
+ for index in indexes
+ ]
+ self._select_indexes(new_indexes)
+
+ def select_assets_by_name(self, asset_names):
+ """Select assets by their names.
+
+ Args:
+ asset_names (list): List of asset names.
+ """
+ indexes = self._model.get_indexes_by_asset_names(asset_names)
+ new_indexes = [
+ self._proxy.mapFromSource(index)
+ for index in indexes
+ ]
+ self._select_indexes(new_indexes)
+
+ def clear_underlines(self):
+ """Clear underlines in asset items."""
+ self._model.clear_underlines()
+
+ self._view.updateGeometries()
+
+ def set_underline_colors(self, colors_by_asset_id):
+ """Change underline colors for passed assets.
+
+ Args:
+ colors_by_asset_id (dict): Key is asset id and value is list
+ of underline colors.
+ """
+ self._model.set_underline_colors(colors_by_asset_id)
+ # Trigger repaint
+ self._view.updateGeometries()
diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py
index e87da7f0b4..ef1cd3cf5c 100644
--- a/openpype/tools/utils/host_tools.py
+++ b/openpype/tools/utils/host_tools.py
@@ -62,19 +62,18 @@ class HostToolsHelper:
save = True
workfiles_tool = self.get_workfiles_tool(parent)
- if use_context:
- context = {
- "asset": avalon.api.Session["AVALON_ASSET"],
- "silo": avalon.api.Session["AVALON_SILO"],
- "task": avalon.api.Session["AVALON_TASK"]
- }
- workfiles_tool.set_context(context)
+ workfiles_tool.set_save_enabled(save)
- if save:
- workfiles_tool.set_save_enabled(save)
+ if not workfiles_tool.isVisible():
+ workfiles_tool.show()
+
+ if use_context:
+ context = {
+ "asset": avalon.api.Session["AVALON_ASSET"],
+ "task": avalon.api.Session["AVALON_TASK"]
+ }
+ workfiles_tool.set_context(context)
- workfiles_tool.refresh()
- workfiles_tool.show()
# Pull window to the front.
workfiles_tool.raise_()
workfiles_tool.activateWindow()
@@ -109,23 +108,19 @@ class HostToolsHelper:
def get_creator_tool(self, parent):
"""Create, cache and return creator tool window."""
if self._creator_tool is None:
- from avalon.tools.creator.app import Window
+ from openpype.tools.creator import CreatorWindow
- creator_window = Window(parent=parent or self._parent)
+ creator_window = CreatorWindow(parent=parent or self._parent)
self._creator_tool = creator_window
return self._creator_tool
def show_creator(self, parent=None):
"""Show tool to create new instantes for publishing."""
- from avalon import style
-
creator_tool = self.get_creator_tool(parent)
creator_tool.refresh()
creator_tool.show()
- creator_tool.setStyleSheet(style.load_stylesheet())
-
# Pull window to the front.
creator_tool.raise_()
creator_tool.activateWindow()
diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py
index e2815f26e4..4626e35a93 100644
--- a/openpype/tools/utils/lib.py
+++ b/openpype/tools/utils/lib.py
@@ -445,6 +445,30 @@ class GroupsConfig:
return ordered_groups, subset_docs_without_group, subset_docs_by_group
+class DynamicQThread(QtCore.QThread):
+ """QThread which can run any function with argument and kwargs.
+
+ Args:
+ func (function): Function which will be called.
+ args (tuple): Arguments which will be passed to function.
+ kwargs (tuple): Keyword arguments which will be passed to function.
+ parent (QObject): Parent of thread.
+ """
+ def __init__(self, func, args=None, kwargs=None, parent=None):
+ super(DynamicQThread, self).__init__(parent)
+ if args is None:
+ args = tuple()
+ if kwargs is None:
+ kwargs = {}
+ self._func = func
+ self._args = args
+ self._kwargs = kwargs
+
+ def run(self):
+ """Execute the function with arguments."""
+ self._func(*self._args, **self._kwargs)
+
+
def create_qthread(func, *args, **kwargs):
class Thread(QtCore.QThread):
def run(self):
diff --git a/openpype/tools/utils/tasks_widget.py b/openpype/tools/utils/tasks_widget.py
index 9ffcde2885..513402b455 100644
--- a/openpype/tools/utils/tasks_widget.py
+++ b/openpype/tools/utils/tasks_widget.py
@@ -226,10 +226,6 @@ class TasksWidget(QtWidgets.QWidget):
self._tasks_model.refresh()
def set_asset_id(self, asset_id):
- # Asset deselected
- if asset_id is None:
- return
-
# Try and preserve the last selected task and reselect it
# after switching assets. If there's no currently selected
# asset keep whatever the "last selected" was prior to it.
diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py
index 15bcbeff90..493255071d 100644
--- a/openpype/tools/utils/widgets.py
+++ b/openpype/tools/utils/widgets.py
@@ -7,6 +7,7 @@ from Qt import QtWidgets, QtCore, QtGui
from avalon.vendor import qtawesome, qargparse
from avalon import style
+from openpype.style import get_objected_colors
from .models import AssetModel, RecursiveSortFilterProxyModel
from .views import AssetsView
@@ -15,6 +16,28 @@ from .delegates import AssetDelegate
log = logging.getLogger(__name__)
+class PlaceholderLineEdit(QtWidgets.QLineEdit):
+ """Set placeholder color of QLineEdit in Qt 5.12 and higher."""
+ def __init__(self, *args, **kwargs):
+ super(PlaceholderLineEdit, self).__init__(*args, **kwargs)
+ self._first_show = True
+
+ def showEvent(self, event):
+ super(PlaceholderLineEdit, self).showEvent(event)
+ if self._first_show:
+ self._first_show = False
+ filter_palette = self.palette()
+ if hasattr(filter_palette, "PlaceholderText"):
+ color_obj = get_objected_colors()["font"]
+ color = color_obj.get_qcolor()
+ color.setAlpha(67)
+ filter_palette.setColor(
+ filter_palette.PlaceholderText,
+ color
+ )
+ self.setPalette(filter_palette)
+
+
class AssetWidget(QtWidgets.QWidget):
"""A Widget to display a tree of assets with filter
diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py
index aa98e67158..a4b1717a1c 100644
--- a/openpype/tools/workfiles/app.py
+++ b/openpype/tools/workfiles/app.py
@@ -12,15 +12,12 @@ from avalon import io, api, pipeline
from openpype import style
from openpype.tools.utils.lib import (
- schedule, qt_app_context
+ schedule,
+ qt_app_context
)
-from openpype.tools.utils.widgets import AssetWidget
+from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget
from openpype.tools.utils.tasks_widget import TasksWidget
from openpype.tools.utils.delegates import PrettyTimeDelegate
-
-from .model import FilesModel
-from .view import FilesView
-
from openpype.lib import (
Anatomy,
get_workdir,
@@ -30,6 +27,9 @@ from openpype.lib import (
get_workfile_template_key
)
+from .model import FilesModel
+from .view import FilesView
+
log = logging.getLogger(__name__)
module = sys.modules[__name__]
@@ -59,20 +59,39 @@ class NameWindow(QtWidgets.QDialog):
# Set work file data for template formatting
asset_name = session["AVALON_ASSET"]
+ task_name = session["AVALON_TASK"]
project_doc = io.find_one(
{"type": "project"},
{
"name": True,
- "data.code": True
+ "data.code": True,
+ "config.tasks": True,
}
)
+ asset_doc = io.find_one(
+ {
+ "type": "asset",
+ "name": asset_name
+ },
+ {"data.tasks": True}
+ )
+
+ task_type = asset_doc["data"]["tasks"].get(task_name, {}).get("type")
+
+ project_task_types = project_doc["config"]["tasks"]
+ task_short = project_task_types.get(task_type, {}).get("short_name")
+
self.data = {
"project": {
"name": project_doc["name"],
"code": project_doc["data"].get("code")
},
"asset": asset_name,
- "task": session["AVALON_TASK"],
+ "task": {
+ "name": task_name,
+ "type": task_type,
+ "short": task_short,
+ },
"version": 1,
"user": getpass.getuser(),
"comment": "",
@@ -326,7 +345,8 @@ class FilesWidget(QtWidgets.QWidget):
super(FilesWidget, self).__init__(parent=parent)
# Setup
- self._asset = None
+ self._asset_id = None
+ self._asset_doc = None
self._task_name = None
self._task_type = None
@@ -422,15 +442,17 @@ class FilesWidget(QtWidgets.QWidget):
self.btn_browse = btn_browse
self.btn_save = btn_save
- def set_asset_task(self, asset, task_name, task_type):
- self._asset = asset
+ def set_asset_task(self, asset_id, task_name, task_type):
+ if asset_id != self._asset_id:
+ self._asset_doc = None
+ self._asset_id = asset_id
self._task_name = task_name
self._task_type = task_type
# Define a custom session so we can query the work root
# for a "Work area" that is not our current Session.
# This way we can browse it even before we enter it.
- if self._asset and self._task_name and self._task_type:
+ if self._asset_id and self._task_name and self._task_type:
session = self._get_session()
self.root = self.host.work_root(session)
self.files_model.set_root(self.root)
@@ -446,6 +468,14 @@ class FilesWidget(QtWidgets.QWidget):
# Manually trigger file selection
self.on_file_select()
+ def _get_asset_doc(self):
+ if self._asset_id is None:
+ return None
+
+ if self._asset_doc is None:
+ self._asset_doc = io.find_one({"_id": self._asset_id})
+ return self._asset_doc
+
def _get_session(self):
"""Return a modified session for the current asset and task"""
@@ -457,7 +487,7 @@ class FilesWidget(QtWidgets.QWidget):
)
changes = pipeline.compute_session_changes(
session,
- asset=self._asset,
+ asset=self._get_asset_doc(),
task=self._task_name,
template_key=self.template_key
)
@@ -471,7 +501,7 @@ class FilesWidget(QtWidgets.QWidget):
session = api.Session.copy()
changes = pipeline.compute_session_changes(
session,
- asset=self._asset,
+ asset=self._get_asset_doc(),
task=self._task_name,
template_key=self.template_key
)
@@ -481,7 +511,7 @@ class FilesWidget(QtWidgets.QWidget):
return
api.update_current_task(
- asset=self._asset,
+ asset=self._get_asset_doc(),
task=self._task_name,
template_key=self.template_key
)
@@ -628,7 +658,9 @@ class FilesWidget(QtWidgets.QWidget):
self._enter_session() # Make sure we are in the right session
self.host.save_file(file_path)
- self.set_asset_task(self._asset, self._task_name, self._task_type)
+ self.set_asset_task(
+ self._asset_id, self._task_name, self._task_type
+ )
pipeline.emit("after.workfile.save", [file_path])
@@ -654,7 +686,7 @@ class FilesWidget(QtWidgets.QWidget):
session = api.Session.copy()
changes = pipeline.compute_session_changes(
session,
- asset=self._asset,
+ asset=self._get_asset_doc(),
task=self._task_name,
template_key=self.template_key
)
@@ -679,7 +711,7 @@ class FilesWidget(QtWidgets.QWidget):
# Force a full to the asset as opposed to just self.refresh() so
# that it will actually check again whether the Work directory exists
- self.set_asset_task(self._asset, self._task_name, self._task_type)
+ self.set_asset_task(self._asset_id, self._task_name, self._task_type)
def refresh(self):
"""Refresh listed files for current selection in the interface"""
@@ -775,10 +807,10 @@ class SidePanelWidget(QtWidgets.QWidget):
self.on_note_change()
self.save_clicked.emit()
- def set_context(self, asset_doc, task_name, filepath, workfile_doc):
+ def set_context(self, asset_id, task_name, filepath, workfile_doc):
# Check if asset, task and file are selected
# NOTE workfile document is not requirement
- enabled = bool(asset_doc) and bool(task_name) and bool(filepath)
+ enabled = bool(asset_id) and bool(task_name) and bool(filepath)
self.details_input.setEnabled(enabled)
self.note_input.setEnabled(enabled)
@@ -856,7 +888,7 @@ class Window(QtWidgets.QMainWindow):
home_page_widget = QtWidgets.QWidget(pages_widget)
home_body_widget = QtWidgets.QWidget(home_page_widget)
- assets_widget = AssetWidget(io, parent=home_body_widget)
+ assets_widget = SingleSelectAssetsWidget(io, parent=home_body_widget)
assets_widget.set_current_asset_btn_visibility(True)
tasks_widget = TasksWidget(io, home_body_widget)
@@ -884,14 +916,21 @@ class Window(QtWidgets.QMainWindow):
# the files widget has a filter field which tasks does not.
tasks_widget.setContentsMargins(0, 32, 0, 0)
+ # Set context after asset widget is refreshed
+ # - to do so it is necessary to wait until refresh is done
+ set_context_timer = QtCore.QTimer()
+ set_context_timer.setInterval(100)
+
# Connect signals
- assets_widget.current_changed.connect(self.on_asset_changed)
+ set_context_timer.timeout.connect(self._on_context_set_timeout)
+ assets_widget.selection_changed.connect(self.on_asset_changed)
tasks_widget.task_changed.connect(self.on_task_changed)
files_widget.file_selected.connect(self.on_file_select)
files_widget.workfile_created.connect(self.on_workfile_create)
files_widget.file_opened.connect(self._on_file_opened)
side_panel.save_clicked.connect(self.on_side_panel_save)
+ self._set_context_timer = set_context_timer
self.home_page_widget = home_page_widget
self.pages_widget = pages_widget
self.home_body_widget = home_body_widget
@@ -908,11 +947,13 @@ class Window(QtWidgets.QMainWindow):
self.resize(1200, 600)
self._first_show = True
+ self._context_to_set = None
def showEvent(self, event):
super(Window, self).showEvent(event)
if self._first_show:
self._first_show = False
+ self.refresh()
self.setStyleSheet(style.load_stylesheet())
def keyPressEvent(self, event):
@@ -936,21 +977,17 @@ class Window(QtWidgets.QMainWindow):
schedule(self._on_asset_changed, 50, channel="mongo")
def on_file_select(self, filepath):
- asset_docs = self.assets_widget.get_selected_assets()
- asset_doc = None
- if asset_docs:
- asset_doc = asset_docs[0]
-
+ asset_id = self.assets_widget.get_selected_asset_id()
task_name = self.tasks_widget.get_selected_task_name()
workfile_doc = None
- if asset_doc and task_name and filepath:
+ if asset_id and task_name and filepath:
filename = os.path.split(filepath)[1]
workfile_doc = get_workfile_doc(
- asset_doc["_id"], task_name, filename, io
+ asset_id, task_name, filename, io
)
self.side_panel.set_context(
- asset_doc, task_name, filepath, workfile_doc
+ asset_id, task_name, filepath, workfile_doc
)
def on_workfile_create(self, filepath):
@@ -972,14 +1009,13 @@ class Window(QtWidgets.QMainWindow):
if filepath is None:
filepath = self.files_widget._get_selected_filepath()
task_name = self.tasks_widget.get_selected_task_name()
- asset_docs = self.assets_widget.get_selected_assets()
- if not task_name or not asset_docs or not filepath:
+ asset_id = self.assets_widget.get_selected_asset_id()
+ if not task_name or not asset_id or not filepath:
return
- asset_doc = asset_docs[0]
filename = os.path.split(filepath)[1]
return get_workfile_doc(
- asset_doc["_id"], task_name, filename, io
+ asset_id, task_name, filename, io
)
def _create_workfile_doc(self, filepath, force=False):
@@ -989,62 +1025,68 @@ class Window(QtWidgets.QMainWindow):
if not workfile_doc:
workdir, filename = os.path.split(filepath)
- asset_docs = self.assets_widget.get_selected_assets()
- asset_doc = asset_docs[0]
+ asset_id = self.assets_widget.get_selected_asset_id()
+ asset_doc = io.find_one({"_id": asset_id})
task_name = self.tasks_widget.get_selected_task_name()
create_workfile_doc(asset_doc, task_name, filename, workdir, io)
- def set_context(self, context):
- if "asset" in context:
- asset = context["asset"]
- asset_document = io.find_one(
- {
- "name": asset,
- "type": "asset"
- },
- {"_id": 1}
- ) or {}
-
- # Select the asset
- self.assets_widget.select_assets([asset], expand=True)
-
- self.tasks_widget.set_asset_id(asset_document.get("_id"))
-
- if "task" in context:
- self.tasks_widget.select_task_name(context["task"])
-
def refresh(self):
# Refresh asset widget
self.assets_widget.refresh()
self._on_task_changed()
+ def set_context(self, context):
+ self._context_to_set = context
+ self._set_context_timer.start()
+
+ def _on_context_set_timeout(self):
+ if self._context_to_set is None:
+ self._set_context_timer.stop()
+ return
+
+ if self.assets_widget.refreshing:
+ return
+
+ self._context_to_set, context = None, self._context_to_set
+ if "asset" in context:
+ asset_doc = io.find_one(
+ {
+ "name": context["asset"],
+ "type": "asset"
+ },
+ {"_id": 1}
+ ) or {}
+ asset_id = asset_doc.get("_id")
+ # Select the asset
+ self.assets_widget.select_asset(asset_id)
+ self.tasks_widget.set_asset_id(asset_id)
+
+ if "task" in context:
+ self.tasks_widget.select_task_name(context["task"])
+
def _on_asset_changed(self):
- asset = self.assets_widget.get_selected_assets() or None
- asset_id = None
- if not asset:
+ asset_id = self.assets_widget.get_selected_asset_id()
+ if asset_id:
+ self.tasks_widget.setEnabled(True)
+ else:
# Force disable the other widgets if no
# active selection
self.tasks_widget.setEnabled(False)
self.files_widget.setEnabled(False)
- else:
- asset = asset[0]
- asset_id = asset.get("_id")
- self.tasks_widget.setEnabled(True)
self.tasks_widget.set_asset_id(asset_id)
def _on_task_changed(self):
- asset = self.assets_widget.get_selected_assets() or None
- if asset is not None:
- asset = asset[0]
+ asset_id = self.assets_widget.get_selected_asset_id()
task_name = self.tasks_widget.get_selected_task_name()
task_type = self.tasks_widget.get_selected_task_type()
- self.tasks_widget.setEnabled(bool(asset))
+ asset_is_valid = asset_id is not None
+ self.tasks_widget.setEnabled(asset_is_valid)
- self.files_widget.setEnabled(all([bool(task_name), bool(asset)]))
- self.files_widget.set_asset_task(asset, task_name, task_type)
+ self.files_widget.setEnabled(bool(task_name) and asset_is_valid)
+ self.files_widget.set_asset_task(asset_id, task_name, task_type)
self.files_widget.refresh()
diff --git a/openpype/version.py b/openpype/version.py
index ef4bbe505b..cfcaddf2e1 100644
--- a/openpype/version.py
+++ b/openpype/version.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
-__version__ = "3.6.2-nightly.1"
+__version__ = "3.7.0-nightly.1"
diff --git a/pyproject.toml b/pyproject.toml
index cfe7422d49..d0131d7b55 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
-version = "3.6.2-nightly.1" # OpenPype
+version = "3.7.0-nightly.1" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team "]
license = "MIT License"
diff --git a/repos/avalon-core b/repos/avalon-core
index 7e5efd6885..9499f6517a 160000
--- a/repos/avalon-core
+++ b/repos/avalon-core
@@ -1 +1 @@
-Subproject commit 7e5efd6885330d84bb8495975bcab84df49bfa3d
+Subproject commit 9499f6517a1ff2d3bf94c5d34c0aece146734760
diff --git a/tests/unit/igniter/test_bootstrap_repos.py b/tests/unit/igniter/test_bootstrap_repos.py
index 740a71a5ce..d6e861c262 100644
--- a/tests/unit/igniter/test_bootstrap_repos.py
+++ b/tests/unit/igniter/test_bootstrap_repos.py
@@ -140,7 +140,7 @@ def test_search_string_for_openpype_version(printer):
]
for ver_string in strings:
printer(f"testing {ver_string[0]} should be {ver_string[1]}")
- assert OpenPypeVersion.version_in_str(ver_string[0])[0] == \
+ assert OpenPypeVersion.version_in_str(ver_string[0]) == \
ver_string[1]
diff --git a/tools/run_mongo.ps1 b/tools/run_mongo.ps1
index 32f6cfed17..f6fa37207d 100644
--- a/tools/run_mongo.ps1
+++ b/tools/run_mongo.ps1
@@ -113,7 +113,7 @@ $port = 2707
# path to database
$dbpath = (Get-Item $openpype_root).parent.FullName + "\mongo_db_data"
-$preferred_version = "4.0"
+$preferred_version = "5.0"
$mongoPath = Find-Mongo $preferred_version
Write-Host ">>> " -NoNewLine -ForegroundColor Green
diff --git a/website/docs/admin_openpype_commands.md b/website/docs/admin_openpype_commands.md
index 7a46ee7906..0831cf4f5a 100644
--- a/website/docs/admin_openpype_commands.md
+++ b/website/docs/admin_openpype_commands.md
@@ -32,7 +32,10 @@ For more information [see here](admin_use#run-openpype).
| Command | Description | Arguments |
| --- | --- |: --- :|
-| tray | Launch OpenPype Tray. | [📑](#tray-arguments)
+| contextselection | Open Context selection dialog. | |
+| module | Run command line arguments for modules. | |
+| repack-version | Tool to re-create version zip. | [📑](#repack-version-arguments) |
+| tray | Launch OpenPype Tray. | [📑](#tray-arguments)
| eventserver | This should be ideally used by system service (such as systemd or upstart on linux and window service). | [📑](#eventserver-arguments) |
| launch | Launch application in Pype environment. | [📑](#launch-arguments) |
| publish | Pype takes JSON from provided path and use it to publish data in it. | [📑](#publish-arguments) |
@@ -156,4 +159,10 @@ openpypeconsole settings
`standalonepublisher` has no command-line arguments.
```shell
openpype_console standalonepublisher
-```
\ No newline at end of file
+```
+
+### `repack-version` arguments {#repack-version-arguments}
+Takes path to unzipped and possibly modified OpenPype version. Files will be
+zipped, checksums recalculated and version will be determined by folder name
+(and written to `version.py`).
+
diff --git a/website/docs/admin_settings_project_anatomy.md b/website/docs/admin_settings_project_anatomy.md
index 54023d468f..30784686e2 100644
--- a/website/docs/admin_settings_project_anatomy.md
+++ b/website/docs/admin_settings_project_anatomy.md
@@ -57,7 +57,9 @@ We have a few required anatomy templates for OpenPype to work properly, however
| `project[code]` | Project's code |
| `hierarchy` | All hierarchical parents as subfolders |
| `asset` | Name of asset or shot |
-| `task` | Name of task |
+| `task[name]` | Name of task |
+| `task[type]` | Type of task |
+| `task[short]` | Shortname of task |
| `version` | Version number |
| `subset` | Subset name |
| `family` | Main family name |