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 |