Merge remote-tracking branch 'origin/develop' into feature/royalrender-integration

This commit is contained in:
Ondřej Samohel 2021-10-29 17:26:01 +02:00
commit 96a6fb9d4a
No known key found for this signature in database
GPG key ID: 02376E18990A97C6
101 changed files with 6317 additions and 1819 deletions

View file

@ -1,8 +1,36 @@
# Changelog
## [3.6.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.5.0...HEAD)
**🚀 Enhancements**
- Tools: Experimental tools [\#2167](https://github.com/pypeclub/OpenPype/pull/2167)
- Loader: Refactor and use OpenPype stylesheets [\#2166](https://github.com/pypeclub/OpenPype/pull/2166)
- Add loader for linked smart objects in photoshop [\#2149](https://github.com/pypeclub/OpenPype/pull/2149)
- Burnins: DNxHD profiles handling [\#2142](https://github.com/pypeclub/OpenPype/pull/2142)
- Tools: Single access point for host tools [\#2139](https://github.com/pypeclub/OpenPype/pull/2139)
**🐛 Bug fixes**
- MacOS: Launching of applications may cause Permissions error [\#2175](https://github.com/pypeclub/OpenPype/pull/2175)
- Blender: Fix 'Deselect All' with object not in 'Object Mode' [\#2163](https://github.com/pypeclub/OpenPype/pull/2163)
- Tools: Stylesheets are applied after tool show [\#2161](https://github.com/pypeclub/OpenPype/pull/2161)
- Maya: Collect render - fix UNC path support 🐛 [\#2158](https://github.com/pypeclub/OpenPype/pull/2158)
- Maya: Fix hotbox broken by scriptsmenu [\#2151](https://github.com/pypeclub/OpenPype/pull/2151)
- Ftrack: Ignore save warnings exception in Prepare project action [\#2150](https://github.com/pypeclub/OpenPype/pull/2150)
- Loader thumbnails with smooth edges [\#2147](https://github.com/pypeclub/OpenPype/pull/2147)
- Added validator for source files for Standalone Publisher [\#2138](https://github.com/pypeclub/OpenPype/pull/2138)
**Merged pull requests:**
- Bump pillow from 8.2.0 to 8.3.2 [\#2162](https://github.com/pypeclub/OpenPype/pull/2162)
- Bump axios from 0.21.1 to 0.21.4 in /website [\#2059](https://github.com/pypeclub/OpenPype/pull/2059)
## [3.5.0](https://github.com/pypeclub/OpenPype/tree/3.5.0) (2021-10-17)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.4.1...3.5.0)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.5.0-nightly.8...3.5.0)
**Deprecated:**
@ -66,10 +94,6 @@
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.1-nightly.1...3.4.1)
**🆕 New features**
- Settings: Flag project as deactivated and hide from tools' view [\#2008](https://github.com/pypeclub/OpenPype/pull/2008)
**🚀 Enhancements**
- General: Startup validations [\#2054](https://github.com/pypeclub/OpenPype/pull/2054)
@ -82,8 +106,6 @@
- Loader & Library loader: Use tools from OpenPype [\#2038](https://github.com/pypeclub/OpenPype/pull/2038)
- Adding predefined project folders creation in PM [\#2030](https://github.com/pypeclub/OpenPype/pull/2030)
- WebserverModule: Removed interface of webserver module [\#2028](https://github.com/pypeclub/OpenPype/pull/2028)
- TimersManager: Removed interface of timers manager [\#2024](https://github.com/pypeclub/OpenPype/pull/2024)
- Feature Maya import asset from scene inventory [\#2018](https://github.com/pypeclub/OpenPype/pull/2018)
**🐛 Bug fixes**
@ -101,21 +123,10 @@
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.4.0-nightly.6...3.4.0)
**🆕 New features**
- Nuke: Compatibility with Nuke 13 [\#2003](https://github.com/pypeclub/OpenPype/pull/2003)
**🚀 Enhancements**
- Added possibility to configure of synchronization of workfile version… [\#2041](https://github.com/pypeclub/OpenPype/pull/2041)
- General: Task types in profiles [\#2036](https://github.com/pypeclub/OpenPype/pull/2036)
- Console interpreter: Handle invalid sizes on initialization [\#2022](https://github.com/pypeclub/OpenPype/pull/2022)
- Ftrack: Show OpenPype versions in event server status [\#2019](https://github.com/pypeclub/OpenPype/pull/2019)
- General: Staging icon [\#2017](https://github.com/pypeclub/OpenPype/pull/2017)
- Ftrack: Sync to avalon actions have jobs [\#2015](https://github.com/pypeclub/OpenPype/pull/2015)
- Modules: Connect method is not required [\#2009](https://github.com/pypeclub/OpenPype/pull/2009)
- Settings UI: Number with configurable steps [\#2001](https://github.com/pypeclub/OpenPype/pull/2001)
- Moving project folder structure creation out of ftrack module \#1989 [\#1996](https://github.com/pypeclub/OpenPype/pull/1996)
**🐛 Bug fixes**
@ -124,12 +135,6 @@
- Nuke: typo on a button [\#2034](https://github.com/pypeclub/OpenPype/pull/2034)
- Hiero: Fix "none" named tags [\#2033](https://github.com/pypeclub/OpenPype/pull/2033)
- FFmpeg: Subprocess arguments as list [\#2032](https://github.com/pypeclub/OpenPype/pull/2032)
- General: Fix Python 2 breaking line [\#2016](https://github.com/pypeclub/OpenPype/pull/2016)
- Bugfix/webpublisher task type [\#2006](https://github.com/pypeclub/OpenPype/pull/2006)
### 📖 Documentation
- Documentation: Ftrack launch argsuments update [\#2014](https://github.com/pypeclub/OpenPype/pull/2014)
## [3.3.1](https://github.com/pypeclub/OpenPype/tree/3.3.1) (2021-08-20)

View file

@ -158,6 +158,25 @@ def publish(debug, paths, targets):
PypeCommands.publish(list(paths), targets)
@main.command()
@click.argument("path")
@click.option("-d", "--debug", is_flag=True, help="Print debug messages")
@click.option("-h", "--host", help="Host")
@click.option("-u", "--user", help="User email address")
@click.option("-p", "--project", help="Project")
@click.option("-t", "--targets", help="Targets", default=None,
multiple=True)
def remotepublishfromapp(debug, project, path, host, targets=None, user=None):
"""Start CLI publishing.
Publish collects json from paths provided as an argument.
More than one path is allowed.
"""
if debug:
os.environ['OPENPYPE_DEBUG'] = '3'
PypeCommands.remotepublishfromapp(project, path, host, user,
targets=targets)
@main.command()
@click.argument("path")
@click.option("-d", "--debug", is_flag=True, help="Print debug messages")

View file

@ -13,7 +13,7 @@ class LaunchFoundryAppsWindows(PreLaunchHook):
# Should be as last hook because must change launch arguments to string
order = 1000
app_groups = ["nuke", "nukex", "hiero", "nukestudio"]
app_groups = ["nuke", "nukex", "hiero", "nukestudio", "photoshop"]
platforms = ["windows"]
def execute(self):

View file

@ -95,6 +95,30 @@ def get_local_collection_with_name(name):
return None
def deselect_all():
"""Deselect all objects in the scene.
Blender gives context error if trying to deselect object that it isn't
in object mode.
"""
modes = []
active = bpy.context.view_layer.objects.active
for obj in bpy.data.objects:
if obj.mode != 'OBJECT':
modes.append((obj, obj.mode))
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
for p in modes:
bpy.context.view_layer.objects.active = p[0]
bpy.ops.object.mode_set(mode=p[1])
bpy.context.view_layer.objects.active = active
class Creator(PypeCreatorMixin, blender.Creator):
pass

View file

@ -3,11 +3,12 @@
import bpy
from avalon import api
from avalon.blender import lib
import openpype.hosts.blender.api.plugin
from avalon.blender import lib, ops
from avalon.blender.pipeline import AVALON_INSTANCES
from openpype.hosts.blender.api import plugin
class CreateCamera(openpype.hosts.blender.api.plugin.Creator):
class CreateCamera(plugin.Creator):
"""Polygonal static geometry"""
name = "cameraMain"
@ -16,17 +17,46 @@ class CreateCamera(openpype.hosts.blender.api.plugin.Creator):
icon = "video-camera"
def process(self):
""" Run the creator on Blender main thread"""
mti = ops.MainThreadItem(self._process)
ops.execute_in_main_thread(mti)
def _process(self):
# Get Instance Containter or create it if it does not exist
instances = bpy.data.collections.get(AVALON_INSTANCES)
if not instances:
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
bpy.context.scene.collection.children.link(instances)
# Create instance object
asset = self.data["asset"]
subset = self.data["subset"]
name = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
collection = bpy.data.collections.new(name=name)
bpy.context.scene.collection.children.link(collection)
name = plugin.asset_name(asset, subset)
camera = bpy.data.cameras.new(subset)
camera_obj = bpy.data.objects.new(subset, camera)
instances.objects.link(camera_obj)
asset_group = bpy.data.objects.new(name=name, object_data=None)
asset_group.empty_display_type = 'SINGLE_ARROW'
instances.objects.link(asset_group)
self.data['task'] = api.Session.get('AVALON_TASK')
lib.imprint(collection, self.data)
print(f"self.data: {self.data}")
lib.imprint(asset_group, self.data)
if (self.options or {}).get("useSelection"):
for obj in lib.get_selection():
collection.objects.link(obj)
bpy.context.view_layer.objects.active = asset_group
selected = lib.get_selection()
for obj in selected:
obj.select_set(True)
selected.append(asset_group)
bpy.ops.object.parent_set(keep_transform=True)
else:
plugin.deselect_all()
camera_obj.select_set(True)
asset_group.select_set(True)
bpy.context.view_layer.objects.active = asset_group
bpy.ops.object.parent_set(keep_transform=True)
return collection
return asset_group

View file

@ -47,7 +47,7 @@ class CacheModelLoader(plugin.AssetLoader):
bpy.data.objects.remove(empty)
def _process(self, libpath, asset_group, group_name):
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
collection = bpy.context.view_layer.active_layer_collection.collection
@ -109,7 +109,7 @@ class CacheModelLoader(plugin.AssetLoader):
avalon_info = obj[AVALON_PROPERTY]
avalon_info.update({"container_name": group_name})
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
return objects

View file

@ -1,247 +0,0 @@
"""Load a camera asset in Blender."""
import logging
from pathlib import Path
from pprint import pformat
from typing import Dict, List, Optional
from avalon import api, blender
import bpy
import openpype.hosts.blender.api.plugin
logger = logging.getLogger("openpype").getChild("blender").getChild("load_camera")
class BlendCameraLoader(openpype.hosts.blender.api.plugin.AssetLoader):
"""Load a camera from a .blend file.
Warning:
Loading the same asset more then once is not properly supported at the
moment.
"""
families = ["camera"]
representations = ["blend"]
label = "Link Camera"
icon = "code-fork"
color = "orange"
def _remove(self, objects, lib_container):
for obj in list(objects):
bpy.data.cameras.remove(obj.data)
bpy.data.collections.remove(bpy.data.collections[lib_container])
def _process(self, libpath, lib_container, container_name, actions):
relative = bpy.context.preferences.filepaths.use_relative_paths
with bpy.data.libraries.load(
libpath, link=True, relative=relative
) as (_, data_to):
data_to.collections = [lib_container]
scene = bpy.context.scene
scene.collection.children.link(bpy.data.collections[lib_container])
camera_container = scene.collection.children[lib_container].make_local()
objects_list = []
for obj in camera_container.objects:
local_obj = obj.make_local()
local_obj.data.make_local()
if not local_obj.get(blender.pipeline.AVALON_PROPERTY):
local_obj[blender.pipeline.AVALON_PROPERTY] = dict()
avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY]
avalon_info.update({"container_name": container_name})
if actions[0] is not None:
if local_obj.animation_data is None:
local_obj.animation_data_create()
local_obj.animation_data.action = actions[0]
if actions[1] is not None:
if local_obj.data.animation_data is None:
local_obj.data.animation_data_create()
local_obj.data.animation_data.action = actions[1]
objects_list.append(local_obj)
camera_container.pop(blender.pipeline.AVALON_PROPERTY)
bpy.ops.object.select_all(action='DESELECT')
return objects_list
def process_asset(
self, context: dict, name: str, namespace: Optional[str] = None,
options: Optional[Dict] = None
) -> Optional[List]:
"""
Arguments:
name: Use pre-defined name
namespace: Use pre-defined namespace
context: Full parenthood of representation to load
options: Additional settings dictionary
"""
libpath = self.fname
asset = context["asset"]["name"]
subset = context["subset"]["name"]
lib_container = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
container_name = openpype.hosts.blender.api.plugin.asset_name(
asset, subset, namespace
)
container = bpy.data.collections.new(lib_container)
container.name = container_name
blender.pipeline.containerise_existing(
container,
name,
namespace,
context,
self.__class__.__name__,
)
container_metadata = container.get(
blender.pipeline.AVALON_PROPERTY)
container_metadata["libpath"] = libpath
container_metadata["lib_container"] = lib_container
objects_list = self._process(
libpath, lib_container, container_name, (None, None))
# Save the list of objects in the metadata container
container_metadata["objects"] = objects_list
nodes = list(container.objects)
nodes.append(container)
self[:] = nodes
return nodes
def update(self, container: Dict, representation: Dict):
"""Update the loaded asset.
This will remove all objects of the current collection, load the new
ones and add them to the collection.
If the objects of the collection are used in another collection they
will not be removed, only unlinked. Normally this should not be the
case though.
Warning:
No nested collections are supported at the moment!
"""
collection = bpy.data.collections.get(
container["objectName"]
)
libpath = Path(api.get_representation_path(representation))
extension = libpath.suffix.lower()
logger.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
)
assert collection, (
f"The asset is not loaded: {container['objectName']}"
)
assert not (collection.children), (
"Nested collections are not supported."
)
assert libpath, (
"No existing library file found for {container['objectName']}"
)
assert libpath.is_file(), (
f"The file doesn't exist: {libpath}"
)
assert extension in openpype.hosts.blender.api.plugin.VALID_EXTENSIONS, (
f"Unsupported file: {libpath}"
)
collection_metadata = collection.get(
blender.pipeline.AVALON_PROPERTY)
collection_libpath = collection_metadata["libpath"]
objects = collection_metadata["objects"]
lib_container = collection_metadata["lib_container"]
normalized_collection_libpath = (
str(Path(bpy.path.abspath(collection_libpath)).resolve())
)
normalized_libpath = (
str(Path(bpy.path.abspath(str(libpath))).resolve())
)
logger.debug(
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
normalized_collection_libpath,
normalized_libpath,
)
if normalized_collection_libpath == normalized_libpath:
logger.info("Library already loaded, not updating...")
return
camera = objects[0]
camera_action = None
camera_data_action = None
if camera.animation_data and camera.animation_data.action:
camera_action = camera.animation_data.action
if camera.data.animation_data and camera.data.animation_data.action:
camera_data_action = camera.data.animation_data.action
actions = (camera_action, camera_data_action)
self._remove(objects, lib_container)
objects_list = self._process(
str(libpath), lib_container, collection.name, actions)
# Save the list of objects in the metadata container
collection_metadata["objects"] = objects_list
collection_metadata["libpath"] = str(libpath)
collection_metadata["representation"] = str(representation["_id"])
bpy.ops.object.select_all(action='DESELECT')
def remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene.
Arguments:
container (openpype:container-1.0): Container to remove,
from `host.ls()`.
Returns:
bool: Whether the container was deleted.
Warning:
No nested collections are supported at the moment!
"""
collection = bpy.data.collections.get(
container["objectName"]
)
if not collection:
return False
assert not (collection.children), (
"Nested collections are not supported."
)
collection_metadata = collection.get(
blender.pipeline.AVALON_PROPERTY)
objects = collection_metadata["objects"]
lib_container = collection_metadata["lib_container"]
self._remove(objects, lib_container)
bpy.data.collections.remove(collection)
return True

View file

@ -0,0 +1,252 @@
"""Load a camera asset in Blender."""
import logging
from pathlib import Path
from pprint import pformat
from typing import Dict, List, Optional
import bpy
from avalon import api
from avalon.blender.pipeline import AVALON_CONTAINERS
from avalon.blender.pipeline import AVALON_CONTAINER_ID
from avalon.blender.pipeline import AVALON_PROPERTY
from openpype.hosts.blender.api import plugin
logger = logging.getLogger("openpype").getChild(
"blender").getChild("load_camera")
class BlendCameraLoader(plugin.AssetLoader):
"""Load a camera from a .blend file.
Warning:
Loading the same asset more then once is not properly supported at the
moment.
"""
families = ["camera"]
representations = ["blend"]
label = "Link Camera (Blend)"
icon = "code-fork"
color = "orange"
def _remove(self, asset_group):
objects = list(asset_group.children)
for obj in objects:
if obj.type == 'CAMERA':
bpy.data.cameras.remove(obj.data)
def _process(self, libpath, asset_group, group_name):
with bpy.data.libraries.load(
libpath, link=True, relative=False
) as (data_from, data_to):
data_to.objects = data_from.objects
parent = bpy.context.scene.collection
empties = [obj for obj in data_to.objects if obj.type == 'EMPTY']
container = None
for empty in empties:
if empty.get(AVALON_PROPERTY):
container = empty
break
assert container, "No asset group found"
# Children must be linked before parents,
# otherwise the hierarchy will break
objects = []
nodes = list(container.children)
for obj in nodes:
obj.parent = asset_group
for obj in nodes:
objects.append(obj)
nodes.extend(list(obj.children))
objects.reverse()
for obj in objects:
parent.objects.link(obj)
for obj in objects:
local_obj = plugin.prepare_data(obj, group_name)
if local_obj.type != 'EMPTY':
plugin.prepare_data(local_obj.data, group_name)
if not local_obj.get(AVALON_PROPERTY):
local_obj[AVALON_PROPERTY] = dict()
avalon_info = local_obj[AVALON_PROPERTY]
avalon_info.update({"container_name": group_name})
objects.reverse()
bpy.data.orphans_purge(do_local_ids=False)
plugin.deselect_all()
return objects
def process_asset(
self, context: dict, name: str, namespace: Optional[str] = None,
options: Optional[Dict] = None
) -> Optional[List]:
"""
Arguments:
name: Use pre-defined name
namespace: Use pre-defined namespace
context: Full parenthood of representation to load
options: Additional settings dictionary
"""
libpath = self.fname
asset = context["asset"]["name"]
subset = context["subset"]["name"]
asset_name = plugin.asset_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.asset_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
if not avalon_container:
avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
bpy.context.scene.collection.children.link(avalon_container)
asset_group = bpy.data.objects.new(group_name, object_data=None)
asset_group.empty_display_type = 'SINGLE_ARROW'
avalon_container.objects.link(asset_group)
objects = self._process(libpath, asset_group, group_name)
bpy.context.scene.collection.objects.link(asset_group)
asset_group[AVALON_PROPERTY] = {
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
"name": name,
"namespace": namespace or '',
"loader": str(self.__class__.__name__),
"representation": str(context["representation"]["_id"]),
"libpath": libpath,
"asset_name": asset_name,
"parent": str(context["representation"]["parent"]),
"family": context["representation"]["context"]["family"],
"objectName": group_name
}
self[:] = objects
return objects
def exec_update(self, container: Dict, representation: Dict):
"""Update the loaded asset.
This will remove all children of the asset group, load the new ones
and add them as children of the group.
"""
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
libpath = Path(api.get_representation_path(representation))
extension = libpath.suffix.lower()
self.log.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
)
assert asset_group, (
f"The asset is not loaded: {container['objectName']}"
)
assert libpath, (
"No existing library file found for {container['objectName']}"
)
assert libpath.is_file(), (
f"The file doesn't exist: {libpath}"
)
assert extension in plugin.VALID_EXTENSIONS, (
f"Unsupported file: {libpath}"
)
metadata = asset_group.get(AVALON_PROPERTY)
group_libpath = metadata["libpath"]
normalized_group_libpath = (
str(Path(bpy.path.abspath(group_libpath)).resolve())
)
normalized_libpath = (
str(Path(bpy.path.abspath(str(libpath))).resolve())
)
self.log.debug(
"normalized_group_libpath:\n %s\nnormalized_libpath:\n %s",
normalized_group_libpath,
normalized_libpath,
)
if normalized_group_libpath == normalized_libpath:
self.log.info("Library already loaded, not updating...")
return
# Check how many assets use the same library
count = 0
for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects:
if obj.get(AVALON_PROPERTY).get('libpath') == group_libpath:
count += 1
mat = asset_group.matrix_basis.copy()
self._remove(asset_group)
# If it is the last object to use that library, remove it
if count == 1:
library = bpy.data.libraries.get(bpy.path.basename(group_libpath))
if library:
bpy.data.libraries.remove(library)
self._process(str(libpath), asset_group, object_name)
asset_group.matrix_basis = mat
metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"])
metadata["parent"] = str(representation["parent"])
def exec_remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene.
Arguments:
container (openpype:container-1.0): Container to remove,
from `host.ls()`.
Returns:
bool: Whether the container was deleted.
"""
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
libpath = asset_group.get(AVALON_PROPERTY).get('libpath')
# Check how many assets use the same library
count = 0
for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects:
if obj.get(AVALON_PROPERTY).get('libpath') == libpath:
count += 1
if not asset_group:
return False
self._remove(asset_group)
bpy.data.objects.remove(asset_group)
# If it is the last object to use that library, remove it
if count == 1:
library = bpy.data.libraries.get(bpy.path.basename(libpath))
bpy.data.libraries.remove(library)
return True

View file

@ -0,0 +1,218 @@
"""Load an asset in Blender from an Alembic file."""
from pathlib import Path
from pprint import pformat
from typing import Dict, List, Optional
import bpy
from avalon import api
from avalon.blender import lib
from avalon.blender.pipeline import AVALON_CONTAINERS
from avalon.blender.pipeline import AVALON_CONTAINER_ID
from avalon.blender.pipeline import AVALON_PROPERTY
from openpype.hosts.blender.api import plugin
class FbxCameraLoader(plugin.AssetLoader):
"""Load a camera from FBX.
Stores the imported asset in an empty named after the asset.
"""
families = ["camera"]
representations = ["fbx"]
label = "Load Camera (FBX)"
icon = "code-fork"
color = "orange"
def _remove(self, asset_group):
objects = list(asset_group.children)
for obj in objects:
if obj.type == 'CAMERA':
bpy.data.cameras.remove(obj.data)
elif obj.type == 'EMPTY':
objects.extend(obj.children)
bpy.data.objects.remove(obj)
def _process(self, libpath, asset_group, group_name):
plugin.deselect_all()
collection = bpy.context.view_layer.active_layer_collection.collection
bpy.ops.import_scene.fbx(filepath=libpath)
parent = bpy.context.scene.collection
objects = lib.get_selection()
for obj in objects:
obj.parent = asset_group
for obj in objects:
parent.objects.link(obj)
collection.objects.unlink(obj)
for obj in objects:
name = obj.name
obj.name = f"{group_name}:{name}"
if obj.type != 'EMPTY':
name_data = obj.data.name
obj.data.name = f"{group_name}:{name_data}"
if not obj.get(AVALON_PROPERTY):
obj[AVALON_PROPERTY] = dict()
avalon_info = obj[AVALON_PROPERTY]
avalon_info.update({"container_name": group_name})
plugin.deselect_all()
return objects
def process_asset(
self, context: dict, name: str, namespace: Optional[str] = None,
options: Optional[Dict] = None
) -> Optional[List]:
"""
Arguments:
name: Use pre-defined name
namespace: Use pre-defined namespace
context: Full parenthood of representation to load
options: Additional settings dictionary
"""
libpath = self.fname
asset = context["asset"]["name"]
subset = context["subset"]["name"]
asset_name = plugin.asset_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.asset_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
if not avalon_container:
avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
bpy.context.scene.collection.children.link(avalon_container)
asset_group = bpy.data.objects.new(group_name, object_data=None)
avalon_container.objects.link(asset_group)
objects = self._process(libpath, asset_group, group_name)
objects = []
nodes = list(asset_group.children)
for obj in nodes:
objects.append(obj)
nodes.extend(list(obj.children))
bpy.context.scene.collection.objects.link(asset_group)
asset_group[AVALON_PROPERTY] = {
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
"name": name,
"namespace": namespace or '',
"loader": str(self.__class__.__name__),
"representation": str(context["representation"]["_id"]),
"libpath": libpath,
"asset_name": asset_name,
"parent": str(context["representation"]["parent"]),
"family": context["representation"]["context"]["family"],
"objectName": group_name
}
self[:] = objects
return objects
def exec_update(self, container: Dict, representation: Dict):
"""Update the loaded asset.
This will remove all objects of the current collection, load the new
ones and add them to the collection.
If the objects of the collection are used in another collection they
will not be removed, only unlinked. Normally this should not be the
case though.
Warning:
No nested collections are supported at the moment!
"""
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
libpath = Path(api.get_representation_path(representation))
extension = libpath.suffix.lower()
self.log.info(
"Container: %s\nRepresentation: %s",
pformat(container, indent=2),
pformat(representation, indent=2),
)
assert asset_group, (
f"The asset is not loaded: {container['objectName']}"
)
assert libpath, (
"No existing library file found for {container['objectName']}"
)
assert libpath.is_file(), (
f"The file doesn't exist: {libpath}"
)
assert extension in plugin.VALID_EXTENSIONS, (
f"Unsupported file: {libpath}"
)
metadata = asset_group.get(AVALON_PROPERTY)
group_libpath = metadata["libpath"]
normalized_group_libpath = (
str(Path(bpy.path.abspath(group_libpath)).resolve())
)
normalized_libpath = (
str(Path(bpy.path.abspath(str(libpath))).resolve())
)
self.log.debug(
"normalized_group_libpath:\n %s\nnormalized_libpath:\n %s",
normalized_group_libpath,
normalized_libpath,
)
if normalized_group_libpath == normalized_libpath:
self.log.info("Library already loaded, not updating...")
return
mat = asset_group.matrix_basis.copy()
self._remove(asset_group)
self._process(str(libpath), asset_group, object_name)
asset_group.matrix_basis = mat
metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"])
def exec_remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene.
Arguments:
container (openpype:container-1.0): Container to remove,
from `host.ls()`.
Returns:
bool: Whether the container was deleted.
Warning:
No nested collections are supported at the moment!
"""
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
if not asset_group:
return False
self._remove(asset_group)
bpy.data.objects.remove(asset_group)
return True

View file

@ -46,7 +46,7 @@ class FbxModelLoader(plugin.AssetLoader):
bpy.data.objects.remove(obj)
def _process(self, libpath, asset_group, group_name, action):
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
collection = bpy.context.view_layer.active_layer_collection.collection
@ -112,7 +112,7 @@ class FbxModelLoader(plugin.AssetLoader):
avalon_info = obj[AVALON_PROPERTY]
avalon_info.update({"container_name": group_name})
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
return objects

View file

@ -150,7 +150,7 @@ class BlendLayoutLoader(plugin.AssetLoader):
bpy.data.orphans_purge(do_local_ids=False)
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
return objects

View file

@ -12,6 +12,7 @@ from avalon.blender.pipeline import AVALON_CONTAINERS
from avalon.blender.pipeline import AVALON_CONTAINER_ID
from avalon.blender.pipeline import AVALON_PROPERTY
from avalon.blender.pipeline import AVALON_INSTANCES
from openpype import lib
from openpype.hosts.blender.api import plugin
@ -59,7 +60,7 @@ class JsonLayoutLoader(plugin.AssetLoader):
return None
def _process(self, libpath, asset, asset_group, actions):
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
with open(libpath, "r") as fp:
data = json.load(fp)
@ -103,6 +104,21 @@ class JsonLayoutLoader(plugin.AssetLoader):
options=options
)
# Create the camera asset and the camera instance
creator_plugin = lib.get_creator_by_name("CreateCamera")
if not creator_plugin:
raise ValueError("Creator plugin \"CreateCamera\" was "
"not found.")
api.create(
creator_plugin,
name="camera",
# name=f"{unique_number}_{subset}_animation",
asset=asset,
options={"useSelection": False}
# data={"dependencies": str(context["representation"]["_id"])}
)
def process_asset(self,
context: dict,
name: str,

View file

@ -93,7 +93,7 @@ class BlendModelLoader(plugin.AssetLoader):
bpy.data.orphans_purge(do_local_ids=False)
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
return objects
@ -126,7 +126,7 @@ class BlendModelLoader(plugin.AssetLoader):
asset_group.empty_display_type = 'SINGLE_ARROW'
avalon_container.objects.link(asset_group)
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
if options is not None:
parent = options.get('parent')
@ -158,7 +158,7 @@ class BlendModelLoader(plugin.AssetLoader):
bpy.ops.object.parent_set(keep_transform=True)
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
objects = self._process(libpath, asset_group, group_name)

View file

@ -156,7 +156,7 @@ class BlendRigLoader(plugin.AssetLoader):
while bpy.data.orphans_purge(do_local_ids=False):
pass
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
return objects
@ -191,7 +191,7 @@ class BlendRigLoader(plugin.AssetLoader):
action = None
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
create_animation = False
@ -227,7 +227,7 @@ class BlendRigLoader(plugin.AssetLoader):
bpy.ops.object.parent_set(keep_transform=True)
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
objects = self._process(libpath, asset_group, group_name, action)
@ -250,7 +250,7 @@ class BlendRigLoader(plugin.AssetLoader):
data={"dependencies": str(context["representation"]["_id"])}
)
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
bpy.context.scene.collection.objects.link(asset_group)

View file

@ -28,7 +28,7 @@ class ExtractABC(api.Extractor):
# Perform extraction
self.log.info("Performing extraction..")
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
selected = []
asset_group = None
@ -50,7 +50,7 @@ class ExtractABC(api.Extractor):
flatten=False
)
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
if "representations" not in instance.data:
instance.data["representations"] = []

View file

@ -0,0 +1,73 @@
import os
from openpype import api
from openpype.hosts.blender.api import plugin
import bpy
class ExtractCamera(api.Extractor):
"""Extract as the camera as FBX."""
label = "Extract Camera"
hosts = ["blender"]
families = ["camera"]
optional = True
def process(self, instance):
# Define extract output file path
stagingdir = self.staging_dir(instance)
filename = f"{instance.name}.fbx"
filepath = os.path.join(stagingdir, filename)
# Perform extraction
self.log.info("Performing extraction..")
plugin.deselect_all()
selected = []
camera = None
for obj in instance:
if obj.type == "CAMERA":
obj.select_set(True)
selected.append(obj)
camera = obj
break
assert camera, "No camera found"
context = plugin.create_blender_context(
active=camera, selected=selected)
scale_length = bpy.context.scene.unit_settings.scale_length
bpy.context.scene.unit_settings.scale_length = 0.01
# We export the fbx
bpy.ops.export_scene.fbx(
context,
filepath=filepath,
use_active_collection=False,
use_selection=True,
object_types={'CAMERA'},
bake_anim_simplify_factor=0.0
)
bpy.context.scene.unit_settings.scale_length = scale_length
plugin.deselect_all()
if "representations" not in instance.data:
instance.data["representations"] = []
representation = {
'name': 'fbx',
'ext': 'fbx',
'files': filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(representation)
self.log.info("Extracted instance '%s' to: %s",
instance.name, representation)

View file

@ -24,7 +24,7 @@ class ExtractFBX(api.Extractor):
# Perform extraction
self.log.info("Performing extraction..")
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
selected = []
asset_group = None
@ -60,7 +60,7 @@ class ExtractFBX(api.Extractor):
add_leaf_bones=False
)
bpy.ops.object.select_all(action='DESELECT')
plugin.deselect_all()
for mat in new_materials:
bpy.data.materials.remove(mat)

View file

@ -0,0 +1,48 @@
from typing import List
import mathutils
import pyblish.api
import openpype.hosts.blender.api.action
class ValidateCameraZeroKeyframe(pyblish.api.InstancePlugin):
"""Camera must have a keyframe at frame 0.
Unreal shifts the first keyframe to frame 0. Forcing the camera to have
a keyframe at frame 0 will ensure that the animation will be the same
in Unreal and Blender.
"""
order = openpype.api.ValidateContentsOrder
hosts = ["blender"]
families = ["camera"]
category = "geometry"
version = (0, 1, 0)
label = "Zero Keyframe"
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]
_identity = mathutils.Matrix()
@classmethod
def get_invalid(cls, instance) -> List:
invalid = []
for obj in [obj for obj in instance]:
if obj.type == "CAMERA":
if obj.animation_data and obj.animation_data.action:
action = obj.animation_data.action
frames_set = set()
for fcu in action.fcurves:
for kp in fcu.keyframe_points:
frames_set.add(kp.co[0])
frames = list(frames_set)
frames.sort()
if frames[0] != 0.0:
invalid.append(obj)
return invalid
def process(self, instance):
invalid = self.get_invalid(instance)
if invalid:
raise RuntimeError(
f"Object found in instance is not in Object Mode: {invalid}")

View file

@ -0,0 +1,105 @@
from .api.utils import (
setup
)
from .api.pipeline import (
install,
uninstall,
ls,
containerise,
update_container,
maintained_selection,
remove_instance,
list_instances,
imprint
)
from .api.lib import (
FlameAppFramework,
maintain_current_timeline,
get_project_manager,
get_current_project,
get_current_timeline,
create_bin,
)
from .api.menu import (
FlameMenuProjectConnect,
FlameMenuTimeline
)
from .api.workio import (
open_file,
save_file,
current_file,
has_unsaved_changes,
file_extensions,
work_root
)
import os
HOST_DIR = os.path.dirname(
os.path.abspath(__file__)
)
API_DIR = os.path.join(HOST_DIR, "api")
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
app_framework = None
apps = []
__all__ = [
"HOST_DIR",
"API_DIR",
"PLUGINS_DIR",
"PUBLISH_PATH",
"LOAD_PATH",
"CREATE_PATH",
"INVENTORY_PATH",
"INVENTORY_PATH",
"app_framework",
"apps",
# pipeline
"install",
"uninstall",
"ls",
"containerise",
"update_container",
"reload_pipeline",
"maintained_selection",
"remove_instance",
"list_instances",
"imprint",
# utils
"setup",
# lib
"FlameAppFramework",
"maintain_current_timeline",
"get_project_manager",
"get_current_project",
"get_current_timeline",
"create_bin",
# menu
"FlameMenuProjectConnect",
"FlameMenuTimeline",
# plugin
# workio
"open_file",
"save_file",
"current_file",
"has_unsaved_changes",
"file_extensions",
"work_root"
]

View file

@ -0,0 +1,3 @@
"""
OpenPype Autodesk Flame api
"""

View file

@ -0,0 +1,276 @@
import sys
import os
import pickle
import contextlib
from pprint import pformat
from openpype.api import Logger
log = Logger().get_logger(__name__)
@contextlib.contextmanager
def io_preferences_file(klass, filepath, write=False):
try:
flag = "w" if write else "r"
yield open(filepath, flag)
except IOError as _error:
klass.log.info("Unable to work with preferences `{}`: {}".format(
filepath, _error))
class FlameAppFramework(object):
# flameAppFramework class takes care of preferences
class prefs_dict(dict):
def __init__(self, master, name, **kwargs):
self.name = name
self.master = master
if not self.master.get(self.name):
self.master[self.name] = {}
self.master[self.name].__init__()
def __getitem__(self, k):
return self.master[self.name].__getitem__(k)
def __setitem__(self, k, v):
return self.master[self.name].__setitem__(k, v)
def __delitem__(self, k):
return self.master[self.name].__delitem__(k)
def get(self, k, default=None):
return self.master[self.name].get(k, default)
def setdefault(self, k, default=None):
return self.master[self.name].setdefault(k, default)
def pop(self, k, v=object()):
if v is object():
return self.master[self.name].pop(k)
return self.master[self.name].pop(k, v)
def update(self, mapping=(), **kwargs):
self.master[self.name].update(mapping, **kwargs)
def __contains__(self, k):
return self.master[self.name].__contains__(k)
def copy(self): # don"t delegate w/ super - dict.copy() -> dict :(
return type(self)(self)
def keys(self):
return self.master[self.name].keys()
@classmethod
def fromkeys(cls, keys, v=None):
return cls.master[cls.name].fromkeys(keys, v)
def __repr__(self):
return "{0}({1})".format(
type(self).__name__, self.master[self.name].__repr__())
def master_keys(self):
return self.master.keys()
def __init__(self):
self.name = self.__class__.__name__
self.bundle_name = "OpenPypeFlame"
# self.prefs scope is limited to flame project and user
self.prefs = {}
self.prefs_user = {}
self.prefs_global = {}
self.log = log
try:
import flame
self.flame = flame
self.flame_project_name = self.flame.project.current_project.name
self.flame_user_name = flame.users.current_user.name
except Exception:
self.flame = None
self.flame_project_name = None
self.flame_user_name = None
import socket
self.hostname = socket.gethostname()
if sys.platform == "darwin":
self.prefs_folder = os.path.join(
os.path.expanduser("~"),
"Library",
"Caches",
"OpenPype",
self.bundle_name
)
elif sys.platform.startswith("linux"):
self.prefs_folder = os.path.join(
os.path.expanduser("~"),
".OpenPype",
self.bundle_name)
self.prefs_folder = os.path.join(
self.prefs_folder,
self.hostname,
)
self.log.info("[{}] waking up".format(self.__class__.__name__))
self.load_prefs()
# menu auto-refresh defaults
if not self.prefs_global.get("menu_auto_refresh"):
self.prefs_global["menu_auto_refresh"] = {
"media_panel": True,
"batch": True,
"main_menu": True,
"timeline_menu": True
}
self.apps = []
def get_pref_file_paths(self):
prefix = self.prefs_folder + os.path.sep + self.bundle_name
prefs_file_path = "_".join([
prefix, self.flame_user_name,
self.flame_project_name]) + ".prefs"
prefs_user_file_path = "_".join([
prefix, self.flame_user_name]) + ".prefs"
prefs_global_file_path = prefix + ".prefs"
return (prefs_file_path, prefs_user_file_path, prefs_global_file_path)
def load_prefs(self):
(proj_pref_path, user_pref_path,
glob_pref_path) = self.get_pref_file_paths()
with io_preferences_file(self, proj_pref_path) as prefs_file:
self.prefs = pickle.load(prefs_file)
self.log.info(
"Project - preferences contents:\n{}".format(
pformat(self.prefs)
))
with io_preferences_file(self, user_pref_path) as prefs_file:
self.prefs_user = pickle.load(prefs_file)
self.log.info(
"User - preferences contents:\n{}".format(
pformat(self.prefs_user)
))
with io_preferences_file(self, glob_pref_path) as prefs_file:
self.prefs_global = pickle.load(prefs_file)
self.log.info(
"Global - preferences contents:\n{}".format(
pformat(self.prefs_global)
))
return True
def save_prefs(self):
# make sure the preference folder is available
if not os.path.isdir(self.prefs_folder):
try:
os.makedirs(self.prefs_folder)
except Exception:
self.log.info("Unable to create folder {}".format(
self.prefs_folder))
return False
# get all pref file paths
(proj_pref_path, user_pref_path,
glob_pref_path) = self.get_pref_file_paths()
with io_preferences_file(self, proj_pref_path, True) as prefs_file:
pickle.dump(self.prefs, prefs_file)
self.log.info(
"Project - preferences contents:\n{}".format(
pformat(self.prefs)
))
with io_preferences_file(self, user_pref_path, True) as prefs_file:
pickle.dump(self.prefs_user, prefs_file)
self.log.info(
"User - preferences contents:\n{}".format(
pformat(self.prefs_user)
))
with io_preferences_file(self, glob_pref_path, True) as prefs_file:
pickle.dump(self.prefs_global, prefs_file)
self.log.info(
"Global - preferences contents:\n{}".format(
pformat(self.prefs_global)
))
return True
@contextlib.contextmanager
def maintain_current_timeline(to_timeline, from_timeline=None):
"""Maintain current timeline selection during context
Attributes:
from_timeline (resolve.Timeline)[optional]:
Example:
>>> print(from_timeline.GetName())
timeline1
>>> print(to_timeline.GetName())
timeline2
>>> with maintain_current_timeline(to_timeline):
... print(get_current_timeline().GetName())
timeline2
>>> print(get_current_timeline().GetName())
timeline1
"""
# todo: this is still Resolve's implementation
project = get_current_project()
working_timeline = from_timeline or project.GetCurrentTimeline()
# swith to the input timeline
project.SetCurrentTimeline(to_timeline)
try:
# do a work
yield
finally:
# put the original working timeline to context
project.SetCurrentTimeline(working_timeline)
def get_project_manager():
# TODO: get_project_manager
return
def get_media_storage():
# TODO: get_media_storage
return
def get_current_project():
# TODO: get_current_project
return
def get_current_timeline(new=False):
# TODO: get_current_timeline
return
def create_bin(name, root=None):
# TODO: create_bin
return
def rescan_hooks():
import flame
try:
flame.execute_shortcut('Rescan Python Hooks')
except Exception:
pass

View file

@ -0,0 +1,208 @@
import os
from Qt import QtWidgets
from copy import deepcopy
from openpype.tools.utils.host_tools import HostToolsHelper
menu_group_name = 'OpenPype'
default_flame_export_presets = {
'Publish': {
'PresetVisibility': 2,
'PresetType': 0,
'PresetFile': 'OpenEXR/OpenEXR (16-bit fp PIZ).xml'
},
'Preview': {
'PresetVisibility': 3,
'PresetType': 2,
'PresetFile': 'Generate Preview.xml'
},
'Thumbnail': {
'PresetVisibility': 3,
'PresetType': 0,
'PresetFile': 'Generate Thumbnail.xml'
}
}
class _FlameMenuApp(object):
def __init__(self, framework):
self.name = self.__class__.__name__
self.framework = framework
self.log = framework.log
self.menu_group_name = menu_group_name
self.dynamic_menu_data = {}
# flame module is only avaliable when a
# flame project is loaded and initialized
self.flame = None
try:
import flame
self.flame = flame
except ImportError:
self.flame = None
self.flame_project_name = flame.project.current_project.name
self.prefs = self.framework.prefs_dict(self.framework.prefs, self.name)
self.prefs_user = self.framework.prefs_dict(
self.framework.prefs_user, self.name)
self.prefs_global = self.framework.prefs_dict(
self.framework.prefs_global, self.name)
self.mbox = QtWidgets.QMessageBox()
self.menu = {
"actions": [{
'name': os.getenv("AVALON_PROJECT", "project"),
'isEnabled': False
}],
"name": self.menu_group_name
}
self.tools_helper = HostToolsHelper()
def __getattr__(self, name):
def method(*args, **kwargs):
print('calling %s' % name)
return method
def rescan(self, *args, **kwargs):
if not self.flame:
try:
import flame
self.flame = flame
except ImportError:
self.flame = None
if self.flame:
self.flame.execute_shortcut('Rescan Python Hooks')
self.log.info('Rescan Python Hooks')
class FlameMenuProjectConnect(_FlameMenuApp):
# flameMenuProjectconnect app takes care of the preferences dialog as well
def __init__(self, framework):
_FlameMenuApp.__init__(self, framework)
def __getattr__(self, name):
def method(*args, **kwargs):
project = self.dynamic_menu_data.get(name)
if project:
self.link_project(project)
return method
def build_menu(self):
if not self.flame:
return []
flame_project_name = self.flame_project_name
self.log.info("______ {} ______".format(flame_project_name))
menu = deepcopy(self.menu)
menu['actions'].append({
"name": "Workfiles ...",
"execute": lambda x: self.tools_helper.show_workfiles()
})
menu['actions'].append({
"name": "Create ...",
"execute": lambda x: self.tools_helper.show_creator()
})
menu['actions'].append({
"name": "Publish ...",
"execute": lambda x: self.tools_helper.show_publish()
})
menu['actions'].append({
"name": "Load ...",
"execute": lambda x: self.tools_helper.show_loader()
})
menu['actions'].append({
"name": "Manage ...",
"execute": lambda x: self.tools_helper.show_scene_inventory()
})
menu['actions'].append({
"name": "Library ...",
"execute": lambda x: self.tools_helper.show_library_loader()
})
return menu
def get_projects(self, *args, **kwargs):
pass
def refresh(self, *args, **kwargs):
self.rescan()
def rescan(self, *args, **kwargs):
if not self.flame:
try:
import flame
self.flame = flame
except ImportError:
self.flame = None
if self.flame:
self.flame.execute_shortcut('Rescan Python Hooks')
self.log.info('Rescan Python Hooks')
class FlameMenuTimeline(_FlameMenuApp):
# flameMenuProjectconnect app takes care of the preferences dialog as well
def __init__(self, framework):
_FlameMenuApp.__init__(self, framework)
def __getattr__(self, name):
def method(*args, **kwargs):
project = self.dynamic_menu_data.get(name)
if project:
self.link_project(project)
return method
def build_menu(self):
if not self.flame:
return []
flame_project_name = self.flame_project_name
self.log.info("______ {} ______".format(flame_project_name))
menu = deepcopy(self.menu)
menu['actions'].append({
"name": "Create ...",
"execute": lambda x: self.tools_helper.show_creator()
})
menu['actions'].append({
"name": "Publish ...",
"execute": lambda x: self.tools_helper.show_publish()
})
menu['actions'].append({
"name": "Load ...",
"execute": lambda x: self.tools_helper.show_loader()
})
menu['actions'].append({
"name": "Manage ...",
"execute": lambda x: self.tools_helper.show_scene_inventory()
})
return menu
def get_projects(self, *args, **kwargs):
pass
def refresh(self, *args, **kwargs):
self.rescan()
def rescan(self, *args, **kwargs):
if not self.flame:
try:
import flame
self.flame = flame
except ImportError:
self.flame = None
if self.flame:
self.flame.execute_shortcut('Rescan Python Hooks')
self.log.info('Rescan Python Hooks')

View file

@ -0,0 +1,155 @@
"""
Basic avalon integration
"""
import contextlib
from avalon import api as avalon
from pyblish import api as pyblish
from openpype.api import Logger
AVALON_CONTAINERS = "AVALON_CONTAINERS"
log = Logger().get_logger(__name__)
def install():
from .. import (
PUBLISH_PATH,
LOAD_PATH,
CREATE_PATH,
INVENTORY_PATH
)
# TODO: install
# Disable all families except for the ones we explicitly want to see
family_states = [
"imagesequence",
"render2d",
"plate",
"render",
"mov",
"clip"
]
avalon.data["familiesStateDefault"] = False
avalon.data["familiesStateToggled"] = family_states
log.info("openpype.hosts.flame installed")
pyblish.register_host("flame")
pyblish.register_plugin_path(PUBLISH_PATH)
log.info("Registering Flame plug-ins..")
avalon.register_plugin_path(avalon.Loader, LOAD_PATH)
avalon.register_plugin_path(avalon.Creator, CREATE_PATH)
avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH)
# register callback for switching publishable
pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled)
def uninstall():
from .. import (
PUBLISH_PATH,
LOAD_PATH,
CREATE_PATH,
INVENTORY_PATH
)
# TODO: uninstall
pyblish.deregister_host("flame")
pyblish.deregister_plugin_path(PUBLISH_PATH)
log.info("Deregistering DaVinci Resovle plug-ins..")
avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH)
avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH)
avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH)
# register callback for switching publishable
pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled)
def containerise(tl_segment,
name,
namespace,
context,
loader=None,
data=None):
# TODO: containerise
pass
def ls():
"""List available containers.
"""
# TODO: ls
pass
def parse_container(tl_segment, validate=True):
"""Return container data from timeline_item's openpype tag.
"""
# TODO: parse_container
pass
def update_container(tl_segment, data=None):
"""Update container data to input timeline_item's openpype tag.
"""
# TODO: update_container
pass
@contextlib.contextmanager
def maintained_selection():
"""Maintain selection during context
Example:
>>> with maintained_selection():
... node['selected'].setValue(True)
>>> print(node['selected'].value())
False
"""
# TODO: maintained_selection + remove undo steps
try:
# do the operation
yield
finally:
pass
def reset_selection():
"""Deselect all selected nodes
"""
pass
def on_pyblish_instance_toggled(instance, old_value, new_value):
"""Toggle node passthrough states on instance toggles."""
log.info("instance toggle: {}, old_value: {}, new_value:{} ".format(
instance, old_value, new_value))
# from openpype.hosts.resolve import (
# set_publish_attribute
# )
# # Whether instances should be passthrough based on new value
# timeline_item = instance.data["item"]
# set_publish_attribute(timeline_item, new_value)
def remove_instance(instance):
"""Remove instance marker from track item."""
# TODO: remove_instance
pass
def list_instances():
"""List all created instances from current workfile."""
# TODO: list_instances
pass
def imprint(item, data=None):
# TODO: imprint
pass

View file

@ -0,0 +1,3 @@
# Creator plugin functions
# Publishing plugin functions
# Loader plugin functions

View file

@ -0,0 +1,108 @@
"""
Flame utils for syncing scripts
"""
import os
import shutil
from openpype.api import Logger
log = Logger().get_logger(__name__)
def _sync_utility_scripts(env=None):
""" Synchronizing basic utlility scripts for flame.
To be able to run start OpenPype within Flame we have to copy
all utility_scripts and additional FLAME_SCRIPT_DIR into
`/opt/Autodesk/shared/python`. This will be always synchronizing those
folders.
"""
from .. import HOST_DIR
env = env or os.environ
# initiate inputs
scripts = {}
fsd_env = env.get("FLAME_SCRIPT_DIRS", "")
flame_shared_dir = "/opt/Autodesk/shared/python"
fsd_paths = [os.path.join(
HOST_DIR,
"utility_scripts"
)]
# collect script dirs
log.info("FLAME_SCRIPT_DIRS: `{fsd_env}`".format(**locals()))
log.info("fsd_paths: `{fsd_paths}`".format(**locals()))
# add application environment setting for FLAME_SCRIPT_DIR
# to script path search
for _dirpath in fsd_env.split(os.pathsep):
if not os.path.isdir(_dirpath):
log.warning("Path is not a valid dir: `{_dirpath}`".format(
**locals()))
continue
fsd_paths.append(_dirpath)
# collect scripts from dirs
for path in fsd_paths:
scripts.update({path: os.listdir(path)})
remove_black_list = []
for _k, s_list in scripts.items():
remove_black_list += s_list
log.info("remove_black_list: `{remove_black_list}`".format(**locals()))
log.info("Additional Flame script paths: `{fsd_paths}`".format(**locals()))
log.info("Flame Scripts: `{scripts}`".format(**locals()))
# make sure no script file is in folder
if next(iter(os.listdir(flame_shared_dir)), None):
for _itm in os.listdir(flame_shared_dir):
skip = False
# skip all scripts and folders which are not maintained
if _itm not in remove_black_list:
skip = True
# do not skyp if pyc in extension
if not os.path.isdir(_itm) and "pyc" in os.path.splitext(_itm)[-1]:
skip = False
# continue if skip in true
if skip:
continue
path = os.path.join(flame_shared_dir, _itm)
log.info("Removing `{path}`...".format(**locals()))
if os.path.isdir(path):
shutil.rmtree(path, onerror=None)
else:
os.remove(path)
# copy scripts into Resolve's utility scripts dir
for dirpath, scriptlist in scripts.items():
# directory and scripts list
for _script in scriptlist:
# script in script list
src = os.path.join(dirpath, _script)
dst = os.path.join(flame_shared_dir, _script)
log.info("Copying `{src}` to `{dst}`...".format(**locals()))
if os.path.isdir(src):
shutil.copytree(
src, dst, symlinks=False,
ignore=None, ignore_dangling_symlinks=False
)
else:
shutil.copy2(src, dst)
def setup(env=None):
""" Wrapper installer started from
`flame/hooks/pre_flame_setup.py`
"""
env = env or os.environ
# synchronize resolve utility scripts
_sync_utility_scripts(env)
log.info("Flame OpenPype wrapper has been installed")

View file

@ -0,0 +1,37 @@
"""Host API required Work Files tool"""
import os
from openpype.api import Logger
# from .. import (
# get_project_manager,
# get_current_project
# )
log = Logger().get_logger(__name__)
exported_projet_ext = ".otoc"
def file_extensions():
return [exported_projet_ext]
def has_unsaved_changes():
pass
def save_file(filepath):
pass
def open_file(filepath):
pass
def current_file():
pass
def work_root(session):
return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/")

View file

@ -0,0 +1,132 @@
import os
import json
import tempfile
import contextlib
from openpype.lib import (
PreLaunchHook, get_openpype_username)
from openpype.hosts import flame as opflame
import openpype
from pprint import pformat
class FlamePrelaunch(PreLaunchHook):
""" Flame prelaunch hook
Will make sure flame_script_dirs are coppied to user's folder defined
in environment var FLAME_SCRIPT_DIR.
"""
app_groups = ["flame"]
# todo: replace version number with avalon launch app version
flame_python_exe = "/opt/Autodesk/python/2021/bin/python2.7"
wtc_script_path = os.path.join(
opflame.HOST_DIR, "scripts", "wiretap_com.py")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.signature = "( {} )".format(self.__class__.__name__)
def execute(self):
"""Hook entry method."""
project_doc = self.data["project_doc"]
user_name = get_openpype_username()
self.log.debug("Collected user \"{}\"".format(user_name))
self.log.info(pformat(project_doc))
_db_p_data = project_doc["data"]
width = _db_p_data["resolutionWidth"]
height = _db_p_data["resolutionHeight"]
fps = int(_db_p_data["fps"])
project_data = {
"Name": project_doc["name"],
"Nickname": _db_p_data["code"],
"Description": "Created by OpenPype",
"SetupDir": project_doc["name"],
"FrameWidth": int(width),
"FrameHeight": int(height),
"AspectRatio": float((width / height) * _db_p_data["pixelAspect"]),
"FrameRate": "{} fps".format(fps),
"FrameDepth": "16-bit fp",
"FieldDominance": "PROGRESSIVE"
}
data_to_script = {
# from settings
"host_name": "localhost",
"volume_name": "stonefs",
"group_name": "staff",
"color_policy": "ACES 1.1",
# from project
"project_name": project_doc["name"],
"user_name": user_name,
"project_data": project_data
}
app_arguments = self._get_launch_arguments(data_to_script)
self.log.info(pformat(dict(self.launch_context.env)))
opflame.setup(self.launch_context.env)
self.launch_context.launch_args.extend(app_arguments)
def _get_launch_arguments(self, script_data):
# Dump data to string
dumped_script_data = json.dumps(script_data)
with make_temp_file(dumped_script_data) as tmp_json_path:
# Prepare subprocess arguments
args = [
self.flame_python_exe,
self.wtc_script_path,
tmp_json_path
]
self.log.info("Executing: {}".format(" ".join(args)))
process_kwargs = {
"logger": self.log,
"env": {}
}
openpype.api.run_subprocess(args, **process_kwargs)
# process returned json file to pass launch args
return_json_data = open(tmp_json_path).read()
returned_data = json.loads(return_json_data)
app_args = returned_data.get("app_args")
self.log.info("____ app_args: `{}`".format(app_args))
if not app_args:
RuntimeError("App arguments were not solved")
return app_args
@contextlib.contextmanager
def make_temp_file(data):
try:
# Store dumped json to temporary file
temporary_json_file = tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False
)
temporary_json_file.write(data)
temporary_json_file.close()
temporary_json_filepath = temporary_json_file.name.replace(
"\\", "/"
)
yield temporary_json_filepath
except IOError as _error:
raise IOError(
"Not able to create temp json file: {}".format(
_error
)
)
finally:
# Remove the temporary json
os.remove(temporary_json_filepath)

View file

@ -0,0 +1,490 @@
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import os
import sys
import subprocess
import json
import xml.dom.minidom as minidom
from copy import deepcopy
import datetime
try:
from libwiretapPythonClientAPI import (
WireTapClientInit)
except ImportError:
flame_python_path = "/opt/Autodesk/flame_2021/python"
flame_exe_path = (
"/opt/Autodesk/flame_2021/bin/flame.app"
"/Contents/MacOS/startApp")
sys.path.append(flame_python_path)
from libwiretapPythonClientAPI import (
WireTapClientInit,
WireTapClientUninit,
WireTapNodeHandle,
WireTapServerHandle,
WireTapInt,
WireTapStr
)
class WireTapCom(object):
"""
Comunicator class wrapper for talking to WireTap db.
This way we are able to set new project with settings and
correct colorspace policy. Also we are able to create new user
or get actuall user with similar name (users are usually cloning
their profiles and adding date stamp into suffix).
"""
def __init__(self, host_name=None, volume_name=None, group_name=None):
"""Initialisation of WireTap communication class
Args:
host_name (str, optional): Name of host server. Defaults to None.
volume_name (str, optional): Name of volume. Defaults to None.
group_name (str, optional): Name of user group. Defaults to None.
"""
# set main attributes of server
# if there are none set the default installation
self.host_name = host_name or "localhost"
self.volume_name = volume_name or "stonefs"
self.group_name = group_name or "staff"
# initialize WireTap client
WireTapClientInit()
# add the server to shared variable
self._server = WireTapServerHandle("{}:IFFFS".format(self.host_name))
print("WireTap connected at '{}'...".format(
self.host_name))
def close(self):
self._server = None
WireTapClientUninit()
print("WireTap closed...")
def get_launch_args(
self, project_name, project_data, user_name, *args, **kwargs):
"""Forming launch arguments for OpenPype launcher.
Args:
project_name (str): name of project
project_data (dict): Flame compatible project data
user_name (str): name of user
Returns:
list: arguments
"""
workspace_name = kwargs.get("workspace_name")
color_policy = kwargs.get("color_policy")
self._project_prep(project_name)
self._set_project_settings(project_name, project_data)
self._set_project_colorspace(project_name, color_policy)
user_name = self._user_prep(user_name)
if workspace_name is None:
# default workspace
print("Using a default workspace")
return [
"--start-project={}".format(project_name),
"--start-user={}".format(user_name),
"--create-workspace"
]
else:
print(
"Using a custom workspace '{}'".format(workspace_name))
self._workspace_prep(project_name, workspace_name)
return [
"--start-project={}".format(project_name),
"--start-user={}".format(user_name),
"--create-workspace",
"--start-workspace={}".format(workspace_name)
]
def _workspace_prep(self, project_name, workspace_name):
"""Preparing a workspace
In case it doesn not exists it will create one
Args:
project_name (str): project name
workspace_name (str): workspace name
Raises:
AttributeError: unable to create workspace
"""
workspace_exists = self._child_is_in_parent_path(
"/projects/{}".format(project_name), workspace_name, "WORKSPACE"
)
if not workspace_exists:
project = WireTapNodeHandle(
self._server, "/projects/{}".format(project_name))
workspace_node = WireTapNodeHandle()
created_workspace = project.createNode(
workspace_name, "WORKSPACE", workspace_node)
if not created_workspace:
raise AttributeError(
"Cannot create workspace `{}` in "
"project `{}`: `{}`".format(
workspace_name, project_name, project.lastError())
)
print(
"Workspace `{}` is successfully created".format(workspace_name))
def _project_prep(self, project_name):
"""Preparing a project
In case it doesn not exists it will create one
Args:
project_name (str): project name
Raises:
AttributeError: unable to create project
"""
# test if projeft exists
project_exists = self._child_is_in_parent_path(
"/projects", project_name, "PROJECT")
if not project_exists:
volumes = self._get_all_volumes()
if len(volumes) == 0:
raise AttributeError(
"Not able to create new project. No Volumes existing"
)
# check if volumes exists
if self.volume_name not in volumes:
raise AttributeError(
("Volume '{}' does not exist '{}'").format(
self.volume_name, volumes)
)
# form cmd arguments
project_create_cmd = [
os.path.join(
"/opt/Autodesk/",
"wiretap",
"tools",
"2021",
"wiretap_create_node",
),
'-n',
os.path.join("/volumes", self.volume_name),
'-d',
project_name,
'-g',
]
project_create_cmd.append(self.group_name)
print(project_create_cmd)
exit_code = subprocess.call(
project_create_cmd,
cwd=os.path.expanduser('~'))
if exit_code != 0:
RuntimeError("Cannot create project in flame db")
print(
"A new project '{}' is created.".format(project_name))
def _get_all_volumes(self):
"""Request all available volumens from WireTap
Returns:
list: all available volumes in server
Rises:
AttributeError: unable to get any volumes childs from server
"""
root = WireTapNodeHandle(self._server, "/volumes")
children_num = WireTapInt(0)
get_children_num = root.getNumChildren(children_num)
if not get_children_num:
raise AttributeError(
"Cannot get number of volumes: {}".format(root.lastError())
)
volumes = []
# go trough all children and get volume names
child_obj = WireTapNodeHandle()
for child_idx in range(children_num):
# get a child
if not root.getChild(child_idx, child_obj):
raise AttributeError(
"Unable to get child: {}".format(root.lastError()))
node_name = WireTapStr()
get_children_name = child_obj.getDisplayName(node_name)
if not get_children_name:
raise AttributeError(
"Unable to get child name: {}".format(
child_obj.lastError())
)
volumes.append(node_name.c_str())
return volumes
def _user_prep(self, user_name):
"""Ensuring user does exists in user's stack
Args:
user_name (str): name of a user
Raises:
AttributeError: unable to create user
"""
# get all used usernames in db
used_names = self._get_usernames()
print(">> used_names: {}".format(used_names))
# filter only those which are sharing input user name
filtered_users = [user for user in used_names if user_name in user]
if filtered_users:
# todo: need to find lastly created following regex patern for
# date used in name
return filtered_users.pop()
# create new user name with date in suffix
now = datetime.datetime.now() # current date and time
date = now.strftime("%Y%m%d")
new_user_name = "{}_{}".format(user_name, date)
print(new_user_name)
if not self._child_is_in_parent_path("/users", new_user_name, "USER"):
# Create the new user
users = WireTapNodeHandle(self._server, "/users")
user_node = WireTapNodeHandle()
created_user = users.createNode(new_user_name, "USER", user_node)
if not created_user:
raise AttributeError(
"User {} cannot be created: {}".format(
new_user_name, users.lastError())
)
print("User `{}` is created".format(new_user_name))
return new_user_name
def _get_usernames(self):
"""Requesting all available users from WireTap
Returns:
list: all available user names
Raises:
AttributeError: there are no users in server
"""
root = WireTapNodeHandle(self._server, "/users")
children_num = WireTapInt(0)
get_children_num = root.getNumChildren(children_num)
if not get_children_num:
raise AttributeError(
"Cannot get number of volumes: {}".format(root.lastError())
)
usernames = []
# go trough all children and get volume names
child_obj = WireTapNodeHandle()
for child_idx in range(children_num):
# get a child
if not root.getChild(child_idx, child_obj):
raise AttributeError(
"Unable to get child: {}".format(root.lastError()))
node_name = WireTapStr()
get_children_name = child_obj.getDisplayName(node_name)
if not get_children_name:
raise AttributeError(
"Unable to get child name: {}".format(
child_obj.lastError())
)
usernames.append(node_name.c_str())
return usernames
def _child_is_in_parent_path(self, parent_path, child_name, child_type):
"""Checking if a given child is in parent path.
Args:
parent_path (str): db path to parent
child_name (str): name of child
child_type (str): type of child
Raises:
AttributeError: Not able to get number of children
AttributeError: Not able to get children form parent
AttributeError: Not able to get children name
AttributeError: Not able to get children type
Returns:
bool: True if child is in parent path
"""
parent = WireTapNodeHandle(self._server, parent_path)
# iterate number of children
children_num = WireTapInt(0)
requested = parent.getNumChildren(children_num)
if not requested:
raise AttributeError((
"Error: Cannot request number of "
"childrens from the node {}. Make sure your "
"wiretap service is running: {}").format(
parent_path, parent.lastError())
)
# iterate children
child_obj = WireTapNodeHandle()
for child_idx in range(children_num):
if not parent.getChild(child_idx, child_obj):
raise AttributeError(
"Cannot get child: {}".format(
parent.lastError()))
node_name = WireTapStr()
node_type = WireTapStr()
if not child_obj.getDisplayName(node_name):
raise AttributeError(
"Unable to get child name: %s" % child_obj.lastError()
)
if not child_obj.getNodeTypeStr(node_type):
raise AttributeError(
"Unable to obtain child type: %s" % child_obj.lastError()
)
if (node_name.c_str() == child_name) and (
node_type.c_str() == child_type):
return True
return False
def _set_project_settings(self, project_name, project_data):
"""Setting project attributes.
Args:
project_name (str): name of project
project_data (dict): data with project attributes
(flame compatible)
Raises:
AttributeError: Not able to set project attributes
"""
# generated xml from project_data dict
_xml = "<Project>"
for key, value in project_data.items():
_xml += "<{}>{}</{}>".format(key, value, key)
_xml += "</Project>"
pretty_xml = minidom.parseString(_xml).toprettyxml()
print("__ xml: {}".format(pretty_xml))
# set project data to wiretap
project_node = WireTapNodeHandle(
self._server, "/projects/{}".format(project_name))
if not project_node.setMetaData("XML", _xml):
raise AttributeError(
"Not able to set project attributes {}. Error: {}".format(
project_name, project_node.lastError())
)
print("Project settings successfully set.")
def _set_project_colorspace(self, project_name, color_policy):
"""Set project's colorspace policy.
Args:
project_name (str): name of project
color_policy (str): name of policy
Raises:
RuntimeError: Not able to set colorspace policy
"""
color_policy = color_policy or "Legacy"
project_colorspace_cmd = [
os.path.join(
"/opt/Autodesk/",
"wiretap",
"tools",
"2021",
"wiretap_duplicate_node",
),
"-s",
"/syncolor/policies/Autodesk/{}".format(color_policy),
"-n",
"/projects/{}/syncolor".format(project_name)
]
print(project_colorspace_cmd)
exit_code = subprocess.call(
project_colorspace_cmd,
cwd=os.path.expanduser('~'))
if exit_code != 0:
RuntimeError("Cannot set colorspace {} on project {}".format(
color_policy, project_name
))
if __name__ == "__main__":
# get json exchange data
json_path = sys.argv[-1]
json_data = open(json_path).read()
in_data = json.loads(json_data)
out_data = deepcopy(in_data)
# get main server attributes
host_name = in_data.pop("host_name")
volume_name = in_data.pop("volume_name")
group_name = in_data.pop("group_name")
# initialize class
wiretap_handler = WireTapCom(host_name, volume_name, group_name)
try:
app_args = wiretap_handler.get_launch_args(
project_name=in_data.pop("project_name"),
project_data=in_data.pop("project_data"),
user_name=in_data.pop("user_name"),
**in_data
)
finally:
wiretap_handler.close()
# set returned args back to out data
out_data.update({
"app_args": app_args
})
# write it out back to the exchange json file
with open(json_path, "w") as file_stream:
json.dump(out_data, file_stream, indent=4)

View file

@ -0,0 +1,191 @@
from __future__ import print_function
import sys
from Qt import QtWidgets
from pprint import pformat
import atexit
import openpype
import avalon
import openpype.hosts.flame as opflame
flh = sys.modules[__name__]
flh._project = None
def openpype_install():
"""Registering OpenPype in context
"""
openpype.install()
avalon.api.install(opflame)
print("Avalon registred hosts: {}".format(
avalon.api.registered_host()))
# Exception handler
def exeption_handler(exctype, value, _traceback):
"""Exception handler for improving UX
Args:
exctype (str): type of exception
value (str): exception value
tb (str): traceback to show
"""
import traceback
msg = "OpenPype: Python exception {} in {}".format(value, exctype)
mbox = QtWidgets.QMessageBox()
mbox.setText(msg)
mbox.setDetailedText(
pformat(traceback.format_exception(exctype, value, _traceback)))
mbox.setStyleSheet('QLabel{min-width: 800px;}')
mbox.exec_()
sys.__excepthook__(exctype, value, _traceback)
# add exception handler into sys module
sys.excepthook = exeption_handler
# register clean up logic to be called at Flame exit
def cleanup():
"""Cleaning up Flame framework context
"""
if opflame.apps:
print('`{}` cleaning up apps:\n {}\n'.format(
__file__, pformat(opflame.apps)))
while len(opflame.apps):
app = opflame.apps.pop()
print('`{}` removing : {}'.format(__file__, app.name))
del app
opflame.apps = []
if opflame.app_framework:
print('PYTHON\t: %s cleaning up' % opflame.app_framework.bundle_name)
opflame.app_framework.save_prefs()
opflame.app_framework = None
atexit.register(cleanup)
def load_apps():
"""Load available apps into Flame framework
"""
opflame.apps.append(opflame.FlameMenuProjectConnect(opflame.app_framework))
opflame.apps.append(opflame.FlameMenuTimeline(opflame.app_framework))
opflame.app_framework.log.info("Apps are loaded")
def project_changed_dict(info):
"""Hook for project change action
Args:
info (str): info text
"""
cleanup()
def app_initialized(parent=None):
"""Inicialization of Framework
Args:
parent (obj, optional): Parent object. Defaults to None.
"""
opflame.app_framework = opflame.FlameAppFramework()
print("{} initializing".format(
opflame.app_framework.bundle_name))
load_apps()
"""
Initialisation of the hook is starting from here
First it needs to test if it can import the flame modul.
This will happen only in case a project has been loaded.
Then `app_initialized` will load main Framework which will load
all menu objects as apps.
"""
try:
import flame # noqa
app_initialized(parent=None)
except ImportError:
print("!!!! not able to import flame module !!!!")
def rescan_hooks():
import flame # noqa
flame.execute_shortcut('Rescan Python Hooks')
def _build_app_menu(app_name):
"""Flame menu object generator
Args:
app_name (str): name of menu object app
Returns:
list: menu object
"""
menu = []
# first find the relative appname
app = None
for _app in opflame.apps:
if _app.__class__.__name__ == app_name:
app = _app
if app:
menu.append(app.build_menu())
if opflame.app_framework:
menu_auto_refresh = opflame.app_framework.prefs_global.get(
'menu_auto_refresh', {})
if menu_auto_refresh.get('timeline_menu', True):
try:
import flame # noqa
flame.schedule_idle_event(rescan_hooks)
except ImportError:
print("!-!!! not able to import flame module !!!!")
return menu
""" Flame hooks are starting here
"""
def project_saved(project_name, save_time, is_auto_save):
"""Hook to activate when project is saved
Args:
project_name (str): name of project
save_time (str): time when it was saved
is_auto_save (bool): autosave is on or off
"""
if opflame.app_framework:
opflame.app_framework.save_prefs()
def get_main_menu_custom_ui_actions():
"""Hook to create submenu in start menu
Returns:
list: menu object
"""
# install openpype and the host
openpype_install()
return _build_app_menu("FlameMenuProjectConnect")
def get_timeline_custom_ui_actions():
"""Hook to create submenu in timeline
Returns:
list: menu object
"""
# install openpype and the host
openpype_install()
return _build_app_menu("FlameMenuTimeline")

View file

@ -437,7 +437,8 @@ def empty_sets(sets, force=False):
cmds.connectAttr(src, dest)
# Restore original members
for origin_set, members in original.iteritems():
_iteritems = getattr(original, "iteritems", original.items)
for origin_set, members in _iteritems():
cmds.sets(members, forceElement=origin_set)
@ -581,7 +582,7 @@ def get_shader_assignments_from_shapes(shapes, components=True):
# Build a mapping from parent to shapes to include in lookup.
transforms = {shape.rsplit("|", 1)[0]: shape for shape in shapes}
lookup = set(shapes + transforms.keys())
lookup = set(shapes) | set(transforms.keys())
component_assignments = defaultdict(list)
for shading_group in assignments.keys():
@ -669,7 +670,8 @@ def displaySmoothness(nodes,
yield
finally:
# Revert state
for node, state in originals.iteritems():
_iteritems = getattr(originals, "iteritems", originals.items)
for node, state in _iteritems():
if state:
cmds.displaySmoothness(node, **state)
@ -712,7 +714,8 @@ def no_display_layers(nodes):
yield
finally:
# Restore original members
for layer, members in original.iteritems():
_iteritems = getattr(original, "iteritems", original.items)
for layer, members in _iteritems():
cmds.editDisplayLayerMembers(layer, members, noRecurse=True)

View file

@ -21,10 +21,8 @@ def _get_menu(menu_name=None):
if menu_name is None:
menu_name = pipeline._menu
widgets = dict((
w.objectName(), w) for w in QtWidgets.QApplication.allWidgets())
menu = widgets.get(menu_name)
return menu
widgets = {w.objectName(): w for w in QtWidgets.QApplication.allWidgets()}
return widgets.get(menu_name)
def deferred():
@ -46,6 +44,43 @@ def deferred():
)
)
def add_experimental_item():
cmds.menuItem(
"Experimental tools...",
parent=pipeline._menu,
command=lambda *args: host_tools.show_experimental_tools_dialog(
pipeline._parent
)
)
def add_scripts_menu():
try:
import scriptsmenu.launchformaya as launchformaya
except ImportError:
log.warning(
"Skipping studio.menu install, because "
"'scriptsmenu' module seems unavailable."
)
return
# load configuration of custom menu
project_settings = get_project_settings(os.getenv("AVALON_PROJECT"))
config = project_settings["maya"]["scriptsmenu"]["definition"]
_menu = project_settings["maya"]["scriptsmenu"]["name"]
if not config:
log.warning("Skipping studio menu, no definition found.")
return
# run the launcher for Maya menu
studio_menu = launchformaya.main(
title=_menu.title(),
objectName=_menu.title().lower().replace(" ", "_")
)
# apply configuration
studio_menu.build_from_configuration(studio_menu, config)
def modify_workfiles():
# Find the pipeline menu
top_menu = _get_menu()
@ -101,38 +136,13 @@ def deferred():
log.info("Attempting to install scripts menu ...")
# add_scripts_menu()
add_build_workfiles_item()
add_look_assigner_item()
add_experimental_item()
modify_workfiles()
remove_project_manager()
try:
import scriptsmenu.launchformaya as launchformaya
import scriptsmenu.scriptsmenu as scriptsmenu
except ImportError:
log.warning(
"Skipping studio.menu install, because "
"'scriptsmenu' module seems unavailable."
)
return
# load configuration of custom menu
project_settings = get_project_settings(os.getenv("AVALON_PROJECT"))
config = project_settings["maya"]["scriptsmenu"]["definition"]
_menu = project_settings["maya"]["scriptsmenu"]["name"]
if not config:
log.warning("Skipping studio menu, no definition found.")
return
# run the launcher for Maya menu
studio_menu = launchformaya.main(
title=_menu.title(),
objectName=_menu.title().lower().replace(" ", "_")
)
# apply configuration
studio_menu.build_from_configuration(studio_menu, config)
add_scripts_menu()
def uninstall():
@ -153,7 +163,7 @@ def install():
return
# Allow time for uninstallation to finish.
cmds.evalDeferred(deferred)
cmds.evalDeferred(deferred, lowestPriority=True)
def popup():

View file

@ -5,6 +5,7 @@ import os
import contextlib
import copy
import six
from maya import cmds
from avalon import api, io
@ -69,7 +70,8 @@ def unlocked(nodes):
yield
finally:
# Reapply original states
for uuid, state in states.iteritems():
_iteritems = getattr(states, "iteritems", states.items)
for uuid, state in _iteritems():
nodes_from_id = cmds.ls(uuid, long=True)
if nodes_from_id:
node = nodes_from_id[0]
@ -94,7 +96,7 @@ def load_package(filepath, name, namespace=None):
# Define a unique namespace for the package
namespace = os.path.basename(filepath).split(".")[0]
unique_namespace(namespace)
assert isinstance(namespace, basestring)
assert isinstance(namespace, six.string_types)
# Load the setdress package data
with open(filepath, "r") as fp:

View file

@ -183,7 +183,8 @@ class ExtractFBX(openpype.api.Extractor):
# Apply the FBX overrides through MEL since the commands
# only work correctly in MEL according to online
# available discussions on the topic
for option, value in options.iteritems():
_iteritems = getattr(options, "iteritems", options.items)
for option, value in _iteritems():
key = option[0].upper() + option[1:] # uppercase first letter
# Boolean must be passed as lower-case strings

View file

@ -383,7 +383,7 @@ class MayaSubmitMuster(pyblish.api.InstancePlugin):
"attributes": {
"environmental_variables": {
"value": ", ".join("{!s}={!r}".format(k, v)
for (k, v) in env.iteritems()),
for (k, v) in env.items()),
"state": True,
"subst": False

View file

@ -2,6 +2,8 @@ import pyblish.api
import openpype.api
import string
import six
# Allow only characters, numbers and underscore
allowed = set(string.ascii_lowercase +
string.ascii_uppercase +
@ -29,7 +31,7 @@ class ValidateSubsetName(pyblish.api.InstancePlugin):
raise RuntimeError("Instance is missing subset "
"name: {0}".format(subset))
if not isinstance(subset, basestring):
if not isinstance(subset, six.string_types):
raise TypeError("Instance subset name must be string, "
"got: {0} ({1})".format(subset, type(subset)))

View file

@ -52,7 +52,8 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin):
# Take only the ids with more than one member
invalid = list()
for _ids, members in ids.iteritems():
_iteritems = getattr(ids, "iteritems", ids.items)
for _ids, members in _iteritems():
if len(members) > 1:
cls.log.error("ID found on multiple nodes: '%s'" % members)
invalid.extend(members)

View file

@ -32,7 +32,10 @@ class ValidateNodeNoGhosting(pyblish.api.InstancePlugin):
nodes = cmds.ls(instance, long=True, type=['transform', 'shape'])
invalid = []
for node in nodes:
for attr, required_value in cls._attributes.iteritems():
_iteritems = getattr(
cls._attributes, "iteritems", cls._attributes.items
)
for attr, required_value in _iteritems():
if cmds.attributeQuery(attr, node=node, exists=True):
value = cmds.getAttr('{0}.{1}'.format(node, attr))

View file

@ -33,7 +33,8 @@ class ValidateShapeRenderStats(pyblish.api.Validator):
shapes = cmds.ls(instance, long=True, type='surfaceShape')
invalid = []
for shape in shapes:
for attr, default_value in cls.defaults.iteritems():
_iteritems = getattr(cls.defaults, "iteritems", cls.defaults.items)
for attr, default_value in _iteritems():
if cmds.attributeQuery(attr, node=shape, exists=True):
value = cmds.getAttr('{}.{}'.format(shape, attr))
if value != default_value:
@ -52,7 +53,8 @@ class ValidateShapeRenderStats(pyblish.api.Validator):
@classmethod
def repair(cls, instance):
for shape in cls.get_invalid(instance):
for attr, default_value in cls.defaults.iteritems():
_iteritems = getattr(cls.defaults, "iteritems", cls.defaults.items)
for attr, default_value in _iteritems():
if cmds.attributeQuery(attr, node=shape, exists=True):
plug = '{0}.{1}'.format(shape, attr)

View file

@ -84,6 +84,12 @@ def install():
)
log.debug("Adding menu item: {}".format(name))
# Add experimental tools action
menu.addSeparator()
menu.addCommand(
"Experimental tools...",
host_tools.show_experimental_tools_dialog
)
# adding shortcuts
add_shortcuts_from_presets()

View file

@ -6,7 +6,6 @@ from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name
stub = photoshop.stub()
class ImageLoader(api.Loader):
"""Load images
@ -21,7 +20,7 @@ class ImageLoader(api.Loader):
context["asset"]["name"],
name)
with photoshop.maintained_selection():
layer = stub.import_smart_object(self.fname, layer_name)
layer = self.import_layer(self.fname, layer_name)
self[:] = [layer]
namespace = namespace or layer_name
@ -45,8 +44,9 @@ class ImageLoader(api.Loader):
layer_name = "{}_{}".format(context["asset"], context["subset"])
# switching assets
if namespace_from_container != layer_name:
layer_name = self._get_unique_layer_name(context["asset"],
context["subset"])
layer_name = get_unique_layer_name(stub.get_layers(),
context["asset"],
context["subset"])
else: # switching version - keep same name
layer_name = container["namespace"]
@ -72,3 +72,6 @@ class ImageLoader(api.Loader):
def switch(self, container, representation):
self.update(container, representation)
def import_layer(self, file_name, layer_name):
return stub.import_smart_object(file_name, layer_name)

View file

@ -0,0 +1,82 @@
import re
from avalon import api, photoshop
from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name
stub = photoshop.stub()
class ReferenceLoader(api.Loader):
"""Load reference images
Stores the imported asset in a container named after the asset.
Inheriting from 'load_image' didn't work because of
"Cannot write to closing transport", possible refactor.
"""
families = ["image", "render"]
representations = ["*"]
def load(self, context, name=None, namespace=None, data=None):
layer_name = get_unique_layer_name(stub.get_layers(),
context["asset"]["name"],
name)
with photoshop.maintained_selection():
layer = self.import_layer(self.fname, layer_name)
self[:] = [layer]
namespace = namespace or layer_name
return photoshop.containerise(
name,
namespace,
layer,
context,
self.__class__.__name__
)
def update(self, container, representation):
""" Switch asset or change version """
layer = container.pop("layer")
context = representation.get("context", {})
namespace_from_container = re.sub(r'_\d{3}$', '',
container["namespace"])
layer_name = "{}_{}".format(context["asset"], context["subset"])
# switching assets
if namespace_from_container != layer_name:
layer_name = get_unique_layer_name(stub.get_layers(),
context["asset"],
context["subset"])
else: # switching version - keep same name
layer_name = container["namespace"]
path = api.get_representation_path(representation)
with photoshop.maintained_selection():
stub.replace_smart_object(
layer, path, layer_name
)
stub.imprint(
layer, {"representation": str(representation["_id"])}
)
def remove(self, container):
"""
Removes element from scene: deletes layer + removes from Headline
Args:
container (dict): container to be removed - used to get layer_id
"""
layer = container.pop("layer")
stub.imprint(layer, {})
stub.delete_layer(layer.id)
def switch(self, container, representation):
self.update(container, representation)
def import_layer(self, file_name, layer_name):
return stub.import_smart_object(file_name, layer_name,
as_reference=True)

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
"""Close PS after publish. For Webpublishing only."""
import os
import pyblish.api
from avalon import photoshop
class ClosePS(pyblish.api.ContextPlugin):
"""Close PS after publish. For Webpublishing only.
"""
order = pyblish.api.IntegratorOrder + 14
label = "Close PS"
optional = True
active = True
hosts = ["photoshop"]
def process(self, context):
self.log.info("ClosePS")
if not os.environ.get("IS_HEADLESS"):
return
stub = photoshop.stub()
self.log.info("Shutting down PS")
stub.save()
stub.close()
self.log.info("PS closed")

View file

@ -0,0 +1,135 @@
import pyblish.api
import os
import re
from avalon import photoshop
from openpype.lib import prepare_template_data
from openpype.lib.plugin_tools import parse_json
class CollectRemoteInstances(pyblish.api.ContextPlugin):
"""Gather instances configured color code of a layer.
Used in remote publishing when artists marks publishable layers by color-
coding.
Identifier:
id (str): "pyblish.avalon.instance"
"""
order = pyblish.api.CollectorOrder + 0.100
label = "Instances"
order = pyblish.api.CollectorOrder
hosts = ["photoshop"]
# configurable by Settings
color_code_mapping = []
def process(self, context):
self.log.info("CollectRemoteInstances")
self.log.info("mapping:: {}".format(self.color_code_mapping))
if not os.environ.get("IS_HEADLESS"):
self.log.debug("Not headless publishing, skipping.")
return
# parse variant if used in webpublishing, comes from webpublisher batch
batch_dir = os.environ.get("OPENPYPE_PUBLISH_DATA")
variant = "Main"
if batch_dir and os.path.exists(batch_dir):
# TODO check if batch manifest is same as tasks manifests
task_data = parse_json(os.path.join(batch_dir,
"manifest.json"))
if not task_data:
raise ValueError(
"Cannot parse batch meta in {} folder".format(batch_dir))
variant = task_data["variant"]
stub = photoshop.stub()
layers = stub.get_layers()
instance_names = []
for layer in layers:
self.log.info("Layer:: {}".format(layer))
resolved_family, resolved_subset_template = self._resolve_mapping(
layer
)
self.log.info("resolved_family {}".format(resolved_family))
self.log.info("resolved_subset_template {}".format(
resolved_subset_template))
if not resolved_subset_template or not resolved_family:
self.log.debug("!!! Not marked, skip")
continue
if layer.parents:
self.log.debug("!!! Not a top layer, skip")
continue
instance = context.create_instance(layer.name)
instance.append(layer)
instance.data["family"] = resolved_family
instance.data["publish"] = layer.visible
instance.data["asset"] = context.data["assetEntity"]["name"]
instance.data["task"] = context.data["taskType"]
fill_pairs = {
"variant": variant,
"family": instance.data["family"],
"task": instance.data["task"],
"layer": layer.name
}
subset = resolved_subset_template.format(
**prepare_template_data(fill_pairs))
instance.data["subset"] = subset
instance_names.append(layer.name)
# Produce diagnostic message for any graphical
# user interface interested in visualising it.
self.log.info("Found: \"%s\" " % instance.data["name"])
self.log.info("instance: {} ".format(instance.data))
if len(instance_names) != len(set(instance_names)):
self.log.warning("Duplicate instances found. " +
"Remove unwanted via SubsetManager")
def _resolve_mapping(self, layer):
"""Matches 'layer' color code and name to mapping.
If both color code AND name regex is configured, BOTH must be valid
If layer matches to multiple mappings, only first is used!
"""
family_list = []
family = None
subset_name_list = []
resolved_subset_template = None
for mapping in self.color_code_mapping:
if mapping["color_code"] and \
layer.color_code not in mapping["color_code"]:
continue
if mapping["layer_name_regex"] and \
not any(re.search(pattern, layer.name)
for pattern in mapping["layer_name_regex"]):
continue
family_list.append(mapping["family"])
subset_name_list.append(mapping["subset_template_name"])
if len(subset_name_list) > 1:
self.log.warning("Multiple mappings found for '{}'".
format(layer.name))
self.log.warning("Only first subset name template used!")
subset_name_list[:] = subset_name_list[0]
if len(family_list) > 1:
self.log.warning("Multiple mappings found for '{}'".
format(layer.name))
self.log.warning("Only first family used!")
family_list[:] = family_list[0]
if subset_name_list:
resolved_subset_template = subset_name_list.pop()
if family_list:
family = family_list.pop()
return family, resolved_subset_template

View file

@ -12,7 +12,7 @@ class ExtractImage(openpype.api.Extractor):
label = "Extract Image"
hosts = ["photoshop"]
families = ["image"]
families = ["image", "background"]
formats = ["png", "jpg"]
def process(self, instance):

View file

@ -0,0 +1,37 @@
import pyblish.api
import openpype.api
import os
class ValidateSources(pyblish.api.InstancePlugin):
"""Validates source files.
Loops through all 'files' in 'stagingDir' if actually exist. They might
got deleted between starting of SP and now.
"""
order = openpype.api.ValidateContentsOrder
label = "Check source files"
optional = True # only for unforeseeable cases
hosts = ["standalonepublisher"]
def process(self, instance):
self.log.info("instance {}".format(instance.data))
for repre in instance.data.get("representations") or []:
files = []
if isinstance(repre["files"], str):
files.append(repre["files"])
else:
files = list(repre["files"])
for file_name in files:
source_file = os.path.join(repre["stagingDir"],
file_name)
if not os.path.exists(source_file):
raise ValueError("File {} not found".format(source_file))

View file

@ -253,6 +253,7 @@ def create_unreal_project(project_name: str,
"Plugins": [
{"Name": "PythonScriptPlugin", "Enabled": True},
{"Name": "EditorScriptingUtilities", "Enabled": True},
{"Name": "SequencerScripting", "Enabled": True},
{"Name": "Avalon", "Enabled": True}
]
}

View file

@ -6,7 +6,9 @@ from pathlib import Path
from openpype.lib import (
PreLaunchHook,
ApplicationLaunchFailed,
ApplicationNotFound
ApplicationNotFound,
get_workdir_data,
get_workfile_template_key
)
from openpype.hosts.unreal.api import lib as unreal_lib
@ -25,13 +27,46 @@ class UnrealPrelaunchHook(PreLaunchHook):
self.signature = "( {} )".format(self.__class__.__name__)
def _get_work_filename(self):
# Use last workfile if was found
if self.data.get("last_workfile_path"):
last_workfile = Path(self.data.get("last_workfile_path"))
if last_workfile and last_workfile.exists():
return last_workfile.name
# Prepare data for fill data and for getting workfile template key
task_name = self.data["task_name"]
anatomy = self.data["anatomy"]
asset_doc = self.data["asset_doc"]
project_doc = self.data["project_doc"]
asset_tasks = asset_doc.get("data", {}).get("tasks") or {}
task_info = asset_tasks.get(task_name) or {}
task_type = task_info.get("type")
workdir_data = get_workdir_data(
project_doc, asset_doc, task_name, self.host_name
)
# QUESTION raise exception if version is part of filename template?
workdir_data["version"] = 1
workdir_data["ext"] = "uproject"
# Get workfile template key for current context
workfile_template_key = get_workfile_template_key(
task_type,
self.host_name,
project_name=project_doc["name"]
)
# Fill templates
filled_anatomy = anatomy.format(workdir_data)
# Return filename
return filled_anatomy[workfile_template_key]["file"]
def execute(self):
"""Hook entry method."""
asset_name = self.data["asset_name"]
task_name = self.data["task_name"]
workdir = self.launch_context.env["AVALON_WORKDIR"]
engine_version = self.app_name.split("/")[-1].replace("-", ".")
unreal_project_name = f"{asset_name}_{task_name}"
try:
if int(engine_version.split(".")[0]) < 4 and \
int(engine_version.split(".")[1]) < 26:
@ -45,6 +80,8 @@ class UnrealPrelaunchHook(PreLaunchHook):
# so lets keep it quite.
...
unreal_project_filename = self._get_work_filename()
unreal_project_name = os.path.splitext(unreal_project_filename)[0]
# Unreal is sensitive about project names longer then 20 chars
if len(unreal_project_name) > 20:
self.log.warning((
@ -89,10 +126,10 @@ class UnrealPrelaunchHook(PreLaunchHook):
ue4_path = unreal_lib.get_editor_executable_path(
Path(detected[engine_version]))
self.launch_context.launch_args.append(ue4_path.as_posix())
self.launch_context.launch_args = [ue4_path.as_posix()]
project_path.mkdir(parents=True, exist_ok=True)
project_file = project_path / f"{unreal_project_name}.uproject"
project_file = project_path / unreal_project_filename
if not project_file.is_file():
engine_path = detected[engine_version]
self.log.info((

View file

@ -0,0 +1,43 @@
import unreal
from unreal import EditorAssetLibrary as eal
from unreal import EditorLevelLibrary as ell
from openpype.hosts.unreal.api.plugin import Creator
from avalon.unreal import (
instantiate,
)
class CreateCamera(Creator):
"""Layout output for character rigs"""
name = "layoutMain"
label = "Camera"
family = "camera"
icon = "cubes"
root = "/Game/Avalon/Instances"
suffix = "_INS"
def __init__(self, *args, **kwargs):
super(CreateCamera, self).__init__(*args, **kwargs)
def process(self):
data = self.data
name = data["subset"]
data["level"] = ell.get_editor_world().get_path_name()
if not eal.does_directory_exist(self.root):
eal.make_directory(self.root)
factory = unreal.LevelSequenceFactoryNew()
tools = unreal.AssetToolsHelpers().get_asset_tools()
tools.create_asset(name, f"{self.root}/{name}", None, factory)
asset_name = f"{self.root}/{name}/{name}.{name}"
data["members"] = [asset_name]
instantiate(f"{self.root}", name, data, None, self.suffix)

View file

@ -0,0 +1,206 @@
import os
from avalon import api, io, pipeline
from avalon.unreal import lib
from avalon.unreal import pipeline as unreal_pipeline
import unreal
class CameraLoader(api.Loader):
"""Load Unreal StaticMesh from FBX"""
families = ["camera"]
label = "Load Camera"
representations = ["fbx"]
icon = "cube"
color = "orange"
def load(self, context, name, namespace, data):
"""
Load and containerise representation into Content Browser.
This is two step process. First, import FBX to temporary path and
then call `containerise()` on it - this moves all content to new
directory and then it will create AssetContainer there and imprint it
with metadata. This will mark this path as container.
Args:
context (dict): application context
name (str): subset name
namespace (str): in Unreal this is basically path to container.
This is not passed here, so namespace is set
by `containerise()` because only then we know
real path.
data (dict): Those would be data to be imprinted. This is not used
now, data are imprinted by `containerise()`.
Returns:
list(str): list of container content
"""
# Create directory for asset and avalon container
root = "/Game/Avalon/Assets"
asset = context.get('asset').get('name')
suffix = "_CON"
if asset:
asset_name = "{}_{}".format(asset, name)
else:
asset_name = "{}".format(name)
tools = unreal.AssetToolsHelpers().get_asset_tools()
unique_number = 1
if unreal.EditorAssetLibrary.does_directory_exist(f"{root}/{asset}"):
asset_content = unreal.EditorAssetLibrary.list_assets(
f"{root}/{asset}", recursive=False, include_folder=True
)
# Get highest number to make a unique name
folders = [a for a in asset_content
if a[-1] == "/" and f"{name}_" in a]
f_numbers = []
for f in folders:
# Get number from folder name. Splits the string by "_" and
# removes the last element (which is a "/").
f_numbers.append(int(f.split("_")[-1][:-1]))
f_numbers.sort()
if not f_numbers:
unique_number = 1
else:
unique_number = f_numbers[-1] + 1
asset_dir, container_name = tools.create_unique_asset_name(
f"{root}/{asset}/{name}_{unique_number:02d}", suffix="")
container_name += suffix
unreal.EditorAssetLibrary.make_directory(asset_dir)
sequence = tools.create_asset(
asset_name=asset_name,
package_path=asset_dir,
asset_class=unreal.LevelSequence,
factory=unreal.LevelSequenceFactoryNew()
)
io_asset = io.Session["AVALON_ASSET"]
asset_doc = io.find_one({
"type": "asset",
"name": io_asset
})
data = asset_doc.get("data")
if data:
sequence.set_display_rate(unreal.FrameRate(data.get("fps"), 1.0))
sequence.set_playback_start(data.get("frameStart"))
sequence.set_playback_end(data.get("frameEnd"))
settings = unreal.MovieSceneUserImportFBXSettings()
settings.set_editor_property('reduce_keys', False)
unreal.SequencerTools.import_fbx(
unreal.EditorLevelLibrary.get_editor_world(),
sequence,
sequence.get_bindings(),
settings,
self.fname
)
# Create Asset Container
lib.create_avalon_container(container=container_name, path=asset_dir)
data = {
"schema": "openpype:container-2.0",
"id": pipeline.AVALON_CONTAINER_ID,
"asset": asset,
"namespace": asset_dir,
"container_name": container_name,
"asset_name": asset_name,
"loader": str(self.__class__.__name__),
"representation": context["representation"]["_id"],
"parent": context["representation"]["parent"],
"family": context["representation"]["context"]["family"]
}
unreal_pipeline.imprint(
"{}/{}".format(asset_dir, container_name), data)
asset_content = unreal.EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=True
)
for a in asset_content:
unreal.EditorAssetLibrary.save_asset(a)
return asset_content
def update(self, container, representation):
path = container["namespace"]
ar = unreal.AssetRegistryHelpers.get_asset_registry()
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_content = unreal.EditorAssetLibrary.list_assets(
path, recursive=False, include_folder=False
)
asset_name = ""
for a in asset_content:
asset = ar.get_asset_by_object_path(a)
if a.endswith("_CON"):
loaded_asset = unreal.EditorAssetLibrary.load_asset(a)
unreal.EditorAssetLibrary.set_metadata_tag(
loaded_asset, "representation", str(representation["_id"])
)
unreal.EditorAssetLibrary.set_metadata_tag(
loaded_asset, "parent", str(representation["parent"])
)
asset_name = unreal.EditorAssetLibrary.get_metadata_tag(
loaded_asset, "asset_name"
)
elif asset.asset_class == "LevelSequence":
unreal.EditorAssetLibrary.delete_asset(a)
sequence = tools.create_asset(
asset_name=asset_name,
package_path=path,
asset_class=unreal.LevelSequence,
factory=unreal.LevelSequenceFactoryNew()
)
io_asset = io.Session["AVALON_ASSET"]
asset_doc = io.find_one({
"type": "asset",
"name": io_asset
})
data = asset_doc.get("data")
if data:
sequence.set_display_rate(unreal.FrameRate(data.get("fps"), 1.0))
sequence.set_playback_start(data.get("frameStart"))
sequence.set_playback_end(data.get("frameEnd"))
settings = unreal.MovieSceneUserImportFBXSettings()
settings.set_editor_property('reduce_keys', False)
unreal.SequencerTools.import_fbx(
unreal.EditorLevelLibrary.get_editor_world(),
sequence,
sequence.get_bindings(),
settings,
str(representation["data"]["path"])
)
def remove(self, container):
path = container["namespace"]
parent_path = os.path.dirname(path)
unreal.EditorAssetLibrary.delete_directory(path)
asset_content = unreal.EditorAssetLibrary.list_assets(
parent_path, recursive=False, include_folder=True
)
if len(asset_content) == 0:
unreal.EditorAssetLibrary.delete_directory(parent_path)

View file

@ -0,0 +1,54 @@
import os
import unreal
from unreal import EditorAssetLibrary as eal
from unreal import EditorLevelLibrary as ell
import openpype.api
class ExtractCamera(openpype.api.Extractor):
"""Extract a camera."""
label = "Extract Camera"
hosts = ["unreal"]
families = ["camera"]
optional = True
def process(self, instance):
# Define extract output file path
stagingdir = self.staging_dir(instance)
fbx_filename = "{}.fbx".format(instance.name)
# Perform extraction
self.log.info("Performing extraction..")
# Check if the loaded level is the same of the instance
current_level = ell.get_editor_world().get_path_name()
assert current_level == instance.data.get("level"), \
"Wrong level loaded"
for member in instance[:]:
data = eal.find_asset_data(member)
if data.asset_class == "LevelSequence":
ar = unreal.AssetRegistryHelpers.get_asset_registry()
sequence = ar.get_asset_by_object_path(member).get_asset()
unreal.SequencerTools.export_fbx(
ell.get_editor_world(),
sequence,
sequence.get_bindings(),
unreal.FbxExportOption(),
os.path.join(stagingdir, fbx_filename)
)
break
if "representations" not in instance.data:
instance.data["representations"] = []
fbx_representation = {
'name': 'fbx',
'ext': 'fbx',
'files': fbx_filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(fbx_representation)

View file

@ -15,6 +15,7 @@ import tempfile
import pyblish.api
from avalon import io
from openpype.lib import prepare_template_data
from openpype.lib.plugin_tools import parse_json, get_batch_asset_task_info
class CollectPublishedFiles(pyblish.api.ContextPlugin):
@ -33,22 +34,6 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
# from Settings
task_type_to_family = {}
def _load_json(self, path):
path = path.strip('\"')
assert os.path.isfile(path), (
"Path to json file doesn't exist. \"{}\"".format(path)
)
data = None
with open(path, "r") as json_file:
try:
data = json.load(json_file)
except Exception as exc:
self.log.error(
"Error loading json: "
"{} - Exception: {}".format(path, exc)
)
return data
def _process_batch(self, dir_url):
task_subfolders = [
os.path.join(dir_url, o)
@ -56,22 +41,15 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
if os.path.isdir(os.path.join(dir_url, o))]
self.log.info("task_sub:: {}".format(task_subfolders))
for task_dir in task_subfolders:
task_data = self._load_json(os.path.join(task_dir,
"manifest.json"))
task_data = parse_json(os.path.join(task_dir,
"manifest.json"))
self.log.info("task_data:: {}".format(task_data))
ctx = task_data["context"]
task_type = "default_task_type"
task_name = None
if ctx["type"] == "task":
items = ctx["path"].split('/')
asset = items[-2]
os.environ["AVALON_TASK"] = ctx["name"]
task_name = ctx["name"]
task_type = ctx["attributes"]["type"]
else:
asset = ctx["name"]
os.environ["AVALON_TASK"] = ""
asset, task_name, task_type = get_batch_asset_task_info(ctx)
if task_name:
os.environ["AVALON_TASK"] = task_name
is_sequence = len(task_data["files"]) > 1
@ -261,7 +239,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin):
assert batch_dir, (
"Missing `OPENPYPE_PUBLISH_DATA`")
assert batch_dir, \
assert os.path.exists(batch_dir), \
"Folder {} doesn't exist".format(batch_dir)
project_name = os.environ.get("AVALON_PROJECT")

View file

@ -11,6 +11,7 @@ from avalon.api import AvalonMongoDB
from openpype.lib import OpenPypeMongoConnection
from openpype_modules.avalon_apps.rest_api import _RestApiEndpoint
from openpype.lib.plugin_tools import parse_json
from openpype.lib import PypeLogger
@ -175,6 +176,9 @@ class TaskNode(Node):
class WebpublisherBatchPublishEndpoint(_RestApiEndpoint):
"""Triggers headless publishing of batch."""
async def post(self, request) -> Response:
# for postprocessing in host, currently only PS
host_map = {"photoshop": [".psd", ".psb"]}
output = {}
log.info("WebpublisherBatchPublishEndpoint called")
content = await request.json()
@ -182,10 +186,44 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint):
batch_path = os.path.join(self.resource.upload_dir,
content["batch"])
add_args = {
"host": "webpublisher",
"project": content["project_name"],
"user": content["user"]
}
command = "remotepublish"
if content.get("studio_processing"):
log.info("Post processing called")
batch_data = parse_json(os.path.join(batch_path, "manifest.json"))
if not batch_data:
raise ValueError(
"Cannot parse batch meta in {} folder".format(batch_path))
task_dir_name = batch_data["tasks"][0]
task_data = parse_json(os.path.join(batch_path, task_dir_name,
"manifest.json"))
if not task_data:
raise ValueError(
"Cannot parse batch meta in {} folder".format(task_data))
command = "remotepublishfromapp"
for host, extensions in host_map.items():
for ext in extensions:
for file_name in task_data["files"]:
if ext in file_name:
add_args["host"] = host
break
if not add_args.get("host"):
raise ValueError(
"Couldn't discern host from {}".format(task_data["files"]))
openpype_app = self.resource.executable
args = [
openpype_app,
'remotepublish',
command,
batch_path
]
@ -193,12 +231,6 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint):
msg = "Non existent OpenPype executable {}".format(openpype_app)
raise RuntimeError(msg)
add_args = {
"host": "webpublisher",
"project": content["project_name"],
"user": content["user"]
}
for key, value in add_args.items():
args.append("--{}".format(key))
args.append(value)

View file

@ -461,13 +461,8 @@ class ApplicationExecutable:
# On MacOS check if exists path to executable when ends with `.app`
# - it is common that path will lead to "/Applications/Blender" but
# real path is "/Applications/Blender.app"
if (
platform.system().lower() == "darwin"
and not os.path.exists(executable)
):
_executable = executable + ".app"
if os.path.exists(_executable):
executable = _executable
if platform.system().lower() == "darwin":
executable = self.macos_executable_prep(executable)
self.executable_path = executable
@ -477,6 +472,45 @@ class ApplicationExecutable:
def __repr__(self):
return "<{}> {}".format(self.__class__.__name__, self.executable_path)
@staticmethod
def macos_executable_prep(executable):
"""Try to find full path to executable file.
Real executable is stored in '*.app/Contents/MacOS/<executable>'.
Having path to '*.app' gives ability to read it's plist info and
use "CFBundleExecutable" key from plist to know what is "executable."
Plist is stored in '*.app/Contents/Info.plist'.
This is because some '*.app' directories don't have same permissions
as real executable.
"""
# Try to find if there is `.app` file
if not os.path.exists(executable):
_executable = executable + ".app"
if os.path.exists(_executable):
executable = _executable
# Try to find real executable if executable has `Contents` subfolder
contents_dir = os.path.join(executable, "Contents")
if os.path.exists(contents_dir):
executable_filename = None
# Load plist file and check for bundle executable
plist_filepath = os.path.join(contents_dir, "Info.plist")
if os.path.exists(plist_filepath):
import plistlib
parsed_plist = plistlib.readPlist(plist_filepath)
executable_filename = parsed_plist.get("CFBundleExecutable")
if executable_filename:
executable = os.path.join(
contents_dir, "MacOS", executable_filename
)
return executable
def as_args(self):
return [self.executable_path]

View file

@ -487,3 +487,48 @@ def should_decompress(file_url):
"compression: \"dwab\"" in output
return False
def parse_json(path):
"""Parses json file at 'path' location
Returns:
(dict) or None if unparsable
Raises:
AsssertionError if 'path' doesn't exist
"""
path = path.strip('\"')
assert os.path.isfile(path), (
"Path to json file doesn't exist. \"{}\"".format(path)
)
data = None
with open(path, "r") as json_file:
try:
data = json.load(json_file)
except Exception as exc:
log.error(
"Error loading json: "
"{} - Exception: {}".format(path, exc)
)
return data
def get_batch_asset_task_info(ctx):
"""Parses context data from webpublisher's batch metadata
Returns:
(tuple): asset, task_name (Optional), task_type
"""
task_type = "default_task_type"
task_name = None
asset = None
if ctx["type"] == "task":
items = ctx["path"].split('/')
asset = items[-2]
task_name = ctx["name"]
task_type = ctx["attributes"]["type"]
else:
asset = ctx["name"]
return asset, task_name, task_type

View file

@ -0,0 +1,159 @@
import os
from datetime import datetime
import sys
from bson.objectid import ObjectId
import pyblish.util
import pyblish.api
from openpype import uninstall
from openpype.lib.mongo import OpenPypeMongoConnection
def get_webpublish_conn():
"""Get connection to OP 'webpublishes' collection."""
mongo_client = OpenPypeMongoConnection.get_mongo_client()
database_name = os.environ["OPENPYPE_DATABASE_NAME"]
return mongo_client[database_name]["webpublishes"]
def start_webpublish_log(dbcon, batch_id, user):
"""Start new log record for 'batch_id'
Args:
dbcon (OpenPypeMongoConnection)
batch_id (str)
user (str)
Returns
(ObjectId) from DB
"""
return dbcon.insert_one({
"batch_id": batch_id,
"start_date": datetime.now(),
"user": user,
"status": "in_progress"
}).inserted_id
def publish_and_log(dbcon, _id, log, close_plugin_name=None):
"""Loops through all plugins, logs ok and fails into OP DB.
Args:
dbcon (OpenPypeMongoConnection)
_id (str)
log (OpenPypeLogger)
close_plugin_name (str): name of plugin with responsibility to
close host app
"""
# Error exit as soon as any error occurs.
error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}"
close_plugin = _get_close_plugin(close_plugin_name, log)
if isinstance(_id, str):
_id = ObjectId(_id)
log_lines = []
for result in pyblish.util.publish_iter():
for record in result["records"]:
log_lines.append("{}: {}".format(
result["plugin"].label, record.msg))
if result["error"]:
log.error(error_format.format(**result))
uninstall()
log_lines.append(error_format.format(**result))
dbcon.update_one(
{"_id": _id},
{"$set":
{
"finish_date": datetime.now(),
"status": "error",
"log": os.linesep.join(log_lines)
}}
)
if close_plugin: # close host app explicitly after error
context = pyblish.api.Context()
close_plugin().process(context)
sys.exit(1)
else:
dbcon.update_one(
{"_id": _id},
{"$set":
{
"progress": max(result["progress"], 0.95),
"log": os.linesep.join(log_lines)
}}
)
# final update
dbcon.update_one(
{"_id": _id},
{"$set":
{
"finish_date": datetime.now(),
"status": "finished_ok",
"progress": 1,
"log": os.linesep.join(log_lines)
}}
)
def fail_batch(_id, batches_in_progress, dbcon):
"""Set current batch as failed as there are some stuck batches."""
running_batches = [str(batch["_id"])
for batch in batches_in_progress
if batch["_id"] != _id]
msg = "There are still running batches {}\n". \
format("\n".join(running_batches))
msg += "Ask admin to check them and reprocess current batch"
dbcon.update_one(
{"_id": _id},
{"$set":
{
"finish_date": datetime.now(),
"status": "error",
"log": msg
}}
)
raise ValueError(msg)
def find_variant_key(application_manager, host):
"""Searches for latest installed variant for 'host'
Args:
application_manager (ApplicationManager)
host (str)
Returns
(string) (optional)
Raises:
(ValueError) if no variant found
"""
app_group = application_manager.app_groups.get(host)
if not app_group or not app_group.enabled:
raise ValueError("No application {} configured".format(host))
found_variant_key = None
# finds most up-to-date variant if any installed
for variant_key, variant in app_group.variants.items():
for executable in variant.executables:
if executable.exists():
found_variant_key = variant_key
if not found_variant_key:
raise ValueError("No executable for {} found".format(host))
return found_variant_key
def _get_close_plugin(close_plugin_name, log):
if close_plugin_name:
plugins = pyblish.api.discover()
for plugin in plugins:
if plugin.__name__ == close_plugin_name:
return plugin
log.warning("Close plugin not found, app might not close.")

View file

@ -1,6 +1,5 @@
import os
import openpype
from openpype import resources
from openpype.modules import OpenPypeModule
from openpype_interfaces import ITrayModule
@ -52,16 +51,12 @@ class AvalonModule(OpenPypeModule, ITrayModule):
def tray_init(self):
# Add library tool
try:
from Qt import QtGui
from avalon import style
from openpype.tools.libraryloader import LibraryLoaderWindow
self.libraryloader = LibraryLoaderWindow(
icon=QtGui.QIcon(resources.get_openpype_icon_filepath()),
show_projects=True,
show_libraries=True
)
self.libraryloader.setStyleSheet(style.load_stylesheet())
except Exception:
self.log.warning(
"Couldn't load Library loader tool for tray.",
@ -70,6 +65,9 @@ class AvalonModule(OpenPypeModule, ITrayModule):
# Definition of Tray menu
def tray_menu(self, tray_menu):
if self.libraryloader is None:
return
from Qt import QtWidgets
# Actions
action_library_loader = QtWidgets.QAction(
@ -87,6 +85,9 @@ class AvalonModule(OpenPypeModule, ITrayModule):
return
def show_library_loader(self):
if self.libraryloader is None:
return
self.libraryloader.show()
# Raise and activate the window

View file

@ -3,6 +3,7 @@ import json
from avalon.api import AvalonMongoDB
from openpype.api import ProjectSettings
from openpype.lib import create_project
from openpype.settings import SaveWarningExc
from openpype_modules.ftrack.lib import (
ServerAction,
@ -312,7 +313,6 @@ class PrepareProjectServer(ServerAction):
if not in_data:
return
root_values = {}
root_key = "__root__"
for key in tuple(in_data.keys()):
@ -392,7 +392,12 @@ class PrepareProjectServer(ServerAction):
else:
attributes_entity[key] = value
project_settings.save()
try:
project_settings.save()
except SaveWarningExc as exc:
self.log.info("Few warnings happened during settings save:")
for warning in exc.warnings:
self.log.info(str(warning))
# Change custom attributes on project
if custom_attribute_values:

View file

@ -3,6 +3,7 @@ import json
from avalon.api import AvalonMongoDB
from openpype.api import ProjectSettings
from openpype.lib import create_project
from openpype.settings import SaveWarningExc
from openpype_modules.ftrack.lib import (
BaseAction,
@ -417,7 +418,12 @@ class PrepareProjectLocal(BaseAction):
else:
attributes_entity[key] = value
project_settings.save()
try:
project_settings.save()
except SaveWarningExc as exc:
self.log.info("Few warnings happened during settings save:")
for warning in exc.warnings:
self.log.info(str(warning))
# Change custom attributes on project
if custom_attribute_values:

View file

@ -26,14 +26,21 @@ class CollectUsername(pyblish.api.ContextPlugin):
"""
order = pyblish.api.CollectorOrder - 0.488
label = "Collect ftrack username"
hosts = ["webpublisher"]
hosts = ["webpublisher", "photoshop"]
_context = None
def process(self, context):
self.log.info("CollectUsername")
# photoshop could be triggered remotely in webpublisher fashion
if os.environ["AVALON_APP"] == "photoshop":
if not os.environ.get("IS_HEADLESS"):
self.log.debug("Regular process, skipping")
return
os.environ["FTRACK_API_USER"] = os.environ["FTRACK_BOT_API_USER"]
os.environ["FTRACK_API_KEY"] = os.environ["FTRACK_BOT_API_KEY"]
self.log.info("CollectUsername")
for instance in context:
email = instance.data["user_email"]
self.log.info("email:: {}".format(email))

View file

@ -3,10 +3,18 @@
import os
import sys
import json
from datetime import datetime
import time
from openpype.lib import PypeLogger
from openpype.api import get_app_environments_for_context
from openpype.lib.plugin_tools import parse_json, get_batch_asset_task_info
from openpype.lib.remote_publish import (
get_webpublish_conn,
start_webpublish_log,
publish_and_log,
fail_batch,
find_variant_key
)
class PypeCommands:
@ -110,10 +118,116 @@ class PypeCommands:
log.info("Publish finished.")
uninstall()
@staticmethod
def remotepublishfromapp(project, batch_dir, host, user, targets=None):
"""Opens installed variant of 'host' and run remote publish there.
Currently implemented and tested for Photoshop where customer
wants to process uploaded .psd file and publish collected layers
from there.
Checks if no other batches are running (status =='in_progress). If
so, it sleeps for SLEEP (this is separate process),
waits for WAIT_FOR seconds altogether.
Requires installed host application on the machine.
Runs publish process as user would, in automatic fashion.
"""
SLEEP = 5 # seconds for another loop check for concurrently runs
WAIT_FOR = 300 # seconds to wait for conc. runs
from openpype import install, uninstall
from openpype.api import Logger
log = Logger.get_logger()
log.info("remotepublishphotoshop command")
install()
from openpype.lib import ApplicationManager
application_manager = ApplicationManager()
found_variant_key = find_variant_key(application_manager, host)
app_name = "{}/{}".format(host, found_variant_key)
batch_data = None
if batch_dir and os.path.exists(batch_dir):
batch_data = parse_json(os.path.join(batch_dir, "manifest.json"))
if not batch_data:
raise ValueError(
"Cannot parse batch meta in {} folder".format(batch_dir))
asset, task_name, _task_type = get_batch_asset_task_info(
batch_data["context"])
# processing from app expects JUST ONE task in batch and 1 workfile
task_dir_name = batch_data["tasks"][0]
task_data = parse_json(os.path.join(batch_dir, task_dir_name,
"manifest.json"))
workfile_path = os.path.join(batch_dir,
task_dir_name,
task_data["files"][0])
print("workfile_path {}".format(workfile_path))
_, batch_id = os.path.split(batch_dir)
dbcon = get_webpublish_conn()
# safer to start logging here, launch might be broken altogether
_id = start_webpublish_log(dbcon, batch_id, user)
in_progress = True
slept_times = 0
while in_progress:
batches_in_progress = list(dbcon.find({
"status": "in_progress"
}))
if len(batches_in_progress) > 1:
if slept_times * SLEEP >= WAIT_FOR:
fail_batch(_id, batches_in_progress, dbcon)
print("Another batch running, sleeping for a bit")
time.sleep(SLEEP)
slept_times += 1
else:
in_progress = False
# must have for proper launch of app
env = get_app_environments_for_context(
project,
asset,
task_name,
app_name
)
os.environ.update(env)
os.environ["OPENPYPE_PUBLISH_DATA"] = batch_dir
os.environ["IS_HEADLESS"] = "true"
# must pass identifier to update log lines for a batch
os.environ["BATCH_LOG_ID"] = str(_id)
data = {
"last_workfile_path": workfile_path,
"start_last_workfile": True
}
launched_app = application_manager.launch(app_name, **data)
while launched_app.poll() is None:
time.sleep(0.5)
uninstall()
@staticmethod
def remotepublish(project, batch_path, host, user, targets=None):
"""Start headless publishing.
Used to publish rendered assets, workfiles etc.
Publish use json from passed paths argument.
Args:
@ -134,7 +248,6 @@ class PypeCommands:
from openpype import install, uninstall
from openpype.api import Logger
from openpype.lib import OpenPypeMongoConnection
# Register target and host
import pyblish.api
@ -166,62 +279,11 @@ class PypeCommands:
log.info("Running publish ...")
# Error exit as soon as any error occurs.
error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}"
mongo_client = OpenPypeMongoConnection.get_mongo_client()
database_name = os.environ["OPENPYPE_DATABASE_NAME"]
dbcon = mongo_client[database_name]["webpublishes"]
_, batch_id = os.path.split(batch_path)
_id = dbcon.insert_one({
"batch_id": batch_id,
"start_date": datetime.now(),
"user": user,
"status": "in_progress"
}).inserted_id
dbcon = get_webpublish_conn()
_id = start_webpublish_log(dbcon, batch_id, user)
log_lines = []
for result in pyblish.util.publish_iter():
for record in result["records"]:
log_lines.append("{}: {}".format(
result["plugin"].label, record.msg))
if result["error"]:
log.error(error_format.format(**result))
uninstall()
log_lines.append(error_format.format(**result))
dbcon.update_one(
{"_id": _id},
{"$set":
{
"finish_date": datetime.now(),
"status": "error",
"log": os.linesep.join(log_lines)
}}
)
sys.exit(1)
else:
dbcon.update_one(
{"_id": _id},
{"$set":
{
"progress": max(result["progress"], 0.95),
"log": os.linesep.join(log_lines)
}}
)
dbcon.update_one(
{"_id": _id},
{"$set":
{
"finish_date": datetime.now(),
"status": "finished_ok",
"progress": 1,
"log": os.linesep.join(log_lines)
}}
)
publish_and_log(dbcon, _id, log)
log.info("Publish finished.")
uninstall()

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View file

@ -162,9 +162,7 @@
]
}
],
"customNodes": [
]
"customNodes": []
},
"regexInputs": {
"inputs": [

View file

@ -12,6 +12,16 @@
"optional": true,
"active": true
},
"CollectRemoteInstances": {
"color_code_mapping": [
{
"color_code": [],
"layer_name_regex": [],
"family": "",
"subset_template_name": ""
}
]
},
"ExtractImage": {
"formats": [
"png",

View file

@ -97,6 +97,42 @@
}
}
},
"flame": {
"enabled": true,
"label": "Flame",
"icon": "{}/app_icons/flame.png",
"host_name": "flame",
"environment": {
"FLAME_SCRIPT_DIRS": {
"windows": "",
"darwin": "",
"linux": ""
}
},
"variants": {
"2021": {
"use_python_2": true,
"executables": {
"windows": [],
"darwin": [
"/opt/Autodesk/flame_2021/bin/flame.app/Contents/MacOS/startApp"
],
"linux": [
"/opt/Autodesk/flame_2021/bin/flame"
]
},
"arguments": {
"windows": [],
"darwin": [],
"linux": []
},
"environment": {}
},
"__dynamic_keys_labels__": {
"2021": "2021 (Testing Only)"
}
}
},
"nuke": {
"enabled": true,
"label": "Nuke",
@ -620,12 +656,12 @@
"FUSION_UTILITY_SCRIPTS_SOURCE_DIR": [],
"FUSION_UTILITY_SCRIPTS_DIR": {
"windows": "{PROGRAMDATA}/Blackmagic Design/Fusion/Scripts/Comp",
"darvin": "/Library/Application Support/Blackmagic Design/Fusion/Scripts/Comp",
"darwin": "/Library/Application Support/Blackmagic Design/Fusion/Scripts/Comp",
"linux": "/opt/Fusion/Scripts/Comp"
},
"PYTHON36": {
"windows": "{LOCALAPPDATA}/Programs/Python/Python36",
"darvin": "~/Library/Python/3.6/bin",
"darwin": "~/Library/Python/3.6/bin",
"linux": "/opt/Python/3.6/bin"
},
"PYTHONPATH": [
@ -686,22 +722,22 @@
"RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR": [],
"RESOLVE_SCRIPT_API": {
"windows": "{PROGRAMDATA}/Blackmagic Design/DaVinci Resolve/Support/Developer/Scripting",
"darvin": "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting",
"darwin": "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting",
"linux": "/opt/resolve/Developer/Scripting"
},
"RESOLVE_SCRIPT_LIB": {
"windows": "C:/Program Files/Blackmagic Design/DaVinci Resolve/fusionscript.dll",
"darvin": "/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so",
"darwin": "/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so",
"linux": "/opt/resolve/libs/Fusion/fusionscript.so"
},
"RESOLVE_UTILITY_SCRIPTS_DIR": {
"windows": "{PROGRAMDATA}/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Comp",
"darvin": "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Comp",
"darwin": "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Comp",
"linux": "/opt/resolve/Fusion/Scripts/Comp"
},
"PYTHON36_RESOLVE": {
"windows": "{LOCALAPPDATA}/Programs/Python/Python36",
"darvin": "~/Library/Python/3.6/bin",
"darwin": "~/Library/Python/3.6/bin",
"linux": "/opt/Python/3.6/bin"
},
"PYTHONPATH": [

View file

@ -143,6 +143,7 @@ class HostsEnumEntity(BaseEnumEntity):
"aftereffects",
"blender",
"celaction",
"flame",
"fusion",
"harmony",
"hiero",

View file

@ -43,6 +43,61 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"is_group": true,
"key": "CollectRemoteInstances",
"label": "Collect Instances for Webpublish",
"children": [
{
"type": "label",
"label": "Set color for publishable layers, set publishable families."
},
{
"type": "list",
"key": "color_code_mapping",
"label": "Color code mappings",
"use_label_wrap": false,
"collapsible": false,
"object_type": {
"type": "dict",
"children": [
{
"type": "list",
"key": "color_code",
"label": "Color codes for layers",
"object_type": "text"
},
{
"type": "list",
"key": "layer_name_regex",
"label": "Layer name regex",
"object_type": "text"
},
{
"type": "splitter"
},
{
"key": "family",
"label": "Resulting family",
"type": "enum",
"enum_items": [
{
"image": "image"
}
]
},
{
"type": "text",
"key": "subset_template_name",
"label": "Subset template name"
}
]
}
}
]
},
{
"type": "dict",
"collapsible": true,

View file

@ -0,0 +1,39 @@
{
"type": "dict",
"key": "flame",
"label": "Autodesk Flame",
"collapsible": true,
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "schema_template",
"name": "template_host_unchangables"
},
{
"key": "environment",
"label": "Environment",
"type": "raw-json"
},
{
"type": "dict-modifiable",
"key": "variants",
"collapsible_key": true,
"use_label_wrap": false,
"object_type": {
"type": "dict",
"collapsible": true,
"children": [
{
"type": "schema_template",
"name": "template_host_variant_items"
}
]
}
}
]
}

View file

@ -9,6 +9,10 @@
"type": "schema",
"name": "schema_maya"
},
{
"type": "schema",
"name": "schema_flame"
},
{
"type": "schema_template",
"name": "template_nuke",

View file

@ -2,6 +2,8 @@ import os
import json
import collections
from openpype import resources
import six
from .color_defs import parse_color
_STYLESHEET_CACHE = None
@ -10,7 +12,71 @@ _FONT_IDS = None
current_dir = os.path.dirname(os.path.abspath(__file__))
def _get_colors_raw_data():
"""Read data file with stylesheet fill values.
Returns:
dict: Loaded data for stylesheet.
"""
data_path = os.path.join(current_dir, "data.json")
with open(data_path, "r") as data_stream:
data = json.load(data_stream)
return data
def get_colors_data():
"""Only color data from stylesheet data."""
data = _get_colors_raw_data()
return data.get("color") or {}
def _convert_color_values_to_objects(value):
"""Parse all string values in dictionary to Color definitions.
Recursive function calling itself if value is dictionary.
Args:
value (dict, str): String is parsed into color definition object and
dictionary is passed into this function.
Raises:
TypeError: If value in color data do not contain string of dictionary.
"""
if isinstance(value, dict):
output = {}
for _key, _value in value.items():
output[_key] = _convert_color_values_to_objects(_value)
return output
if not isinstance(value, six.string_types):
raise TypeError((
"Unexpected type in colors data '{}'. Expected 'str' or 'dict'."
).format(str(type(value))))
return parse_color(value)
def get_objected_colors():
"""Colors parsed from stylesheet data into color definitions.
Returns:
dict: Parsed color objects by keys in data.
"""
colors_data = get_colors_data()
output = {}
for key, value in colors_data.items():
output[key] = _convert_color_values_to_objects(value)
return output
def _load_stylesheet():
"""Load strylesheet and trigger all related callbacks.
Style require more than a stylesheet string. Stylesheet string
contains paths to resources which must be registered into Qt application
and load fonts used in stylesheets.
Also replace values from stylesheet data into stylesheet text.
"""
from . import qrc_resources
qrc_resources.qInitResources()
@ -19,9 +85,7 @@ def _load_stylesheet():
with open(style_path, "r") as style_file:
stylesheet = style_file.read()
data_path = os.path.join(current_dir, "data.json")
with open(data_path, "r") as data_stream:
data = json.load(data_stream)
data = _get_colors_raw_data()
data_deque = collections.deque()
for item in data.items():
@ -44,6 +108,7 @@ def _load_stylesheet():
def _load_font():
"""Load and register fonts into Qt application."""
from Qt import QtGui
global _FONT_IDS
@ -83,6 +148,7 @@ def _load_font():
def load_stylesheet():
"""Load and return OpenPype Qt stylesheet."""
global _STYLESHEET_CACHE
if _STYLESHEET_CACHE is None:
_STYLESHEET_CACHE = _load_stylesheet()
@ -91,4 +157,5 @@ def load_stylesheet():
def app_icon_path():
"""Path to OpenPype icon."""
return resources.get_openpype_icon_filepath()

View file

@ -0,0 +1,391 @@
"""Color definitions that can be used to parse strings for stylesheet.
Each definition must have available method `get_qcolor` which should return
`QtGui.QColor` representation of the color.
# TODO create abstract class to force this method implementation
Usage: Some colors may be not be used only in stylesheet but is required to
use them in code too. To not hardcode these color values into code it is better
to use same colors that are available fro stylesheets.
It is possible that some colors may not be used in stylesheet at all and thei
definition is used only in code.
"""
import re
def parse_color(value):
"""Parse string value of color to one of objected representation.
Args:
value(str): Color definition usable in stylesheet.
"""
modified_value = value.strip().lower()
if modified_value.startswith("hsla"):
return HSLAColor(value)
if modified_value.startswith("hsl"):
return HSLColor(value)
if modified_value.startswith("#"):
return HEXColor(value)
if modified_value.startswith("rgba"):
return RGBAColor(value)
if modified_value.startswith("rgb"):
return RGBColor(value)
return UnknownColor(value)
def create_qcolor(*args):
"""Create QtGui.QColor object.
Args:
*args (tuple): It is possible to pass initialization arguments for
Qcolor.
"""
from Qt import QtGui
return QtGui.QColor(*args)
def min_max_check(value, min_value, max_value):
"""Validate number value if is in passed range.
Args:
value (int, float): Value which is validated.
min_value (int, float): Minimum possible value. Validation is skipped
if passed value is None.
max_value (int, float): Maximum possible value. Validation is skipped
if passed value is None.
Raises:
ValueError: When 'value' is out of specified range.
"""
if min_value is not None and value < min_value:
raise ValueError("Minimum expected value is '{}' got '{}'".format(
min_value, value
))
if max_value is not None and value > max_value:
raise ValueError("Maximum expected value is '{}' got '{}'".format(
min_value, value
))
def int_validation(value, min_value=None, max_value=None):
"""Validation of integer value within range.
Args:
value (int): Validated value.
min_value (int): Minimum possible value.
max_value (int): Maximum possible value.
Raises:
TypeError: If 'value' is not 'int' type.
"""
if not isinstance(value, int):
raise TypeError((
"Invalid type of hue expected 'int' got {}"
).format(str(type(value))))
min_max_check(value, min_value, max_value)
def float_validation(value, min_value=None, max_value=None):
"""Validation of float value within range.
Args:
value (float): Validated value.
min_value (float): Minimum possible value.
max_value (float): Maximum possible value.
Raises:
TypeError: If 'value' is not 'float' type.
"""
if not isinstance(value, float):
raise TypeError((
"Invalid type of hue expected 'int' got {}"
).format(str(type(value))))
min_max_check(value, min_value, max_value)
class UnknownColor:
"""Color from stylesheet data without known color definition.
This is backup for unknown color definitions which may be for example
constants or definition not yet defined by class.
"""
def __init__(self, value):
self.value = value
def get_qcolor(self):
return create_qcolor(self.value)
class HEXColor:
"""Hex color definition.
Hex color is defined by '#' and 3 or 6 hex values (0-F).
Examples:
"#fff"
"#f3f3f3"
"""
regex = re.compile(r"[a-fA-F0-9]{3}(?:[a-fA-F0-9]{3})?$")
def __init__(self, color_string):
red, green, blue = self.hex_to_rgb(color_string)
self._color_string = color_string
self._red = red
self._green = green
self._blue = blue
@property
def red(self):
return self._red
@property
def green(self):
return self._green
@property
def blue(self):
return self._blue
def to_stylesheet_str(self):
return self._color_string
@classmethod
def hex_to_rgb(cls, value):
"""Convert hex value to rgb."""
hex_value = value.lstrip("#")
if not cls.regex.match(hex_value):
raise ValueError("\"{}\" is not a valid HEX code.".format(value))
output = []
if len(hex_value) == 3:
for char in hex_value:
output.append(int(char * 2, 16))
else:
for idx in range(3):
start_idx = idx * 2
output.append(int(hex_value[start_idx:start_idx + 2], 16))
return output
def get_qcolor(self):
return create_qcolor(self.red, self.green, self.blue)
class RGBColor:
"""Color defined by red green and blue values.
Each color has possible integer range 0-255.
Examples:
"rgb(255, 127, 0)"
"""
def __init__(self, value):
modified_color = value.lower().strip()
content = modified_color.rstrip(")").lstrip("rgb(")
red_str, green_str, blue_str = (
item.strip() for item in content.split(",")
)
red = int(red_str)
green = int(green_str)
blue = int(blue_str)
int_validation(red, 0, 255)
int_validation(green, 0, 255)
int_validation(blue, 0, 255)
self._red = red
self._green = green
self._blue = blue
@property
def red(self):
return self._red
@property
def green(self):
return self._green
@property
def blue(self):
return self._blue
def get_qcolor(self):
return create_qcolor(self.red, self.green, self.blue)
class RGBAColor:
"""Color defined by red green, blue and alpha values.
Each color has possible integer range 0-255.
Examples:
"rgba(255, 127, 0, 127)"
"""
def __init__(self, value):
modified_color = value.lower().strip()
content = modified_color.rstrip(")").lstrip("rgba(")
red_str, green_str, blue_str, alpha_str = (
item.strip() for item in content.split(",")
)
red = int(red_str)
green = int(green_str)
blue = int(blue_str)
if "." in alpha_str:
alpha = int(float(alpha_str) * 100)
else:
alpha = int(alpha_str)
int_validation(red, 0, 255)
int_validation(green, 0, 255)
int_validation(blue, 0, 255)
int_validation(alpha, 0, 255)
self._red = red
self._green = green
self._blue = blue
self._alpha = alpha
@property
def red(self):
return self._red
@property
def green(self):
return self._green
@property
def blue(self):
return self._blue
@property
def alpha(self):
return self._alpha
def get_qcolor(self):
return create_qcolor(self.red, self.green, self.blue, self.alpha)
class HSLColor:
"""Color defined by hue, saturation and light values.
Hue is defined as integer in rage 0-360. Saturation and light can be
defined as float or percent value.
Examples:
"hsl(27, 0.7, 0.3)"
"hsl(27, 70%, 30%)"
"""
def __init__(self, value):
modified_color = value.lower().strip()
content = modified_color.rstrip(")").lstrip("hsl(")
hue_str, sat_str, light_str = (
item.strip() for item in content.split(",")
)
hue = int(hue_str) % 360
if "%" in sat_str:
sat = float(sat_str.rstrip("%")) / 100
else:
sat = float(sat)
if "%" in light_str:
light = float(light_str.rstrip("%")) / 100
else:
light = float(light_str)
int_validation(hue, 0, 360)
float_validation(sat, 0, 1)
float_validation(light, 0, 1)
self._hue = hue
self._saturation = sat
self._light = light
@property
def hue(self):
return self._hue
@property
def saturation(self):
return self._saturation
@property
def light(self):
return self._light
def get_qcolor(self):
color = create_qcolor()
color.setHslF(self.hue / 360, self.saturation, self.light)
return color
class HSLAColor:
"""Color defined by hue, saturation, light and alpha values.
Hue is defined as integer in rage 0-360. Saturation and light can be
defined as float (0-1 range) or percent value(0-100%). And alpha
as float (0-1 range).
Examples:
"hsl(27, 0.7, 0.3)"
"hsl(27, 70%, 30%)"
"""
def __init__(self, value):
modified_color = value.lower().strip()
content = modified_color.rstrip(")").lstrip("hsla(")
hue_str, sat_str, light_str, alpha_str = (
item.strip() for item in content.split(",")
)
hue = int(hue_str) % 360
if "%" in sat_str:
sat = float(sat_str.rstrip("%")) / 100
else:
sat = float(sat)
if "%" in light_str:
light = float(light_str.rstrip("%")) / 100
else:
light = float(light_str)
alpha = float(alpha_str)
int_validation(hue, 0, 360)
float_validation(sat, 0, 1)
float_validation(light, 0, 1)
float_validation(alpha, 0, 1)
self._hue = hue
self._saturation = sat
self._light = light
self._alpha = alpha
@property
def hue(self):
return self._hue
@property
def saturation(self):
return self._saturation
@property
def light(self):
return self._light
@property
def alpha(self):
return self._alpha
def get_qcolor(self):
color = create_qcolor()
color.setHslF(self.hue / 360, self.saturation, self.light, self.alpha)
return color

View file

@ -28,25 +28,36 @@
"bg": "#2C313A",
"bg-inputs": "#21252B",
"bg-buttons": "#434a56",
"bg-button-hover": "hsla(220, 14%, 70%, .3)",
"bg-button-hover": "rgba(168, 175, 189, 0.3)",
"bg-inputs-disabled": "#2C313A",
"bg-buttons-disabled": "#434a56",
"bg-splitter": "#434a56",
"bg-splitter-hover": "rgba(168, 175, 189, 0.3)",
"bg-menu-separator": "rgba(75, 83, 98, 127)",
"bg-scroll-handle": "#4B5362",
"bg-view": "#21252B",
"bg-view-header": "#373D48",
"bg-view-hover": "hsla(220, 14%, 70%, .3)",
"bg-view-hover": "rgba(168, 175, 189, .3)",
"bg-view-alternate": "rgb(36, 42, 50)",
"bg-view-disabled": "#434a56",
"bg-view-alternate-disabled": "#2C313A",
"bg-view-selection": "hsla(200, 60%, 60%, .4)",
"bg-view-selection-hover": "hsla(200, 60%, 60%, .8)",
"bg-view-selection": "rgba(92, 173, 214, .4)",
"bg-view-selection-hover": "rgba(92, 173, 214, .8)",
"border": "#373D48",
"border-hover": "hsla(220, 14%, 70%, .3)",
"border-focus": "hsl(200, 60%, 60%)"
"border-hover": "rgba(168, 175, 189, .3)",
"border-focus": "hsl(200, 60%, 60%)",
"loader": {
"asset-view": {
"selected": "rgba(168, 175, 189, 0.6)",
"hover": "rgba(168, 175, 189, 0.3)",
"selected-hover": "rgba(168, 175, 189, 0.7)"
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View file

@ -10,19 +10,7 @@ from PyQt5 import QtCore
qt_resource_data = b"\
\x00\x00\x00\xa0\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1c\x1f\x24\
\xc6\x09\x17\x00\x00\x00\x24\x49\x44\x41\x54\x08\xd7\x63\x60\x40\
\x05\xff\xcf\xc3\x58\x4c\xc8\x5c\x26\x64\x59\x26\x64\xc5\x70\x0e\
\xa3\x21\x9c\xc3\x68\x88\x61\x1a\x0a\x00\x00\x6d\x84\x09\x75\x37\
\x9e\xd9\x23\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
\x00\x00\x07\x30\
\x00\x00\x07\x06\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x00\x31\xac\xdc\x63\
@ -80,10 +68,10 @@ qt_resource_data = b"\
\x65\x3d\x22\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\x36\
\x2d\x32\x2e\x31\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x6f\x64\
\x69\x66\x79\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\
\x2d\x33\x31\x54\x31\x32\x3a\x33\x33\x3a\x31\x34\x2b\x30\x32\x3a\
\x2d\x33\x31\x54\x31\x32\x3a\x33\x30\x3a\x31\x31\x2b\x30\x32\x3a\
\x30\x30\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x65\x74\x61\x64\
\x61\x74\x61\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\
\x2d\x33\x31\x54\x31\x32\x3a\x33\x33\x3a\x31\x34\x2b\x30\x32\x3a\
\x2d\x33\x31\x54\x31\x32\x3a\x33\x30\x3a\x31\x31\x2b\x30\x32\x3a\
\x30\x30\x22\x3e\x0a\x20\x20\x20\x3c\x78\x6d\x70\x4d\x4d\x3a\x48\
\x69\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x20\x20\x3c\x72\x64\x66\
\x3a\x53\x65\x71\x3e\x0a\x20\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\
@ -94,14 +82,14 @@ qt_resource_data = b"\
\x6e\x69\x74\x79\x20\x44\x65\x73\x69\x67\x6e\x65\x72\x20\x31\x2e\
\x39\x2e\x32\x22\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\
\x3a\x77\x68\x65\x6e\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\x2d\x33\
\x31\x54\x31\x32\x3a\x33\x33\x3a\x31\x34\x2b\x30\x32\x3a\x30\x30\
\x31\x54\x31\x32\x3a\x33\x30\x3a\x31\x31\x2b\x30\x32\x3a\x30\x30\
\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x53\x65\
\x71\x3e\x0a\x20\x20\x20\x3c\x2f\x78\x6d\x70\x4d\x4d\x3a\x48\x69\
\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x3c\x2f\x72\x64\x66\x3a\x44\
\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x3c\x2f\x72\
\x64\x66\x3a\x52\x44\x46\x3e\x0a\x3c\x2f\x78\x3a\x78\x6d\x70\x6d\
\x65\x74\x61\x3e\x0a\x3c\x3f\x78\x70\x61\x63\x6b\x65\x74\x20\x65\
\x6e\x64\x3d\x22\x72\x22\x3f\x3e\x48\x8b\x5b\x5e\x00\x00\x01\x83\
\x6e\x64\x3d\x22\x72\x22\x3f\x3e\x85\x9d\x9f\x08\x00\x00\x01\x83\
\x69\x43\x43\x50\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\
\x36\x2d\x32\x2e\x31\x00\x00\x28\x91\x75\x91\xcf\x2b\x44\x51\x14\
\xc7\x3f\x66\x68\xfc\x18\x8d\x62\x61\x31\x65\x12\x16\x42\x83\x12\
@ -128,30 +116,15 @@ qt_resource_data = b"\
\xfd\xec\x73\x74\x07\xd1\x35\xf9\xaa\x4b\xd8\xd9\x85\x0e\x39\xef\
\x59\xf8\x06\x8e\xfd\x67\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x97\x49\x44\x41\x54\x18\x95\x6d\xcf\xb1\x6a\x02\x41\
\x14\x85\xe1\x6f\xb7\xb6\xd0\x27\x48\x3d\x56\x69\x03\xb1\xb4\x48\
\x3b\x6c\xa5\xf1\x39\xf6\x59\x02\x56\x42\xba\x61\x0a\x0b\x3b\x1b\
\x1b\x6b\x41\x18\x02\x29\x6d\xe3\xbe\x82\xcd\x06\x16\xd9\xdb\xdd\
\x9f\xff\x5c\xee\xa9\x62\x2a\x13\x4c\x73\x13\x6e\x46\x26\xa6\xf2\
\x82\xae\x46\x8b\xdf\x98\xca\xfb\x88\xb4\xc0\x0f\xda\x1a\x5b\x74\
\xd8\xc7\x54\xc2\x40\x9a\x63\x8f\x3f\x7c\x55\x3d\x7c\xc5\x09\x77\
\xbc\xa1\xc2\x19\x33\x2c\x72\x13\x2e\xd5\xe0\xc2\x12\x07\x5c\x51\
\x23\xe0\x23\x37\xe1\xa8\x4f\x0e\x7f\xda\x60\xd7\xaf\x9f\xb9\x09\
\xdf\x63\x05\xff\xe5\x75\x4c\x65\xf5\xcc\x1f\x0d\x33\x2c\x83\xb6\
\x06\x44\x83\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
\x00\x00\x00\xa5\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\x9c\x53\x34\xfc\x5d\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x0b\x02\x04\x6d\
\x98\x1b\x69\x00\x00\x00\x29\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18\x32\x32\x30\x20\x0b\x32\x1a\
\x32\x30\x30\x42\x98\x10\x41\x46\x43\x14\x13\x50\xb5\xa3\x01\x00\
\xd6\x10\x07\xd2\x2f\x48\xdf\x4a\x00\x00\x00\x00\x49\x45\x4e\x44\
\xae\x42\x60\x82\
\x00\x00\x00\x6d\x49\x44\x41\x54\x18\x95\x75\xcf\xc1\x09\xc2\x50\
\x10\x84\xe1\xd7\x85\x07\x9b\xd0\x43\x40\xd2\x82\x78\x14\x7b\x30\
\x57\x21\x8d\x84\x60\x3f\x62\x4b\x7a\x48\xcc\x97\x83\xfb\x30\x04\
\xdf\x9c\x86\x7f\x67\x99\xdd\x84\x0d\xaa\x54\x10\x6a\x6c\x13\x1e\
\xbe\xba\xfe\x09\x35\x31\x7b\xe6\x8d\x0f\x26\x1c\x17\xa1\x53\xb0\
\x11\x87\x0c\x2f\x01\x07\xec\xb0\x0f\x3f\xe1\xbc\xae\x69\xa3\xe6\
\x85\x77\xf8\x5b\xe9\xf0\xbb\x9f\xfa\xd2\x83\x39\xdc\xa3\x5b\xf3\
\x19\x2e\xa8\x89\xb5\x30\xf7\x43\xa0\x00\x00\x00\x00\x49\x45\x4e\
\x44\xae\x42\x60\x82\
\x00\x00\x00\xa0\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
@ -164,6 +137,19 @@ qt_resource_data = b"\
\x05\x73\x3e\xc0\x58\x4c\xc8\x5c\x26\x64\x59\x26\x64\xc5\x70\x4e\
\x8a\x00\x9c\x93\x22\x80\x61\x1a\x0a\x00\x00\x29\x95\x08\xaf\x88\
\xac\xba\x34\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
\x00\x00\x00\xa6\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\
\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\
\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\
\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\
\x44\xae\x42\x60\x82\
\x00\x00\x07\xad\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
@ -289,6 +275,45 @@ qt_resource_data = b"\
\x5e\x78\xa2\x9e\x0e\xa7\x20\x74\x47\x39\x1d\xf6\xe1\x95\x2b\xd6\
\xb1\x44\x8e\x0e\xcb\x58\xf0\x0f\x52\x8a\x79\x18\xdc\xe2\x02\x70\
\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
\x00\x00\x00\xa6\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\
\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\
\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\
\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\
\x44\xae\x42\x60\x82\
\x00\x00\x00\xa6\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1d\x00\xb0\
\xd5\x35\xa3\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x06\xfe\x9f\x67\x60\x60\x42\x30\xa1\x1c\x08\x93\x81\x81\x09\xc1\
\x64\x60\x60\x62\x60\x60\x34\x44\xe2\x20\x73\x19\x90\x8d\x40\x02\
\x00\x64\x40\x09\x75\x86\xb3\xad\x9c\x00\x00\x00\x00\x49\x45\x4e\
\x44\xae\x42\x60\x82\
\x00\x00\x00\xa6\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\
\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\
\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\
\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\
\x44\xae\x42\x60\x82\
\x00\x00\x00\xa0\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
@ -313,18 +338,26 @@ qt_resource_data = b"\
\x0d\xe6\x7c\x80\xb1\x18\x91\x05\x52\x04\xe0\x42\x08\x15\x29\x02\
\x0c\x0c\x8c\xc8\x02\x08\x95\x68\x00\x00\xac\xac\x07\x90\x4e\x65\
\x34\xac\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
\x00\x00\x00\x9e\
\x00\x00\x00\x45\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53\xde\
\x00\x00\x00\x0c\x49\x44\x41\x54\x08\x99\x63\x60\x60\x60\x00\x00\
\x00\x04\x00\x01\xa3\x0a\x15\xe3\x00\x00\x00\x00\x49\x45\x4e\x44\
\xae\x42\x60\x82\
\x00\x00\x00\xa5\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\
\x02\x62\x4b\x47\x44\x00\x9c\x53\x34\xfc\x5d\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x0f\xfd\
\x8f\xf8\x2e\x00\x00\x00\x22\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1\x42\x48\x2a\x0c\x19\
\x18\x18\x91\x05\x10\x2a\xd1\x00\x00\xca\xb5\x07\xd2\x76\xbb\xb2\
\xc5\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x0b\x02\x04\x6d\
\x98\x1b\x69\x00\x00\x00\x29\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18\x32\x32\x30\x20\x0b\x32\x1a\
\x32\x30\x30\x42\x98\x10\x41\x46\x43\x14\x13\x50\xb5\xa3\x01\x00\
\xd6\x10\x07\xd2\x2f\x48\xdf\x4a\x00\x00\x00\x00\x49\x45\x4e\x44\
\xae\x42\x60\x82\
\x00\x00\x00\xa5\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
@ -341,15 +374,170 @@ qt_resource_data = b"\
\x00\x00\x00\xa6\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\
\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\
\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\
\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\
\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1f\x20\xb9\
\x8d\x77\xe9\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x06\xe6\x7c\x60\x60\x60\x42\x30\xa1\x1c\x08\x93\x81\x81\x09\xc1\
\x64\x60\x60\x62\x60\x48\x11\x40\xe2\x20\x73\x19\x90\x8d\x40\x02\
\x00\x23\xed\x08\xaf\x64\x9f\x0f\x15\x00\x00\x00\x00\x49\x45\x4e\
\x44\xae\x42\x60\x82\
\x00\x00\x00\xa5\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\x9c\x53\x34\xfc\x5d\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x0b\x02\x04\x6d\
\x98\x1b\x69\x00\x00\x00\x29\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18\x32\x32\x30\x20\x0b\x32\x1a\
\x32\x30\x30\x42\x98\x10\x41\x46\x43\x14\x13\x50\xb5\xa3\x01\x00\
\xd6\x10\x07\xd2\x2f\x48\xdf\x4a\x00\x00\x00\x00\x49\x45\x4e\x44\
\xae\x42\x60\x82\
\x00\x00\x07\x30\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x00\x31\xac\xdc\x63\
\x00\x00\x04\xb0\x69\x54\x58\x74\x58\x4d\x4c\x3a\x63\x6f\x6d\x2e\
\x61\x64\x6f\x62\x65\x2e\x78\x6d\x70\x00\x00\x00\x00\x00\x3c\x3f\
\x78\x70\x61\x63\x6b\x65\x74\x20\x62\x65\x67\x69\x6e\x3d\x22\xef\
\xbb\xbf\x22\x20\x69\x64\x3d\x22\x57\x35\x4d\x30\x4d\x70\x43\x65\
\x68\x69\x48\x7a\x72\x65\x53\x7a\x4e\x54\x63\x7a\x6b\x63\x39\x64\
\x22\x3f\x3e\x0a\x3c\x78\x3a\x78\x6d\x70\x6d\x65\x74\x61\x20\x78\
\x6d\x6c\x6e\x73\x3a\x78\x3d\x22\x61\x64\x6f\x62\x65\x3a\x6e\x73\
\x3a\x6d\x65\x74\x61\x2f\x22\x20\x78\x3a\x78\x6d\x70\x74\x6b\x3d\
\x22\x58\x4d\x50\x20\x43\x6f\x72\x65\x20\x35\x2e\x35\x2e\x30\x22\
\x3e\x0a\x20\x3c\x72\x64\x66\x3a\x52\x44\x46\x20\x78\x6d\x6c\x6e\
\x73\x3a\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\
\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\
\x2f\x32\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\
\x73\x23\x22\x3e\x0a\x20\x20\x3c\x72\x64\x66\x3a\x44\x65\x73\x63\
\x72\x69\x70\x74\x69\x6f\x6e\x20\x72\x64\x66\x3a\x61\x62\x6f\x75\
\x74\x3d\x22\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x65\
\x78\x69\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\
\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x65\x78\x69\x66\x2f\x31\x2e\
\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x74\x69\
\x66\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\
\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x74\x69\x66\x66\x2f\x31\x2e\x30\
\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x70\x68\x6f\
\x74\x6f\x73\x68\x6f\x70\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\
\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x70\x68\x6f\x74\
\x6f\x73\x68\x6f\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\
\x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\x3d\x22\x68\x74\x74\x70\x3a\
\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\
\x61\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\
\x6e\x73\x3a\x78\x6d\x70\x4d\x4d\x3d\x22\x68\x74\x74\x70\x3a\x2f\
\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\x61\
\x70\x2f\x31\x2e\x30\x2f\x6d\x6d\x2f\x22\x0a\x20\x20\x20\x20\x78\
\x6d\x6c\x6e\x73\x3a\x73\x74\x45\x76\x74\x3d\x22\x68\x74\x74\x70\
\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\
\x78\x61\x70\x2f\x31\x2e\x30\x2f\x73\x54\x79\x70\x65\x2f\x52\x65\
\x73\x6f\x75\x72\x63\x65\x45\x76\x65\x6e\x74\x23\x22\x0a\x20\x20\
\x20\x65\x78\x69\x66\x3a\x50\x69\x78\x65\x6c\x58\x44\x69\x6d\x65\
\x6e\x73\x69\x6f\x6e\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x65\x78\
\x69\x66\x3a\x50\x69\x78\x65\x6c\x59\x44\x69\x6d\x65\x6e\x73\x69\
\x6f\x6e\x3d\x22\x37\x22\x0a\x20\x20\x20\x65\x78\x69\x66\x3a\x43\
\x6f\x6c\x6f\x72\x53\x70\x61\x63\x65\x3d\x22\x31\x22\x0a\x20\x20\
\x20\x74\x69\x66\x66\x3a\x49\x6d\x61\x67\x65\x57\x69\x64\x74\x68\
\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x49\x6d\
\x61\x67\x65\x4c\x65\x6e\x67\x74\x68\x3d\x22\x37\x22\x0a\x20\x20\
\x20\x74\x69\x66\x66\x3a\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\
\x55\x6e\x69\x74\x3d\x22\x32\x22\x0a\x20\x20\x20\x74\x69\x66\x66\
\x3a\x58\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\
\x2e\x30\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x59\x52\x65\x73\
\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\x2e\x30\x22\x0a\x20\
\x20\x20\x70\x68\x6f\x74\x6f\x73\x68\x6f\x70\x3a\x43\x6f\x6c\x6f\
\x72\x4d\x6f\x64\x65\x3d\x22\x33\x22\x0a\x20\x20\x20\x70\x68\x6f\
\x74\x6f\x73\x68\x6f\x70\x3a\x49\x43\x43\x50\x72\x6f\x66\x69\x6c\
\x65\x3d\x22\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\x36\
\x2d\x32\x2e\x31\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x6f\x64\
\x69\x66\x79\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\
\x2d\x33\x31\x54\x31\x32\x3a\x33\x33\x3a\x31\x34\x2b\x30\x32\x3a\
\x30\x30\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x65\x74\x61\x64\
\x61\x74\x61\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\
\x2d\x33\x31\x54\x31\x32\x3a\x33\x33\x3a\x31\x34\x2b\x30\x32\x3a\
\x30\x30\x22\x3e\x0a\x20\x20\x20\x3c\x78\x6d\x70\x4d\x4d\x3a\x48\
\x69\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x20\x20\x3c\x72\x64\x66\
\x3a\x53\x65\x71\x3e\x0a\x20\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\
\x6c\x69\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\x3a\x61\
\x63\x74\x69\x6f\x6e\x3d\x22\x70\x72\x6f\x64\x75\x63\x65\x64\x22\
\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\x3a\x73\x6f\x66\
\x74\x77\x61\x72\x65\x41\x67\x65\x6e\x74\x3d\x22\x41\x66\x66\x69\
\x6e\x69\x74\x79\x20\x44\x65\x73\x69\x67\x6e\x65\x72\x20\x31\x2e\
\x39\x2e\x32\x22\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\
\x3a\x77\x68\x65\x6e\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\x2d\x33\
\x31\x54\x31\x32\x3a\x33\x33\x3a\x31\x34\x2b\x30\x32\x3a\x30\x30\
\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x53\x65\
\x71\x3e\x0a\x20\x20\x20\x3c\x2f\x78\x6d\x70\x4d\x4d\x3a\x48\x69\
\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x3c\x2f\x72\x64\x66\x3a\x44\
\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x3c\x2f\x72\
\x64\x66\x3a\x52\x44\x46\x3e\x0a\x3c\x2f\x78\x3a\x78\x6d\x70\x6d\
\x65\x74\x61\x3e\x0a\x3c\x3f\x78\x70\x61\x63\x6b\x65\x74\x20\x65\
\x6e\x64\x3d\x22\x72\x22\x3f\x3e\x48\x8b\x5b\x5e\x00\x00\x01\x83\
\x69\x43\x43\x50\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\
\x36\x2d\x32\x2e\x31\x00\x00\x28\x91\x75\x91\xcf\x2b\x44\x51\x14\
\xc7\x3f\x66\x68\xfc\x18\x8d\x62\x61\x31\x65\x12\x16\x42\x83\x12\
\x1b\x8b\x99\x18\x0a\x8b\x99\x51\x7e\x6d\x66\x9e\x79\x33\x6a\xde\
\x78\xbd\x37\xd2\x64\xab\x6c\xa7\x28\xb1\xf1\x6b\xc1\x5f\xc0\x56\
\x59\x2b\x45\xa4\x64\xa7\xac\x89\x0d\x7a\xce\x9b\x51\x23\x99\x73\
\x3b\xf7\x7c\xee\xf7\xde\x73\xba\xf7\x5c\x70\x44\xd3\x8a\x66\x56\
\xfa\x41\xcb\x64\x8d\x70\x28\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\
\x85\x1a\x3a\xf1\xc6\x14\x53\x9f\x8c\x8c\x46\x29\x6b\xef\xb7\x54\
\xd8\xf1\xba\xdb\xae\x55\xfe\xdc\xbf\x56\xb7\x98\x30\x15\xa8\xa8\
\x16\x1e\x56\x74\x23\x2b\x3c\x26\x3c\xb1\x9a\xd5\x6d\xde\x12\x6e\
\x52\x52\xb1\x45\xe1\x13\xe1\x2e\x43\x2e\x28\x7c\x63\xeb\xf1\x22\
\x3f\xdb\x9c\x2c\xf2\xa7\xcd\x46\x34\x1c\x04\x47\x83\xb0\x2f\xf9\
\x8b\xe3\xbf\x58\x49\x19\x9a\xb0\xbc\x9c\x36\x2d\xbd\xa2\xfc\xdc\
\xc7\x7e\x89\x3b\x91\x99\x8e\x48\x6c\x15\xf7\x62\x12\x26\x44\x00\
\x1f\xe3\x8c\x10\x64\x80\x5e\x86\x64\x1e\xa0\x9b\x3e\x7a\x64\x45\
\x99\x7c\x7f\x21\x7f\x8a\x65\xc9\x55\x64\xd6\xc9\x61\xb0\x44\x92\
\x14\x59\xba\x44\x5d\x91\xea\x09\x89\xaa\xe8\x09\x19\x69\x72\x76\
\xff\xff\xf6\xd5\x54\xfb\xfb\x8a\xd5\xdd\x01\xa8\x7a\xb4\xac\xd7\
\x76\x70\x6d\xc2\x57\xde\xb2\x3e\x0e\x2c\xeb\xeb\x10\x9c\x0f\x70\
\x9e\x29\xe5\x2f\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9e\x75\
\x38\xbd\x28\x69\xf1\x6d\x38\xdb\x80\xe6\x7b\x3d\x66\xc4\x0a\x92\
\x53\xdc\xa1\xaa\xf0\x72\x0c\xf5\xb3\xd0\x78\x05\xb5\xf3\xc5\x9e\
\xfd\xec\x73\x74\x07\xd1\x35\xf9\xaa\x4b\xd8\xd9\x85\x0e\x39\xef\
\x59\xf8\x06\x8e\xfd\x67\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x97\x49\x44\x41\x54\x18\x95\x6d\xcf\xb1\x6a\x02\x41\
\x14\x85\xe1\x6f\xb7\xb6\xd0\x27\x48\x3d\x56\x69\x03\xb1\xb4\x48\
\x3b\x6c\xa5\xf1\x39\xf6\x59\x02\x56\x42\xba\x61\x0a\x0b\x3b\x1b\
\x1b\x6b\x41\x18\x02\x29\x6d\xe3\xbe\x82\xcd\x06\x16\xd9\xdb\xdd\
\x9f\xff\x5c\xee\xa9\x62\x2a\x13\x4c\x73\x13\x6e\x46\x26\xa6\xf2\
\x82\xae\x46\x8b\xdf\x98\xca\xfb\x88\xb4\xc0\x0f\xda\x1a\x5b\x74\
\xd8\xc7\x54\xc2\x40\x9a\x63\x8f\x3f\x7c\x55\x3d\x7c\xc5\x09\x77\
\xbc\xa1\xc2\x19\x33\x2c\x72\x13\x2e\xd5\xe0\xc2\x12\x07\x5c\x51\
\x23\xe0\x23\x37\xe1\xa8\x4f\x0e\x7f\xda\x60\xd7\xaf\x9f\xb9\x09\
\xdf\x63\x05\xff\xe5\x75\x4c\x65\xf5\xcc\x1f\x0d\x33\x2c\x83\xb6\
\x06\x44\x83\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
\x00\x00\x00\xa0\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1c\x1f\x24\
\xc6\x09\x17\x00\x00\x00\x24\x49\x44\x41\x54\x08\xd7\x63\x60\x40\
\x05\xff\xcf\xc3\x58\x4c\xc8\x5c\x26\x64\x59\x26\x64\xc5\x70\x0e\
\xa3\x21\x9c\xc3\x68\x88\x61\x1a\x0a\x00\x00\x6d\x84\x09\x75\x37\
\x9e\xd9\x23\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
\x00\x00\x00\xa6\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1d\x00\xb0\
\xd5\x35\xa3\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x06\xfe\x9f\x67\x60\x60\x42\x30\xa1\x1c\x08\x93\x81\x81\x09\xc1\
\x64\x60\x60\x62\x60\x60\x34\x44\xe2\x20\x73\x19\x90\x8d\x40\x02\
\x00\x64\x40\x09\x75\x86\xb3\xad\x9c\x00\x00\x00\x00\x49\x45\x4e\
\x44\xae\x42\x60\x82\
\x00\x00\x07\xdd\
\x89\
@ -479,45 +667,6 @@ qt_resource_data = b"\
\x71\x5b\x73\x5c\x40\x48\xa5\xdd\x61\x81\x0d\x9e\x6b\x8e\xff\xfd\
\xcf\x3f\xcc\x31\xe9\x01\x1c\x00\x73\x52\x2d\x71\xe4\x4a\x1b\x69\
\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
\x00\x00\x00\xa6\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\
\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\
\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\
\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\
\x44\xae\x42\x60\x82\
\x00\x00\x00\xa5\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\x9c\x53\x34\xfc\x5d\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x0b\x02\x04\x6d\
\x98\x1b\x69\x00\x00\x00\x29\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18\x32\x32\x30\x20\x0b\x32\x1a\
\x32\x30\x30\x42\x98\x10\x41\x46\x43\x14\x13\x50\xb5\xa3\x01\x00\
\xd6\x10\x07\xd2\x2f\x48\xdf\x4a\x00\x00\x00\x00\x49\x45\x4e\x44\
\xae\x42\x60\x82\
\x00\x00\x00\xa6\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1f\x20\xb9\
\x8d\x77\xe9\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x06\xe6\x7c\x60\x60\x60\x42\x30\xa1\x1c\x08\x93\x81\x81\x09\xc1\
\x64\x60\x60\x62\x60\x48\x11\x40\xe2\x20\x73\x19\x90\x8d\x40\x02\
\x00\x23\xed\x08\xaf\x64\x9f\x0f\x15\x00\x00\x00\x00\x49\x45\x4e\
\x44\xae\x42\x60\x82\
\x00\x00\x00\x9e\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
@ -530,160 +679,18 @@ qt_resource_data = b"\
\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1\x42\x48\x2a\x0c\x19\
\x18\x18\x91\x05\x10\x2a\xd1\x00\x00\xca\xb5\x07\xd2\x76\xbb\xb2\
\xc5\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
\x00\x00\x00\xa6\
\x00\x00\x00\x9e\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce\x7c\x4e\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x3b\xdc\
\x3b\x0c\x9b\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x00\x8c\x0c\x0c\x73\x3e\x20\x0b\xa4\x08\x30\x32\x30\x20\x0b\xa6\
\x08\x30\x30\x30\x42\x98\x10\xc1\x14\x01\x14\x13\x50\xb5\xa3\x01\
\x00\xc6\xb9\x07\x90\x5d\x66\x1f\x83\x00\x00\x00\x00\x49\x45\x4e\
\x44\xae\x42\x60\x82\
\x00\x00\x00\xa6\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1d\x00\xb0\
\xd5\x35\xa3\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x06\xfe\x9f\x67\x60\x60\x42\x30\xa1\x1c\x08\x93\x81\x81\x09\xc1\
\x64\x60\x60\x62\x60\x60\x34\x44\xe2\x20\x73\x19\x90\x8d\x40\x02\
\x00\x64\x40\x09\x75\x86\xb3\xad\x9c\x00\x00\x00\x00\x49\x45\x4e\
\x44\xae\x42\x60\x82\
\x00\x00\x00\xa6\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\
\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02\x62\x4b\x47\x44\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x14\x1d\x00\xb0\
\xd5\x35\xa3\x00\x00\x00\x2a\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x06\xfe\x9f\x67\x60\x60\x42\x30\xa1\x1c\x08\x93\x81\x81\x09\xc1\
\x64\x60\x60\x62\x60\x60\x34\x44\xe2\x20\x73\x19\x90\x8d\x40\x02\
\x00\x64\x40\x09\x75\x86\xb3\xad\x9c\x00\x00\x00\x00\x49\x45\x4e\
\x44\xae\x42\x60\x82\
\x00\x00\x07\x06\
\x89\
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x00\x31\xac\xdc\x63\
\x00\x00\x04\xb0\x69\x54\x58\x74\x58\x4d\x4c\x3a\x63\x6f\x6d\x2e\
\x61\x64\x6f\x62\x65\x2e\x78\x6d\x70\x00\x00\x00\x00\x00\x3c\x3f\
\x78\x70\x61\x63\x6b\x65\x74\x20\x62\x65\x67\x69\x6e\x3d\x22\xef\
\xbb\xbf\x22\x20\x69\x64\x3d\x22\x57\x35\x4d\x30\x4d\x70\x43\x65\
\x68\x69\x48\x7a\x72\x65\x53\x7a\x4e\x54\x63\x7a\x6b\x63\x39\x64\
\x22\x3f\x3e\x0a\x3c\x78\x3a\x78\x6d\x70\x6d\x65\x74\x61\x20\x78\
\x6d\x6c\x6e\x73\x3a\x78\x3d\x22\x61\x64\x6f\x62\x65\x3a\x6e\x73\
\x3a\x6d\x65\x74\x61\x2f\x22\x20\x78\x3a\x78\x6d\x70\x74\x6b\x3d\
\x22\x58\x4d\x50\x20\x43\x6f\x72\x65\x20\x35\x2e\x35\x2e\x30\x22\
\x3e\x0a\x20\x3c\x72\x64\x66\x3a\x52\x44\x46\x20\x78\x6d\x6c\x6e\
\x73\x3a\x72\x64\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\
\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x30\x32\
\x2f\x32\x32\x2d\x72\x64\x66\x2d\x73\x79\x6e\x74\x61\x78\x2d\x6e\
\x73\x23\x22\x3e\x0a\x20\x20\x3c\x72\x64\x66\x3a\x44\x65\x73\x63\
\x72\x69\x70\x74\x69\x6f\x6e\x20\x72\x64\x66\x3a\x61\x62\x6f\x75\
\x74\x3d\x22\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x65\
\x78\x69\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\
\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x65\x78\x69\x66\x2f\x31\x2e\
\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x74\x69\
\x66\x66\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\
\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x74\x69\x66\x66\x2f\x31\x2e\x30\
\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x70\x68\x6f\
\x74\x6f\x73\x68\x6f\x70\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x6e\
\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x70\x68\x6f\x74\
\x6f\x73\x68\x6f\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\
\x78\x6d\x6c\x6e\x73\x3a\x78\x6d\x70\x3d\x22\x68\x74\x74\x70\x3a\
\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\
\x61\x70\x2f\x31\x2e\x30\x2f\x22\x0a\x20\x20\x20\x20\x78\x6d\x6c\
\x6e\x73\x3a\x78\x6d\x70\x4d\x4d\x3d\x22\x68\x74\x74\x70\x3a\x2f\
\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\x78\x61\
\x70\x2f\x31\x2e\x30\x2f\x6d\x6d\x2f\x22\x0a\x20\x20\x20\x20\x78\
\x6d\x6c\x6e\x73\x3a\x73\x74\x45\x76\x74\x3d\x22\x68\x74\x74\x70\
\x3a\x2f\x2f\x6e\x73\x2e\x61\x64\x6f\x62\x65\x2e\x63\x6f\x6d\x2f\
\x78\x61\x70\x2f\x31\x2e\x30\x2f\x73\x54\x79\x70\x65\x2f\x52\x65\
\x73\x6f\x75\x72\x63\x65\x45\x76\x65\x6e\x74\x23\x22\x0a\x20\x20\
\x20\x65\x78\x69\x66\x3a\x50\x69\x78\x65\x6c\x58\x44\x69\x6d\x65\
\x6e\x73\x69\x6f\x6e\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x65\x78\
\x69\x66\x3a\x50\x69\x78\x65\x6c\x59\x44\x69\x6d\x65\x6e\x73\x69\
\x6f\x6e\x3d\x22\x37\x22\x0a\x20\x20\x20\x65\x78\x69\x66\x3a\x43\
\x6f\x6c\x6f\x72\x53\x70\x61\x63\x65\x3d\x22\x31\x22\x0a\x20\x20\
\x20\x74\x69\x66\x66\x3a\x49\x6d\x61\x67\x65\x57\x69\x64\x74\x68\
\x3d\x22\x31\x30\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x49\x6d\
\x61\x67\x65\x4c\x65\x6e\x67\x74\x68\x3d\x22\x37\x22\x0a\x20\x20\
\x20\x74\x69\x66\x66\x3a\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\
\x55\x6e\x69\x74\x3d\x22\x32\x22\x0a\x20\x20\x20\x74\x69\x66\x66\
\x3a\x58\x52\x65\x73\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\
\x2e\x30\x22\x0a\x20\x20\x20\x74\x69\x66\x66\x3a\x59\x52\x65\x73\
\x6f\x6c\x75\x74\x69\x6f\x6e\x3d\x22\x37\x32\x2e\x30\x22\x0a\x20\
\x20\x20\x70\x68\x6f\x74\x6f\x73\x68\x6f\x70\x3a\x43\x6f\x6c\x6f\
\x72\x4d\x6f\x64\x65\x3d\x22\x33\x22\x0a\x20\x20\x20\x70\x68\x6f\
\x74\x6f\x73\x68\x6f\x70\x3a\x49\x43\x43\x50\x72\x6f\x66\x69\x6c\
\x65\x3d\x22\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\x36\
\x2d\x32\x2e\x31\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x6f\x64\
\x69\x66\x79\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\
\x2d\x33\x31\x54\x31\x32\x3a\x33\x30\x3a\x31\x31\x2b\x30\x32\x3a\
\x30\x30\x22\x0a\x20\x20\x20\x78\x6d\x70\x3a\x4d\x65\x74\x61\x64\
\x61\x74\x61\x44\x61\x74\x65\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\
\x2d\x33\x31\x54\x31\x32\x3a\x33\x30\x3a\x31\x31\x2b\x30\x32\x3a\
\x30\x30\x22\x3e\x0a\x20\x20\x20\x3c\x78\x6d\x70\x4d\x4d\x3a\x48\
\x69\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x20\x20\x3c\x72\x64\x66\
\x3a\x53\x65\x71\x3e\x0a\x20\x20\x20\x20\x20\x3c\x72\x64\x66\x3a\
\x6c\x69\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\x3a\x61\
\x63\x74\x69\x6f\x6e\x3d\x22\x70\x72\x6f\x64\x75\x63\x65\x64\x22\
\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\x3a\x73\x6f\x66\
\x74\x77\x61\x72\x65\x41\x67\x65\x6e\x74\x3d\x22\x41\x66\x66\x69\
\x6e\x69\x74\x79\x20\x44\x65\x73\x69\x67\x6e\x65\x72\x20\x31\x2e\
\x39\x2e\x32\x22\x0a\x20\x20\x20\x20\x20\x20\x73\x74\x45\x76\x74\
\x3a\x77\x68\x65\x6e\x3d\x22\x32\x30\x32\x31\x2d\x30\x35\x2d\x33\
\x31\x54\x31\x32\x3a\x33\x30\x3a\x31\x31\x2b\x30\x32\x3a\x30\x30\
\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x2f\x72\x64\x66\x3a\x53\x65\
\x71\x3e\x0a\x20\x20\x20\x3c\x2f\x78\x6d\x70\x4d\x4d\x3a\x48\x69\
\x73\x74\x6f\x72\x79\x3e\x0a\x20\x20\x3c\x2f\x72\x64\x66\x3a\x44\
\x65\x73\x63\x72\x69\x70\x74\x69\x6f\x6e\x3e\x0a\x20\x3c\x2f\x72\
\x64\x66\x3a\x52\x44\x46\x3e\x0a\x3c\x2f\x78\x3a\x78\x6d\x70\x6d\
\x65\x74\x61\x3e\x0a\x3c\x3f\x78\x70\x61\x63\x6b\x65\x74\x20\x65\
\x6e\x64\x3d\x22\x72\x22\x3f\x3e\x85\x9d\x9f\x08\x00\x00\x01\x83\
\x69\x43\x43\x50\x73\x52\x47\x42\x20\x49\x45\x43\x36\x31\x39\x36\
\x36\x2d\x32\x2e\x31\x00\x00\x28\x91\x75\x91\xcf\x2b\x44\x51\x14\
\xc7\x3f\x66\x68\xfc\x18\x8d\x62\x61\x31\x65\x12\x16\x42\x83\x12\
\x1b\x8b\x99\x18\x0a\x8b\x99\x51\x7e\x6d\x66\x9e\x79\x33\x6a\xde\
\x78\xbd\x37\xd2\x64\xab\x6c\xa7\x28\xb1\xf1\x6b\xc1\x5f\xc0\x56\
\x59\x2b\x45\xa4\x64\xa7\xac\x89\x0d\x7a\xce\x9b\x51\x23\x99\x73\
\x3b\xf7\x7c\xee\xf7\xde\x73\xba\xf7\x5c\x70\x44\xd3\x8a\x66\x56\
\xfa\x41\xcb\x64\x8d\x70\x28\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\
\x85\x1a\x3a\xf1\xc6\x14\x53\x9f\x8c\x8c\x46\x29\x6b\xef\xb7\x54\
\xd8\xf1\xba\xdb\xae\x55\xfe\xdc\xbf\x56\xb7\x98\x30\x15\xa8\xa8\
\x16\x1e\x56\x74\x23\x2b\x3c\x26\x3c\xb1\x9a\xd5\x6d\xde\x12\x6e\
\x52\x52\xb1\x45\xe1\x13\xe1\x2e\x43\x2e\x28\x7c\x63\xeb\xf1\x22\
\x3f\xdb\x9c\x2c\xf2\xa7\xcd\x46\x34\x1c\x04\x47\x83\xb0\x2f\xf9\
\x8b\xe3\xbf\x58\x49\x19\x9a\xb0\xbc\x9c\x36\x2d\xbd\xa2\xfc\xdc\
\xc7\x7e\x89\x3b\x91\x99\x8e\x48\x6c\x15\xf7\x62\x12\x26\x44\x00\
\x1f\xe3\x8c\x10\x64\x80\x5e\x86\x64\x1e\xa0\x9b\x3e\x7a\x64\x45\
\x99\x7c\x7f\x21\x7f\x8a\x65\xc9\x55\x64\xd6\xc9\x61\xb0\x44\x92\
\x14\x59\xba\x44\x5d\x91\xea\x09\x89\xaa\xe8\x09\x19\x69\x72\x76\
\xff\xff\xf6\xd5\x54\xfb\xfb\x8a\xd5\xdd\x01\xa8\x7a\xb4\xac\xd7\
\x76\x70\x6d\xc2\x57\xde\xb2\x3e\x0e\x2c\xeb\xeb\x10\x9c\x0f\x70\
\x9e\x29\xe5\x2f\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9e\x75\
\x38\xbd\x28\x69\xf1\x6d\x38\xdb\x80\xe6\x7b\x3d\x66\xc4\x0a\x92\
\x53\xdc\xa1\xaa\xf0\x72\x0c\xf5\xb3\xd0\x78\x05\xb5\xf3\xc5\x9e\
\xfd\xec\x73\x74\x07\xd1\x35\xf9\xaa\x4b\xd8\xd9\x85\x0e\x39\xef\
\x59\xf8\x06\x8e\xfd\x67\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09\x70\
\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x6d\x49\x44\x41\x54\x18\x95\x75\xcf\xc1\x09\xc2\x50\
\x10\x84\xe1\xd7\x85\x07\x9b\xd0\x43\x40\xd2\x82\x78\x14\x7b\x30\
\x57\x21\x8d\x84\x60\x3f\x62\x4b\x7a\x48\xcc\x97\x83\xfb\x30\x04\
\xdf\x9c\x86\x7f\x67\x99\xdd\x84\x0d\xaa\x54\x10\x6a\x6c\x13\x1e\
\xbe\xba\xfe\x09\x35\x31\x7b\xe6\x8d\x0f\x26\x1c\x17\xa1\x53\xb0\
\x11\x87\x0c\x2f\x01\x07\xec\xb0\x0f\x3f\xe1\xbc\xae\x69\xa3\xe6\
\x85\x77\xf8\x5b\xe9\xf0\xbb\x9f\xfa\xd2\x83\x39\xdc\xa3\x5b\xf3\
\x19\x2e\xa8\x89\xb5\x30\xf7\x43\xa0\x00\x00\x00\x00\x49\x45\x4e\
\x44\xae\x42\x60\x82\
\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xdc\x08\x17\x08\x15\x0f\xfd\
\x8f\xf8\x2e\x00\x00\x00\x22\x49\x44\x41\x54\x08\xd7\x63\x60\xc0\
\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1\x42\x48\x2a\x0c\x19\
\x18\x18\x91\x05\x10\x2a\xd1\x00\x00\xca\xb5\x07\xd2\x76\xbb\xb2\
\xc5\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
"
qt_resource_name = b"\
@ -696,119 +703,124 @@ qt_resource_name = b"\
\x00\x69\
\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\
\x00\x0f\
\x02\x9f\x05\x87\
\x00\x72\
\x00\x69\x00\x67\x00\x68\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x12\
\x05\x8f\x9d\x07\
\x06\x53\x25\xa7\
\x00\x62\
\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x6f\x00\x70\x00\x65\x00\x6e\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\
\x00\x67\
\x00\x1b\
\x03\x5a\x32\x27\
\x00\x63\
\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\
\x00\x73\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x6f\x00\x70\x00\x65\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x18\
\x03\x8e\xde\x67\
\x00\x72\
\x00\x69\x00\x67\x00\x68\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\
\x00\x6c\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x11\
\x0b\xda\x30\xa7\
\x00\x62\
\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x63\x00\x6c\x00\x6f\x00\x73\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\
\
\x00\x12\
\x03\x8d\x04\x47\
\x00\x72\
\x00\x69\x00\x67\x00\x68\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\
\x00\x67\
\x00\x15\
\x0f\xf3\xc0\x07\
\x00\x75\
\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x64\
\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x0f\
\x01\x73\x8b\x07\
\x00\x75\
\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x0e\
\x04\xa2\xfc\xa7\
\x00\x64\
\x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x12\
\x01\x2e\x03\x27\
\x00\x63\
\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\
\x00\x67\
\x00\x14\
\x04\x5e\x2d\xa7\
\x00\x62\
\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x63\x00\x6c\x00\x6f\x00\x73\x00\x65\x00\x64\x00\x5f\x00\x6f\x00\x6e\x00\x2e\
\x00\x70\x00\x6e\x00\x67\
\x00\x17\
\x0c\xab\x51\x07\
\x00\x64\
\x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\x00\x6c\
\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x11\
\x01\x1f\xc3\x87\
\x00\x64\
\x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\
\
\x00\x17\
\x0c\x65\xce\x07\
\x00\x6c\
\x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\x00\x6c\
\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x0c\
\x06\xe6\xe6\x67\
\x00\x75\
\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x15\
\x03\x27\x72\x67\
\x00\x63\
\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\
\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x11\
\x0b\xda\x30\xa7\
\x00\x62\
\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x63\x00\x6c\x00\x6f\x00\x73\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\
\
\x00\x17\
\x0c\xab\x51\x07\
\x00\x64\
\x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\x00\x6c\
\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x11\
\x00\xb8\x8c\x07\
\x00\x6c\
\x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\
\
\x00\x12\
\x01\x2e\x03\x27\
\x00\x63\
\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\
\x00\x67\
\x00\x0f\
\x02\x9f\x05\x87\
\x00\x72\
\x00\x69\x00\x67\x00\x68\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x15\
\x0f\xf3\xc0\x07\
\x00\x75\
\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x64\
\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x0f\
\x0c\xe2\x68\x67\
\x00\x74\
\x00\x72\x00\x61\x00\x6e\x00\x73\x00\x70\x00\x61\x00\x72\x00\x65\x00\x6e\x00\x74\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x0e\
\x04\xa2\xfc\xa7\
\x00\x64\
\x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x1b\
\x03\x5a\x32\x27\
\x00\x63\
\x00\x6f\x00\x6d\x00\x62\x00\x6f\x00\x62\x00\x6f\x00\x78\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\
\x00\x73\x00\x61\x00\x62\x00\x6c\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x17\
\x0c\x65\xce\x07\
\x00\x6c\
\x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x64\x00\x69\x00\x73\x00\x61\x00\x62\x00\x6c\
\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x11\
\x01\x1f\xc3\x87\
\x00\x64\
\x00\x6f\x00\x77\x00\x6e\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\
\
\x00\x12\
\x05\x8f\x9d\x07\
\x00\x62\
\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x6f\x00\x70\x00\x65\x00\x6e\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\
\x00\x67\
\x00\x12\
\x03\x8d\x04\x47\
\x00\x72\
\x00\x69\x00\x67\x00\x68\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\
\x00\x67\
\x00\x0e\
\x0e\xde\xfa\xc7\
\x00\x6c\
\x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x0f\
\x06\x53\x25\xa7\
\x00\x14\
\x04\x5e\x2d\xa7\
\x00\x62\
\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x6f\x00\x70\x00\x65\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x72\x00\x61\x00\x6e\x00\x63\x00\x68\x00\x5f\x00\x63\x00\x6c\x00\x6f\x00\x73\x00\x65\x00\x64\x00\x5f\x00\x6f\x00\x6e\x00\x2e\
\x00\x70\x00\x6e\x00\x67\
\x00\x0c\
\x06\xe6\xe6\x67\
\x00\x75\
\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\
\x00\x0f\
\x01\x73\x8b\x07\
\x00\x75\
\x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x5f\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\
"
qt_resource_struct_v1 = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x16\x00\x02\x00\x00\x00\x13\x00\x00\x00\x03\
\x00\x00\x02\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x1f\x3c\
\x00\x00\x02\x3c\x00\x00\x00\x00\x00\x01\x00\x00\x1c\x9d\
\x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x13\x68\
\x00\x00\x01\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x12\x1d\
\x00\x00\x00\x16\x00\x02\x00\x00\x00\x14\x00\x00\x00\x03\
\x00\x00\x01\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x10\xb3\
\x00\x00\x02\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x15\x93\
\x00\x00\x01\x36\x00\x00\x00\x00\x00\x01\x00\x00\x11\x5d\
\x00\x00\x03\x54\x00\x00\x00\x00\x00\x01\x00\x00\x27\x41\
\x00\x00\x01\x60\x00\x00\x00\x00\x00\x01\x00\x00\x12\x07\
\x00\x00\x00\x82\x00\x00\x00\x00\x00\x01\x00\x00\x07\xae\
\x00\x00\x01\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x14\x40\
\x00\x00\x02\xbc\x00\x00\x00\x00\x00\x01\x00\x00\x1d\x70\
\x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x07\x0a\
\x00\x00\x03\x08\x00\x00\x00\x00\x00\x01\x00\x00\x1e\xbe\
\x00\x00\x01\xd8\x00\x00\x00\x00\x00\x01\x00\x00\x13\x97\
\x00\x00\x02\x92\x00\x00\x00\x00\x00\x01\x00\x00\x16\x3c\
\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x02\xb6\x00\x00\x00\x00\x00\x01\x00\x00\x1e\x92\
\x00\x00\x00\x76\x00\x00\x00\x00\x00\x01\x00\x00\x07\xd8\
\x00\x00\x01\x10\x00\x00\x00\x00\x00\x01\x00\x00\x10\xd6\
\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x08\x81\
\x00\x00\x01\xda\x00\x00\x00\x00\x00\x01\x00\x00\x14\x12\
\x00\x00\x01\x8e\x00\x00\x00\x00\x00\x01\x00\x00\x12\xbf\
\x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa4\
\x00\x00\x03\x30\x00\x00\x00\x00\x00\x01\x00\x00\x20\x90\
\x00\x00\x02\x98\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf0\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x09\x25\
\x00\x00\x02\x64\x00\x00\x00\x00\x00\x01\x00\x00\x1d\x46\
\x00\x00\x02\x08\x00\x00\x00\x00\x00\x01\x00\x00\x1b\xf3\
\x00\x00\x03\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x1f\xe6\
\x00\x00\x01\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x11\x7a\
\x00\x00\x03\x36\x00\x00\x00\x00\x00\x01\x00\x00\x26\x9f\
\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x08\x58\
\x00\x00\x02\x36\x00\x00\x00\x00\x00\x01\x00\x00\x14\xe9\
\x00\x00\x00\xda\x00\x00\x00\x00\x00\x01\x00\x00\x10\x09\
\x00\x00\x01\xb4\x00\x00\x00\x00\x00\x01\x00\x00\x13\x4e\
\x00\x00\x02\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x1e\x14\
\x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x00\x12\xab\
"
qt_resource_struct_v2 = b"\
@ -816,49 +828,50 @@ qt_resource_struct_v2 = b"\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x16\x00\x02\x00\x00\x00\x13\x00\x00\x00\x03\
\x00\x00\x00\x16\x00\x02\x00\x00\x00\x14\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x02\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x1f\x3c\
\x00\x00\x01\x76\x41\x9d\xa2\x35\
\x00\x00\x02\x3c\x00\x00\x00\x00\x00\x01\x00\x00\x1c\x9d\
\x00\x00\x01\x76\x41\x9d\xa2\x35\
\x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x13\x68\
\x00\x00\x01\x79\xb4\x72\xcc\x9c\
\x00\x00\x01\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x12\x1d\
\x00\x00\x01\x76\x41\x9d\xa2\x39\
\x00\x00\x01\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x10\xb3\
\x00\x00\x01\x79\xec\x37\x3f\xbc\
\x00\x00\x02\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x15\x93\
\x00\x00\x01\x79\xec\x37\x3f\xba\
\x00\x00\x01\x36\x00\x00\x00\x00\x00\x01\x00\x00\x11\x5d\
\x00\x00\x01\x79\xec\x37\x3f\xb6\
\x00\x00\x03\x54\x00\x00\x00\x00\x00\x01\x00\x00\x27\x41\
\x00\x00\x01\x79\xec\x37\x3f\xc0\
\x00\x00\x01\x60\x00\x00\x00\x00\x00\x01\x00\x00\x12\x07\
\x00\x00\x01\x79\xec\x37\x3f\xbc\
\x00\x00\x00\x82\x00\x00\x00\x00\x00\x01\x00\x00\x07\xae\
\x00\x00\x01\x79\xec\x37\x3f\xb7\
\x00\x00\x01\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x14\x40\
\x00\x00\x01\x79\xec\x37\x3f\xb7\
\x00\x00\x02\xbc\x00\x00\x00\x00\x00\x01\x00\x00\x1d\x70\
\x00\x00\x01\x79\xec\x37\x3f\xbe\
\x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x07\x0a\
\x00\x00\x01\x79\xec\x37\x3f\xbd\
\x00\x00\x03\x08\x00\x00\x00\x00\x00\x01\x00\x00\x1e\xbe\
\x00\x00\x01\x79\xec\x37\x3f\xb4\
\x00\x00\x01\xd8\x00\x00\x00\x00\x00\x01\x00\x00\x13\x97\
\x00\x00\x01\x79\xec\x37\x3f\xb8\
\x00\x00\x02\x92\x00\x00\x00\x00\x00\x01\x00\x00\x16\x3c\
\x00\x00\x01\x79\xec\x37\x3f\xb5\
\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x76\x41\x9d\xa2\x37\
\x00\x00\x02\xb6\x00\x00\x00\x00\x00\x01\x00\x00\x1e\x92\
\x00\x00\x01\x79\xb4\x72\xcc\x9c\
\x00\x00\x00\x76\x00\x00\x00\x00\x00\x01\x00\x00\x07\xd8\
\x00\x00\x01\x79\xb4\x72\xcc\x9c\
\x00\x00\x01\x10\x00\x00\x00\x00\x00\x01\x00\x00\x10\xd6\
\x00\x00\x01\x76\x41\x9d\xa2\x37\
\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x08\x81\
\x00\x00\x01\x76\x41\x9d\xa2\x37\
\x00\x00\x01\xda\x00\x00\x00\x00\x00\x01\x00\x00\x14\x12\
\x00\x00\x01\x79\xc2\x05\x2b\x60\
\x00\x00\x01\x8e\x00\x00\x00\x00\x00\x01\x00\x00\x12\xbf\
\x00\x00\x01\x76\x41\x9d\xa2\x35\
\x00\x00\x00\x4c\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa4\
\x00\x00\x01\x79\xc1\xfc\x16\x91\
\x00\x00\x03\x30\x00\x00\x00\x00\x00\x01\x00\x00\x20\x90\
\x00\x00\x01\x79\xc1\xf9\x4b\x78\
\x00\x00\x02\x98\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf0\
\x00\x00\x01\x76\x41\x9d\xa2\x39\
\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x09\x25\
\x00\x00\x01\x79\xc2\x05\x91\x2a\
\x00\x00\x02\x64\x00\x00\x00\x00\x00\x01\x00\x00\x1d\x46\
\x00\x00\x01\x76\x41\x9d\xa2\x35\
\x00\x00\x02\x08\x00\x00\x00\x00\x00\x01\x00\x00\x1b\xf3\
\x00\x00\x01\x76\x41\x9d\xa2\x35\
\x00\x00\x03\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x1f\xe6\
\x00\x00\x01\x76\x41\x9d\xa2\x35\
\x00\x00\x01\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x11\x7a\
\x00\x00\x01\x76\x41\x9d\xa2\x39\
\x00\x00\x01\x79\xec\x37\x3f\xb5\
\x00\x00\x03\x36\x00\x00\x00\x00\x00\x01\x00\x00\x26\x9f\
\x00\x00\x01\x79\xec\x37\x3f\xbe\
\x00\x00\x00\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x08\x58\
\x00\x00\x01\x79\xec\x37\x3f\xb3\
\x00\x00\x02\x36\x00\x00\x00\x00\x00\x01\x00\x00\x14\xe9\
\x00\x00\x01\x79\xec\x37\x3f\xbb\
\x00\x00\x00\xda\x00\x00\x00\x00\x00\x01\x00\x00\x10\x09\
\x00\x00\x01\x79\xec\x37\x3f\xb9\
\x00\x00\x01\xb4\x00\x00\x00\x00\x00\x01\x00\x00\x13\x4e\
\x00\x00\x01\x7c\xa7\x41\xfc\x00\
\x00\x00\x02\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x1e\x14\
\x00\x00\x01\x79\xec\x37\x3f\xbb\
\x00\x00\x01\x84\x00\x00\x00\x00\x00\x01\x00\x00\x12\xab\
\x00\x00\x01\x79\xec\x37\x3f\xbf\
"
qt_version = [int(v) for v in QtCore.qVersion().split('.')]
if qt_version < [5, 8, 0]:
rcc_version = 1

View file

@ -1,75 +1,15 @@
# Resource object code (Python 3)
# Created by: object code
# Created by: The Resource Compiler for Qt version 5.15.2
# -*- coding: utf-8 -*-
# Resource object code
#
# Created: Fri Oct 22 11:42:52 2021
# by: The Resource Compiler for PySide2 (Qt v5.6.1)
#
# WARNING! All changes made in this file will be lost!
from PySide2 import QtCore
qt_resource_data = b"\
\x00\x00\x00\x9f\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x14\x1f\xf9\
#\xd9\x0b\x00\x00\x00#IDAT\x08\xd7c`\xc0\
\x0d\xe6|\x80\xb1\x18\x91\x05R\x04\xe0B\x08\x15)\x02\
\x0c\x0c\x8c\xc8\x02\x08\x95h\x00\x00\xac\xac\x07\x90Ne\
4\xac\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x00\xa6\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\
;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\
\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\
\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\
\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\
D\xaeB`\x82\
\x00\x00\x00\xa5\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\
\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\
\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\
200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\
\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\
\xaeB`\x82\
\x00\x00\x00\xa5\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\
\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\
\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\
200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\
\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\
\xaeB`\x82\
\x00\x00\x00\xa0\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f\x0d\xfc\
R+\x9c\x00\x00\x00$IDAT\x08\xd7c`@\
\x05s>\xc0XL\xc8\x5c&dY&d\xc5pN\
\x8a\x00\x9c\x93\x22\x80a\x1a\x0a\x00\x00)\x95\x08\xaf\x88\
\xac\xba4\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x00\xa6\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@ -83,31 +23,33 @@ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
d``b``4D\xe2 s\x19\x90\x8d@\x02\
\x00d@\x09u\x86\xb3\xad\x9c\x00\x00\x00\x00IEN\
D\xaeB`\x82\
\x00\x00\x00\x9e\
\x00\x00\x00\xa5\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\
\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\
\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\
200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\
\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\
\xaeB`\x82\
\x00\x00\x00\xa6\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\
\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\
\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\
\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\
\xc5\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x00\x9e\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\
\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\
\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\
\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\
\xc5\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x07\x06\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\
;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\
\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\
\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\
\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\
D\xaeB`\x82\
\x00\x00\x070\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x001\xac\xdcc\
@ -165,10 +107,10 @@ toshop:ICCProfil\
e=\x22sRGB IEC61966\
-2.1\x22\x0a xmp:Mod\
ifyDate=\x222021-05\
-31T12:30:11+02:\
-31T12:33:14+02:\
00\x22\x0a xmp:Metad\
ataDate=\x222021-05\
-31T12:30:11+02:\
-31T12:33:14+02:\
00\x22>\x0a <xmpMM:H\
istory>\x0a <rdf\
:Seq>\x0a <rdf:\
@ -179,14 +121,14 @@ twareAgent=\x22Affi\
nity Designer 1.\
9.2\x22\x0a stEvt\
:when=\x222021-05-3\
1T12:30:11+02:00\
1T12:33:14+02:00\
\x22/>\x0a </rdf:Se\
q>\x0a </xmpMM:Hi\
story>\x0a </rdf:D\
escription>\x0a </r\
df:RDF>\x0a</x:xmpm\
eta>\x0a<?xpacket e\
nd=\x22r\x22?>\x85\x9d\x9f\x08\x00\x00\x01\x83\
nd=\x22r\x22?>H\x8b[^\x00\x00\x01\x83\
iCCPsRGB IEC6196\
6-2.1\x00\x00(\x91u\x91\xcf+DQ\x14\
\xc7?fh\xfc\x18\x8dba1e\x12\x16B\x83\x12\
@ -213,28 +155,54 @@ S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\xc5\x9e\
\xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e9\xef\
Y\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00mIDAT\x18\x95u\xcf\xc1\x09\xc2P\
\x10\x84\xe1\xd7\x85\x07\x9b\xd0C@\xd2\x82x\x14{0\
W!\x8d\x84`?bKzH\xcc\x97\x83\xfb0\x04\
\xdf\x9c\x86\x7fg\x99\xdd\x84\x0d\xaaT\x10jl\x13\x1e\
\xbe\xba\xfe\x0951{\xe6\x8d\x0f&\x1c\x17\xa1S\xb0\
\x11\x87\x0c/\x01\x07\xec\xb0\x0f?\xe1\xbc\xaei\xa3\xe6\
\x85w\xf8[\xe9\xf0\xbb\x9f\xfa\xd2\x839\xdc\xa3[\xf3\
\x19.\xa8\x89\xb50\xf7C\xa0\x00\x00\x00\x00IEN\
D\xaeB`\x82\
\x00\x00\x00\xa6\
\x00\x00\x00\x97IDAT\x18\x95m\xcf\xb1j\x02A\
\x14\x85\xe1o\xb7\xb6\xd0'H=Vi\x03\xb1\xb4H\
;l\xa5\xf19\xf6Y\x02VB\xbaa\x0a\x0b;\x1b\
\x1bkA\x18\x02)m\xe3\xbe\x82\xcd\x06\x16\xd9\xdb\xdd\
\x9f\xff\x5c\xee\xa9b*\x13Ls\x13nF&\xa6\xf2\
\x82\xaeF\x8b\xdf\x98\xca\xfb\x88\xb4\xc0\x0f\xda\x1a[t\
\xd8\xc7T\xc2@\x9ac\x8f?|U=|\xc5\x09w\
\xbc\xa1\xc2\x193,r\x13.\xd5\xe0\xc2\x12\x07\x5cQ\
#\xe0#7\xe1\xa8O\x0e\x7f\xda`\xd7\xaf\x9f\xb9\x09\
\xdfc\x05\xff\xe5uLe\xf5\xcc\x1f\x0d3,\x83\xb6\
\x06D\x83\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x00\xa0\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f \xb9\
\x8dw\xe9\x00\x00\x00*IDAT\x08\xd7c`\xc0\
\x06\xe6|```B0\xa1\x1c\x08\x93\x81\x81\x09\xc1\
d``b`H\x11@\xe2 s\x19\x90\x8d@\x02\
\x00#\xed\x08\xafd\x9f\x0f\x15\x00\x00\x00\x00IEN\
D\xaeB`\x82\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\
\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\
\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\
\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\
\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x00\x9e\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\
\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\
\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\
\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\
\xc5\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x00\xa5\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\
\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\
\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\
200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\
\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\
\xaeB`\x82\
\x00\x00\x07\xdd\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@ -363,6 +331,221 @@ zpp\xf0\xe3\x0e.\xa4\xd2\xae\xf0\x8a\xf7\x9a\xe3V\
q[s\x5c@H\xa5\xdda\x81\x0d\x9ek\x8e\xff\xfd\
\xcf?\xcc1\xe9\x01\x1c\x00sR-q\xe4J\x1bi\
\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x00\x9e\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15\x0f\xfd\
\x8f\xf8.\x00\x00\x00\x22IDAT\x08\xd7c`\xc0\
\x0d\xfe\x9f\x87\xb1\x18\x91\x05\x18\x0d\xe1BH*\x0c\x19\
\x18\x18\x91\x05\x10*\xd1\x00\x00\xca\xb5\x07\xd2v\xbb\xb2\
\xc5\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x00\xa6\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\
;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\
\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\
\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\
\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\
D\xaeB`\x82\
\x00\x00\x00\xa6\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\
;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\
\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\
\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\
\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\
D\xaeB`\x82\
\x00\x00\x00\xa5\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\
\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\
\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\
200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\
\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\
\xaeB`\x82\
\x00\x00\x00\xa0\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f\x0d\xfc\
R+\x9c\x00\x00\x00$IDAT\x08\xd7c`@\
\x05s>\xc0XL\xc8\x5c&dY&d\xc5pN\
\x8a\x00\x9c\x93\x22\x80a\x1a\x0a\x00\x00)\x95\x08\xaf\x88\
\xac\xba4\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x00\x9f\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x14\x1f\xf9\
#\xd9\x0b\x00\x00\x00#IDAT\x08\xd7c`\xc0\
\x0d\xe6|\x80\xb1\x18\x91\x05R\x04\xe0B\x08\x15)\x02\
\x0c\x0c\x8c\xc8\x02\x08\x95h\x00\x00\xac\xac\x07\x90Ne\
4\xac\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x07\x06\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x001\xac\xdcc\
\x00\x00\x04\xb0iTXtXML:com.\
adobe.xmp\x00\x00\x00\x00\x00<?\
xpacket begin=\x22\xef\
\xbb\xbf\x22 id=\x22W5M0MpCe\
hiHzreSzNTczkc9d\
\x22?>\x0a<x:xmpmeta x\
mlns:x=\x22adobe:ns\
:meta/\x22 x:xmptk=\
\x22XMP Core 5.5.0\x22\
>\x0a <rdf:RDF xmln\
s:rdf=\x22http://ww\
w.w3.org/1999/02\
/22-rdf-syntax-n\
s#\x22>\x0a <rdf:Desc\
ription rdf:abou\
t=\x22\x22\x0a xmlns:e\
xif=\x22http://ns.a\
dobe.com/exif/1.\
0/\x22\x0a xmlns:ti\
ff=\x22http://ns.ad\
obe.com/tiff/1.0\
/\x22\x0a xmlns:pho\
toshop=\x22http://n\
s.adobe.com/phot\
oshop/1.0/\x22\x0a \
xmlns:xmp=\x22http:\
//ns.adobe.com/x\
ap/1.0/\x22\x0a xml\
ns:xmpMM=\x22http:/\
/ns.adobe.com/xa\
p/1.0/mm/\x22\x0a x\
mlns:stEvt=\x22http\
://ns.adobe.com/\
xap/1.0/sType/Re\
sourceEvent#\x22\x0a \
exif:PixelXDime\
nsion=\x2210\x22\x0a ex\
if:PixelYDimensi\
on=\x227\x22\x0a exif:C\
olorSpace=\x221\x22\x0a \
tiff:ImageWidth\
=\x2210\x22\x0a tiff:Im\
ageLength=\x227\x22\x0a \
tiff:Resolution\
Unit=\x222\x22\x0a tiff\
:XResolution=\x2272\
.0\x22\x0a tiff:YRes\
olution=\x2272.0\x22\x0a \
photoshop:Colo\
rMode=\x223\x22\x0a pho\
toshop:ICCProfil\
e=\x22sRGB IEC61966\
-2.1\x22\x0a xmp:Mod\
ifyDate=\x222021-05\
-31T12:30:11+02:\
00\x22\x0a xmp:Metad\
ataDate=\x222021-05\
-31T12:30:11+02:\
00\x22>\x0a <xmpMM:H\
istory>\x0a <rdf\
:Seq>\x0a <rdf:\
li\x0a stEvt:a\
ction=\x22produced\x22\
\x0a stEvt:sof\
twareAgent=\x22Affi\
nity Designer 1.\
9.2\x22\x0a stEvt\
:when=\x222021-05-3\
1T12:30:11+02:00\
\x22/>\x0a </rdf:Se\
q>\x0a </xmpMM:Hi\
story>\x0a </rdf:D\
escription>\x0a </r\
df:RDF>\x0a</x:xmpm\
eta>\x0a<?xpacket e\
nd=\x22r\x22?>\x85\x9d\x9f\x08\x00\x00\x01\x83\
iCCPsRGB IEC6196\
6-2.1\x00\x00(\x91u\x91\xcf+DQ\x14\
\xc7?fh\xfc\x18\x8dba1e\x12\x16B\x83\x12\
\x1b\x8b\x99\x18\x0a\x8b\x99Q~mf\x9ey3j\xde\
x\xbd7\xd2d\xabl\xa7(\xb1\xf1k\xc1_\xc0V\
Y+E\xa4d\xa7\xac\x89\x0dz\xce\x9bQ#\x99s\
;\xf7|\xee\xf7\xdes\xba\xf7\x5cpD\xd3\x8afV\
\xfaA\xcbd\x8dp(\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\
\x85\x1a:\xf1\xc6\x14S\x9f\x8c\x8cF)k\xef\xb7T\
\xd8\xf1\xba\xdb\xaeU\xfe\xdc\xbfV\xb7\x980\x15\xa8\xa8\
\x16\x1eVt#+<&<\xb1\x9a\xd5m\xde\x12n\
RR\xb1E\xe1\x13\xe1.C.(|c\xeb\xf1\x22\
?\xdb\x9c,\xf2\xa7\xcdF4\x1c\x04G\x83\xb0/\xf9\
\x8b\xe3\xbfXI\x19\x9a\xb0\xbc\x9c6-\xbd\xa2\xfc\xdc\
\xc7~\x89;\x91\x99\x8eHl\x15\xf7b\x12&D\x00\
\x1f\xe3\x8c\x10d\x80^\x86d\x1e\xa0\x9b>zdE\
\x99|\x7f!\x7f\x8ae\xc9Ud\xd6\xc9a\xb0D\x92\
\x14Y\xbaD]\x91\xea\x09\x89\xaa\xe8\x09\x19irv\
\xff\xff\xf6\xd5T\xfb\xfb\x8a\xd5\xdd\x01\xa8z\xb4\xac\xd7\
vpm\xc2W\xde\xb2>\x0e,\xeb\xeb\x10\x9c\x0fp\
\x9e)\xe5/\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9eu\
8\xbd(i\xf1m8\xdb\x80\xe6{=f\xc4\x0a\x92\
S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\xc5\x9e\
\xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e9\xef\
Y\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00mIDAT\x18\x95u\xcf\xc1\x09\xc2P\
\x10\x84\xe1\xd7\x85\x07\x9b\xd0C@\xd2\x82x\x14{0\
W!\x8d\x84`?bKzH\xcc\x97\x83\xfb0\x04\
\xdf\x9c\x86\x7fg\x99\xdd\x84\x0d\xaaT\x10jl\x13\x1e\
\xbe\xba\xfe\x0951{\xe6\x8d\x0f&\x1c\x17\xa1S\xb0\
\x11\x87\x0c/\x01\x07\xec\xb0\x0f?\xe1\xbc\xaei\xa3\xe6\
\x85w\xf8[\xe9\xf0\xbb\x9f\xfa\xd2\x839\xdc\xa3[\xf3\
\x19.\xa8\x89\xb50\xf7C\xa0\x00\x00\x00\x00IEN\
D\xaeB`\x82\
\x00\x00\x00\xa6\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1f \xb9\
\x8dw\xe9\x00\x00\x00*IDAT\x08\xd7c`\xc0\
\x06\xe6|```B0\xa1\x1c\x08\x93\x81\x81\x09\xc1\
d``b`H\x11@\xe2 s\x19\x90\x8d@\x02\
\x00#\xed\x08\xafd\x9f\x0f\x15\x00\x00\x00\x00IEN\
D\xaeB`\x82\
\x00\x00\x00\xa0\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\
\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\
\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\
\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\
\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x07\xad\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
@ -501,186 +684,6 @@ HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
d``b``4D\xe2 s\x19\x90\x8d@\x02\
\x00d@\x09u\x86\xb3\xad\x9c\x00\x00\x00\x00IEN\
D\xaeB`\x82\
\x00\x00\x00\xa5\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\x9cS4\xfc]\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x0b\x02\x04m\
\x98\x1bi\x00\x00\x00)IDAT\x08\xd7c`\xc0\
\x00\x8c\x0c\x0c\xff\xcf\xa3\x08\x18220 \x0b2\x1a\
200B\x98\x10AFC\x14\x13P\xb5\xa3\x01\x00\
\xd6\x10\x07\xd2/H\xdfJ\x00\x00\x00\x00IEND\
\xaeB`\x82\
\x00\x00\x00\xa0\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\
\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\
\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\
\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\
\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x070\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x0a\x00\x00\x00\x07\x08\x06\x00\x00\x001\xac\xdcc\
\x00\x00\x04\xb0iTXtXML:com.\
adobe.xmp\x00\x00\x00\x00\x00<?\
xpacket begin=\x22\xef\
\xbb\xbf\x22 id=\x22W5M0MpCe\
hiHzreSzNTczkc9d\
\x22?>\x0a<x:xmpmeta x\
mlns:x=\x22adobe:ns\
:meta/\x22 x:xmptk=\
\x22XMP Core 5.5.0\x22\
>\x0a <rdf:RDF xmln\
s:rdf=\x22http://ww\
w.w3.org/1999/02\
/22-rdf-syntax-n\
s#\x22>\x0a <rdf:Desc\
ription rdf:abou\
t=\x22\x22\x0a xmlns:e\
xif=\x22http://ns.a\
dobe.com/exif/1.\
0/\x22\x0a xmlns:ti\
ff=\x22http://ns.ad\
obe.com/tiff/1.0\
/\x22\x0a xmlns:pho\
toshop=\x22http://n\
s.adobe.com/phot\
oshop/1.0/\x22\x0a \
xmlns:xmp=\x22http:\
//ns.adobe.com/x\
ap/1.0/\x22\x0a xml\
ns:xmpMM=\x22http:/\
/ns.adobe.com/xa\
p/1.0/mm/\x22\x0a x\
mlns:stEvt=\x22http\
://ns.adobe.com/\
xap/1.0/sType/Re\
sourceEvent#\x22\x0a \
exif:PixelXDime\
nsion=\x2210\x22\x0a ex\
if:PixelYDimensi\
on=\x227\x22\x0a exif:C\
olorSpace=\x221\x22\x0a \
tiff:ImageWidth\
=\x2210\x22\x0a tiff:Im\
ageLength=\x227\x22\x0a \
tiff:Resolution\
Unit=\x222\x22\x0a tiff\
:XResolution=\x2272\
.0\x22\x0a tiff:YRes\
olution=\x2272.0\x22\x0a \
photoshop:Colo\
rMode=\x223\x22\x0a pho\
toshop:ICCProfil\
e=\x22sRGB IEC61966\
-2.1\x22\x0a xmp:Mod\
ifyDate=\x222021-05\
-31T12:33:14+02:\
00\x22\x0a xmp:Metad\
ataDate=\x222021-05\
-31T12:33:14+02:\
00\x22>\x0a <xmpMM:H\
istory>\x0a <rdf\
:Seq>\x0a <rdf:\
li\x0a stEvt:a\
ction=\x22produced\x22\
\x0a stEvt:sof\
twareAgent=\x22Affi\
nity Designer 1.\
9.2\x22\x0a stEvt\
:when=\x222021-05-3\
1T12:33:14+02:00\
\x22/>\x0a </rdf:Se\
q>\x0a </xmpMM:Hi\
story>\x0a </rdf:D\
escription>\x0a </r\
df:RDF>\x0a</x:xmpm\
eta>\x0a<?xpacket e\
nd=\x22r\x22?>H\x8b[^\x00\x00\x01\x83\
iCCPsRGB IEC6196\
6-2.1\x00\x00(\x91u\x91\xcf+DQ\x14\
\xc7?fh\xfc\x18\x8dba1e\x12\x16B\x83\x12\
\x1b\x8b\x99\x18\x0a\x8b\x99Q~mf\x9ey3j\xde\
x\xbd7\xd2d\xabl\xa7(\xb1\xf1k\xc1_\xc0V\
Y+E\xa4d\xa7\xac\x89\x0dz\xce\x9bQ#\x99s\
;\xf7|\xee\xf7\xdes\xba\xf7\x5cpD\xd3\x8afV\
\xfaA\xcbd\x8dp(\xe0\x9b\x99\x9d\xf3\xb9\x9e\xa8\xa2\
\x85\x1a:\xf1\xc6\x14S\x9f\x8c\x8cF)k\xef\xb7T\
\xd8\xf1\xba\xdb\xaeU\xfe\xdc\xbfV\xb7\x980\x15\xa8\xa8\
\x16\x1eVt#+<&<\xb1\x9a\xd5m\xde\x12n\
RR\xb1E\xe1\x13\xe1.C.(|c\xeb\xf1\x22\
?\xdb\x9c,\xf2\xa7\xcdF4\x1c\x04G\x83\xb0/\xf9\
\x8b\xe3\xbfXI\x19\x9a\xb0\xbc\x9c6-\xbd\xa2\xfc\xdc\
\xc7~\x89;\x91\x99\x8eHl\x15\xf7b\x12&D\x00\
\x1f\xe3\x8c\x10d\x80^\x86d\x1e\xa0\x9b>zdE\
\x99|\x7f!\x7f\x8ae\xc9Ud\xd6\xc9a\xb0D\x92\
\x14Y\xbaD]\x91\xea\x09\x89\xaa\xe8\x09\x19irv\
\xff\xff\xf6\xd5T\xfb\xfb\x8a\xd5\xdd\x01\xa8z\xb4\xac\xd7\
vpm\xc2W\xde\xb2>\x0e,\xeb\xeb\x10\x9c\x0fp\
\x9e)\xe5/\xef\xc3\xe0\x9b\xe8\xf9\x92\xd6\xb6\x07\x9eu\
8\xbd(i\xf1m8\xdb\x80\xe6{=f\xc4\x0a\x92\
S\xdc\xa1\xaa\xf0r\x0c\xf5\xb3\xd0x\x05\xb5\xf3\xc5\x9e\
\xfd\xecst\x07\xd15\xf9\xaaK\xd8\xd9\x85\x0e9\xef\
Y\xf8\x06\x8e\xfdg\xf8\xfd\x8a\x18\x97\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x97IDAT\x18\x95m\xcf\xb1j\x02A\
\x14\x85\xe1o\xb7\xb6\xd0'H=Vi\x03\xb1\xb4H\
;l\xa5\xf19\xf6Y\x02VB\xbaa\x0a\x0b;\x1b\
\x1bkA\x18\x02)m\xe3\xbe\x82\xcd\x06\x16\xd9\xdb\xdd\
\x9f\xff\x5c\xee\xa9b*\x13Ls\x13nF&\xa6\xf2\
\x82\xaeF\x8b\xdf\x98\xca\xfb\x88\xb4\xc0\x0f\xda\x1a[t\
\xd8\xc7T\xc2@\x9ac\x8f?|U=|\xc5\x09w\
\xbc\xa1\xc2\x193,r\x13.\xd5\xe0\xc2\x12\x07\x5cQ\
#\xe0#7\xe1\xa8O\x0e\x7f\xda`\xd7\xaf\x9f\xb9\x09\
\xdfc\x05\xff\xe5uLe\xf5\xcc\x1f\x0d3,\x83\xb6\
\x06D\x83\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x00\xa6\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\
;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\
\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\
\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\
\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\
D\xaeB`\x82\
\x00\x00\x00\xa0\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x06\x00\x00\x00\x09\x08\x04\x00\x00\x00\xbb\x93\x95\x16\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x14\x1c\x1f$\
\xc6\x09\x17\x00\x00\x00$IDAT\x08\xd7c`@\
\x05\xff\xcf\xc3XL\xc8\x5c&dY&d\xc5p\x0e\
\xa3!\x9c\xc3h\x88a\x1a\x0a\x00\x00m\x84\x09u7\
\x9e\xd9#\x00\x00\x00\x00IEND\xaeB`\x82\
\x00\x00\x00\xa6\
\x89\
PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
\x00\x00\x09\x00\x00\x00\x06\x08\x04\x00\x00\x00\xbb\xce|N\
\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\
\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x00\x09p\
HYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\
\x00\x00\x00\x07tIME\x07\xdc\x08\x17\x08\x15;\xdc\
;\x0c\x9b\x00\x00\x00*IDAT\x08\xd7c`\xc0\
\x00\x8c\x0c\x0cs> \x0b\xa4\x08020 \x0b\xa6\
\x08000B\x98\x10\xc1\x14\x01\x14\x13P\xb5\xa3\x01\
\x00\xc6\xb9\x07\x90]f\x1f\x83\x00\x00\x00\x00IEN\
D\xaeB`\x82\
"
qt_resource_name = b"\
@ -692,62 +695,6 @@ qt_resource_name = b"\
\x07\x03}\xc3\
\x00i\
\x00m\x00a\x00g\x00e\x00s\
\x00\x15\
\x0f\xf3\xc0\x07\
\x00u\
\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\
\x00.\x00p\x00n\x00g\
\x00\x12\
\x01.\x03'\
\x00c\
\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\
\x00g\
\x00\x0e\
\x04\xa2\xfc\xa7\
\x00d\
\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\
\x00\x1b\
\x03Z2'\
\x00c\
\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\
\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\
\x00\x18\
\x03\x8e\xdeg\
\x00r\
\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\
\x00l\x00e\x00d\x00.\x00p\x00n\x00g\
\x00\x11\
\x00\xb8\x8c\x07\
\x00l\
\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\
\
\x00\x0f\
\x01s\x8b\x07\
\x00u\
\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\
\x00\x0c\
\x06\xe6\xe6g\
\x00u\
\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\
\x00\x0f\
\x06S%\xa7\
\x00b\
\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00.\x00p\x00n\x00g\
\x00\x17\
\x0ce\xce\x07\
\x00l\
\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\
\x00e\x00d\x00.\x00p\x00n\x00g\
\x00\x14\
\x04^-\xa7\
\x00b\
\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00_\x00o\x00n\x00.\
\x00p\x00n\x00g\
\x00\x11\
\x0b\xda0\xa7\
\x00b\
\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00.\x00p\x00n\x00g\
\
\x00\x0e\
\x0e\xde\xfa\xc7\
\x00l\
@ -757,87 +704,121 @@ qt_resource_name = b"\
\x00d\
\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\
\
\x00\x0f\
\x02\x9f\x05\x87\
\x00r\
\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\
\x00\x12\
\x01.\x03'\
\x00c\
\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\
\x00g\
\x00\x12\
\x05\x8f\x9d\x07\
\x00b\
\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00_\x00o\x00n\x00.\x00p\x00n\
\x00g\
\x00\x17\
\x0c\xabQ\x07\
\x00d\
\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\
\x00e\x00d\x00.\x00p\x00n\x00g\
\x00\x12\
\x03\x8d\x04G\
\x00r\
\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\
\x00g\
\x00\x0f\
\x01s\x8b\x07\
\x00u\
\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\
\x00\x1b\
\x03Z2'\
\x00c\
\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\
\x00s\x00a\x00b\x00l\x00e\x00d\x00.\x00p\x00n\x00g\
\x00\x14\
\x04^-\xa7\
\x00b\
\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00_\x00o\x00n\x00.\
\x00p\x00n\x00g\
\x00\x0c\
\x06\xe6\xe6g\
\x00u\
\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\
\x00\x17\
\x0c\xabQ\x07\
\x00d\
\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\
\x00e\x00d\x00.\x00p\x00n\x00g\
\x00\x15\
\x03'rg\
\x00c\
\x00o\x00m\x00b\x00o\x00b\x00o\x00x\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\
\x00.\x00p\x00n\x00g\
\x00\x0e\
\x04\xa2\xfc\xa7\
\x00d\
\x00o\x00w\x00n\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\
\x00\x18\
\x03\x8e\xdeg\
\x00r\
\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\
\x00l\x00e\x00d\x00.\x00p\x00n\x00g\
\x00\x15\
\x0f\xf3\xc0\x07\
\x00u\
\x00p\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\x00e\x00d\
\x00.\x00p\x00n\x00g\
\x00\x0f\
\x06S%\xa7\
\x00b\
\x00r\x00a\x00n\x00c\x00h\x00_\x00o\x00p\x00e\x00n\x00.\x00p\x00n\x00g\
\x00\x17\
\x0ce\xce\x07\
\x00l\
\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00d\x00i\x00s\x00a\x00b\x00l\
\x00e\x00d\x00.\x00p\x00n\x00g\
\x00\x0f\
\x02\x9f\x05\x87\
\x00r\
\x00i\x00g\x00h\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00.\x00p\x00n\x00g\
\x00\x11\
\x0b\xda0\xa7\
\x00b\
\x00r\x00a\x00n\x00c\x00h\x00_\x00c\x00l\x00o\x00s\x00e\x00d\x00.\x00p\x00n\x00g\
\
\x00\x11\
\x00\xb8\x8c\x07\
\x00l\
\x00e\x00f\x00t\x00_\x00a\x00r\x00r\x00o\x00w\x00_\x00o\x00n\x00.\x00p\x00n\x00g\
\
"
qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x16\x00\x02\x00\x00\x00\x13\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x01\x16\x00\x00\x00\x00\x00\x01\x00\x00\x03C\
\x00\x00\x01vA\x9d\xa25\
\x00\x00\x02P\x00\x00\x00\x00\x00\x01\x00\x00\x1d!\
\x00\x00\x01vA\x9d\xa25\
\x00\x00\x00X\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa3\
\x00\x00\x01y\xb4r\xcc\x9c\
\x00\x00\x01>\x00\x00\x00\x00\x00\x01\x00\x00\x03\xed\
\x00\x00\x01vA\x9d\xa29\
\x00\x00\x02x\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xca\
\x00\x00\x01vA\x9d\xa27\
\x00\x00\x03$\x00\x00\x00\x00\x00\x01\x00\x00&\xf0\
\x00\x00\x01y\xb4r\xcc\x9c\
\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x01\xf6\
\x00\x00\x01y\xb4r\xcc\x9c\
\x00\x00\x02\xfa\x00\x00\x00\x00\x00\x01\x00\x00&L\
\x00\x00\x01vA\x9d\xa27\
\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x02\x9f\
\x00\x00\x01vA\x9d\xa27\
\x00\x00\x01\xd8\x00\x00\x00\x00\x00\x01\x00\x00\x0c\xe5\
\x00\x00\x01y\xc2\x05+`\
\x00\x00\x00\x82\x00\x00\x00\x00\x00\x01\x00\x00\x01M\
\x00\x00\x01vA\x9d\xa25\
\x00\x00\x02\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x1en\
\x00\x00\x01y\xc1\xfc\x16\x91\
\x00\x00\x01\x80\x00\x00\x00\x00\x00\x01\x00\x00\x051\
\x00\x00\x01y\xc1\xf9Kx\
\x00\x00\x01b\x00\x00\x00\x00\x00\x01\x00\x00\x04\x8f\
\x00\x00\x01vA\x9d\xa29\
\x00\x00\x02\x06\x00\x00\x00\x00\x00\x01\x00\x00\x14\xc6\
\x00\x00\x01y\xc2\x05\x91*\
\x00\x00\x01\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x0c;\
\x00\x00\x01vA\x9d\xa25\
\x00\x00\x02\xc6\x00\x00\x00\x00\x00\x01\x00\x00%\xa2\
\x00\x00\x01vA\x9d\xa25\
\x00\x00\x02.\x00\x00\x00\x00\x00\x01\x00\x00\x1cw\
\x00\x00\x01vA\x9d\xa25\
\x00\x00\x03,\x00\x00\x00\x00\x00\x01\x00\x00&\xf0\
\x00\x00\x00J\x00\x00\x00\x00\x00\x01\x00\x00\x00\xaa\
\x00\x00\x00r\x00\x00\x00\x00\x00\x01\x00\x00\x01S\
\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x01\x00\x00\x09\xd5\
\x00\x00\x02\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x1e\x9b\
\x00\x00\x01\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x14M\
\x00\x00\x01\x14\x00\x00\x00\x00\x00\x01\x00\x00\x0aw\
\x00\x00\x00\xc6\x00\x00\x00\x00\x00\x01\x00\x00\x091\
\x00\x00\x02\x22\x00\x00\x00\x00\x00\x01\x00\x00\x15\xa0\
\x00\x00\x01P\x00\x00\x00\x00\x00\x01\x00\x00\x0b \
\x00\x00\x02\x00\x00\x00\x00\x00\x00\x01\x00\x00\x14\xf7\
\x00\x00\x00\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x01\xfd\
\x00\x00\x02\x88\x00\x00\x00\x00\x00\x01\x00\x00\x16\xe7\
\x00\x00\x01~\x00\x00\x00\x00\x00\x01\x00\x00\x13\x01\
\x00\x00\x03\x04\x00\x00\x00\x00\x00\x01\x00\x00\x1f?\
\x00\x00\x02\xac\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf1\
\x00\x00\x01\x9c\x00\x00\x00\x00\x00\x01\x00\x00\x13\xa3\
\x00\x00\x00(\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01vA\x9d\xa29\
\x00\x00\x02X\x00\x00\x00\x00\x00\x01\x00\x00\x16D\
"
def qInitResources():
QtCore.qRegisterResourceData(
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
0x01, qt_resource_struct, qt_resource_name, qt_resource_data
)
def qCleanupResources():
QtCore.qUnregisterResourceData(
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
0x01, qt_resource_struct, qt_resource_name, qt_resource_data
)

View file

@ -19,5 +19,6 @@
<file>images/up_arrow.png</file>
<file>images/up_arrow_disabled.png</file>
<file>images/up_arrow_on.png</file>
<file>images/transparent.png</file>
</qresource>
</RCC>

View file

@ -200,12 +200,28 @@ QComboBox::down-arrow, QComboBox::down-arrow:on, QComboBox::down-arrow:hover, QC
}
/* Splitter */
QSplitter {
border: none;
QSplitter::handle {
border: 3px solid transparent;
}
QSplitter::handle {
border: 1px dotted {color:bg-menu-separator};
QSplitter::handle:horizontal {
/* must be single like because of Nuke*/
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,stop:0.3 rgba(0, 0, 0, 0),stop:0.5 {color:bg-splitter},stop:0.7 rgba(0, 0, 0, 0));
}
QSplitter::handle:vertical {
/* must be single like because of Nuke*/
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,stop:0.3 rgba(0, 0, 0, 0),stop:0.5 {color:bg-splitter},stop:0.7 rgba(0, 0, 0, 0));
}
QSplitter::handle:horizontal:hover {
/* must be single like because of Nuke*/
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,stop:0.3 rgba(0, 0, 0, 0),stop:0.5 {color:bg-splitter-hover},stop:0.7 rgba(0, 0, 0, 0));
}
QSplitter::handle:vertical:hover {
/* must be single like because of Nuke*/
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,stop:0.3 rgba(0, 0, 0, 0),stop:0.5 {color:bg-splitter-hover},stop:0.7 rgba(0, 0, 0, 0));
}
/* SLider */
@ -232,18 +248,15 @@ QSlider::groove:focus {
border-color: {color:border-focus};
}
QSlider::handle {
background: qlineargradient(
x1: 0, y1: 0.5,
x2: 1, y2: 0.5,
stop: 0 {palette:blue-base},
stop: 1 {palette:green-base}
);
/* must be single like because of Nuke*/
background: qlineargradient(x1: 0, y1: 0.5, x2: 1, y2: 0.5,stop: 0 {palette:blue-base},stop: 1 {palette:green-base});
border: 1px solid #5c5c5c;
width: 10px;
height: 10px;
border-radius: 5px;
}
QSlider::handle:horizontal {
margin: -2px 0;
}
@ -252,12 +265,8 @@ QSlider::handle:vertical {
}
QSlider::handle:disabled {
background: qlineargradient(
x1:0, y1:0,
x2:1, y2:1,
stop:0 {color:bg-buttons},
stop:1 {color:bg-buttons-disabled}
);
/* must be single like because of Nuke*/
background: qlineargradient(x1:0, y1:0,x2:1, y2:1,stop:0 {color:bg-buttons},stop:1 {color:bg-buttons-disabled});
}
/* Tab widget*/
@ -275,19 +284,15 @@ QTabBar::tab {
border-left: 3px solid transparent;
border-top: 1px solid {color:border};
border-right: 1px solid {color:border};
background: qlineargradient(
x1: 0, y1: 1, x2: 0, y2: 0,
stop: 0.5 {color:bg}, stop: 1.0 {color:bg-inputs}
);
/* must be single like because of Nuke*/
background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0,stop: 0.5 {color:bg}, stop: 1.0 {color:bg-inputs});
}
QTabBar::tab:selected {
background: {color:grey-lighter};
border-left: 3px solid {color:border-focus};
background: qlineargradient(
x1: 0, y1: 1, x2: 0, y2: 0,
stop: 0.5 {color:bg}, stop: 1.0 {color:border}
);
/* must be single like because of Nuke*/
background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0,stop: 0.5 {color:bg}, stop: 1.0 {color:border});
}
QTabBar::tab:!selected {
@ -335,6 +340,15 @@ QHeaderView::section:first {
QHeaderView::section:last {
border-right: none;
}
QHeaderView::down-arrow {
image: url(:/openpype/images/down_arrow.png);
}
QHeaderView::up-arrow {
image: url(:/openpype/images/up_arrow.png);
}
/* Views QListView QTreeView QTableView */
QAbstractItemView {
border: 0px solid {color:border};
@ -393,23 +407,42 @@ QAbstractItemView::branch:open:has-children:has-siblings {
QAbstractItemView::branch:open:has-children:!has-siblings:hover,
QAbstractItemView::branch:open:has-children:has-siblings:hover {
border-image: none;
image: url(:/openpype/images//branch_open_on.png);
image: url(:/openpype/images/branch_open_on.png);
background: transparent;
}
QAbstractItemView::branch:has-children:!has-siblings:closed,
QAbstractItemView::branch:closed:has-children:has-siblings {
border-image: none;
image: url(:/openpype/images//branch_closed.png);
image: url(:/openpype/images/branch_closed.png);
background: transparent;
}
QAbstractItemView::branch:has-children:!has-siblings:closed:hover,
QAbstractItemView::branch:closed:has-children:has-siblings:hover {
border-image: none;
image: url(:/openpype/images//branch_closed_on.png);
image: url(:/openpype/images/branch_closed_on.png);
background: transparent;
}
QAbstractItemView::branch:has-siblings:!adjoins-item {
border-image: none;
image: url(:/openpype/images/transparent.png);
background: transparent;
}
QAbstractItemView::branch:has-siblings:adjoins-item {
border-image: none;
image: url(:/openpype/images/transparent.png);
background: transparent;
}
QAbstractItemView::branch:!has-children:!has-siblings:adjoins-item {
border-image: none;
image: url(:/openpype/images/transparent.png);
background: transparent;
}
/* Progress bar */
QProgressBar {
border: 1px solid {color:border};
@ -425,12 +458,8 @@ QProgressBar:vertical {
}
QProgressBar::chunk {
background: qlineargradient(
x1: 0, y1: 0.5,
x2: 1, y2: 0.5,
stop: 0 {palette:blue-base},
stop: 1 {palette:green-base}
);
/* must be single like because of Nuke*/
background: qlineargradient(x1: 0, y1: 0.5,x2: 1, y2: 0.5,stop: 0 {palette:blue-base},stop: 1 {palette:green-base});
}
/* Scroll bars */
@ -629,3 +658,16 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
#PythonInterpreterOutput, #PythonCodeEditor {
font-family: "Roboto Mono";
}
#SubsetView::item, #RepresentationView:item {
padding: 5px 1px;
border: 0px;
}
#OptionalActionBody, #OptionalActionOption {
background: transparent;
}
#OptionalActionBody[state="hover"], #OptionalActionOption[state="hover"] {
background: {color:bg-view-hover};
}

View file

@ -0,0 +1,14 @@
from .tools_def import (
ExperimentalTools,
LOCAL_EXPERIMENTAL_KEY
)
from .dialog import ExperimentalToolsDialog
__all__ = (
"ExperimentalTools",
"LOCAL_EXPERIMENTAL_KEY",
"ExperimentalToolsDialog"
)

View file

@ -0,0 +1,212 @@
from Qt import QtWidgets, QtCore, QtGui
from openpype.style import (
load_stylesheet,
app_icon_path
)
from .tools_def import ExperimentalTools
class ToolButton(QtWidgets.QPushButton):
triggered = QtCore.Signal(str)
def __init__(self, identifier, *args, **kwargs):
super(ToolButton, self).__init__(*args, **kwargs)
self._identifier = identifier
self.clicked.connect(self._on_click)
def _on_click(self):
self.triggered.emit(self._identifier)
class ExperimentalToolsDialog(QtWidgets.QDialog):
refresh_interval = 3000
def __init__(self, parent=None):
super(ExperimentalToolsDialog, self).__init__(parent)
self.setWindowTitle("OpenPype Experimental tools")
icon = QtGui.QIcon(app_icon_path())
self.setWindowIcon(icon)
# Widgets for cases there are not available experimental tools
empty_widget = QtWidgets.QWidget(self)
empty_label = QtWidgets.QLabel(
"There are no experimental tools available...", empty_widget
)
empty_btns_layout = QtWidgets.QHBoxLayout()
ok_btn = QtWidgets.QPushButton("OK", empty_widget)
empty_btns_layout.setContentsMargins(0, 0, 0, 0)
empty_btns_layout.addStretch(1)
empty_btns_layout.addWidget(ok_btn, 0)
empty_layout = QtWidgets.QVBoxLayout(empty_widget)
empty_layout.setContentsMargins(0, 0, 0, 0)
empty_layout.addWidget(empty_label)
empty_layout.addStretch(1)
empty_layout.addLayout(empty_btns_layout)
# Content of Experimental tools
# Layout where buttons are added
content_layout = QtWidgets.QVBoxLayout()
content_layout.setContentsMargins(0, 0, 0, 0)
# Separator line
separator_widget = QtWidgets.QWidget(self)
separator_widget.setObjectName("Separator")
separator_widget.setMinimumHeight(2)
separator_widget.setMaximumHeight(2)
# Label describing how to turn off tools
tool_btns_widget = QtWidgets.QWidget(self)
tool_btns_label = QtWidgets.QLabel(
(
"You can enable these features in"
"<br><b>OpenPype tray -> Settings -> Experimental tools</b>"
),
tool_btns_widget
)
tool_btns_label.setAlignment(QtCore.Qt.AlignCenter)
tool_btns_layout = QtWidgets.QVBoxLayout(tool_btns_widget)
tool_btns_layout.setContentsMargins(0, 0, 0, 0)
tool_btns_layout.addLayout(content_layout)
tool_btns_layout.addStretch(1)
tool_btns_layout.addWidget(separator_widget, 0)
tool_btns_layout.addWidget(tool_btns_label, 0)
experimental_tools = ExperimentalTools()
# Main layout
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(empty_widget, 1)
layout.addWidget(tool_btns_widget, 1)
refresh_timer = QtCore.QTimer()
refresh_timer.setInterval(self.refresh_interval)
refresh_timer.timeout.connect(self._on_refresh_timeout)
ok_btn.clicked.connect(self._on_ok_click)
self._empty_widget = empty_widget
self._tool_btns_widget = tool_btns_widget
self._content_layout = content_layout
self._experimental_tools = experimental_tools
self._buttons_by_tool_identifier = {}
self._refresh_timer = refresh_timer
# Is dialog first shown
self._first_show = True
# Trigger refresh when window get's activity
self._refresh_on_active = True
# Is window active
self._window_is_active = False
def refresh(self):
self._experimental_tools.refresh_availability()
buttons_to_remove = set(self._buttons_by_tool_identifier.keys())
for idx, tool in enumerate(self._experimental_tools.tools):
identifier = tool.identifier
if identifier in buttons_to_remove:
buttons_to_remove.remove(identifier)
is_new = False
button = self._buttons_by_tool_identifier[identifier]
else:
is_new = True
button = ToolButton(identifier, self._tool_btns_widget)
button.triggered.connect(self._on_btn_trigger)
self._buttons_by_tool_identifier[identifier] = button
self._content_layout.insertWidget(idx, button)
if button.text() != tool.label:
button.setText(tool.label)
if tool.enabled:
button.setToolTip(tool.tooltip)
elif is_new or button.isEnabled():
button.setToolTip((
"You can enable this tool in local settings."
"\n\nOpenPype Tray > Settings > Experimental Tools"
))
if tool.enabled != button.isEnabled():
button.setEnabled(tool.enabled)
for identifier in buttons_to_remove:
button = self._buttons_by_tool_identifier.pop(identifier)
button.setVisible(False)
idx = self._content_layout.indexOf(button)
self._content_layout.takeAt(idx)
button.deleteLater()
self._set_visibility()
def _is_content_visible(self):
return len(self._buttons_by_tool_identifier) > 0
def _set_visibility(self):
content_visible = self._is_content_visible()
self._tool_btns_widget.setVisible(content_visible)
self._empty_widget.setVisible(not content_visible)
def _on_ok_click(self):
self.close()
def _on_btn_trigger(self, identifier):
tool = self._experimental_tools.tools_by_identifier.get(identifier)
if tool is not None:
tool.execute()
def showEvent(self, event):
super(ExperimentalToolsDialog, self).showEvent(event)
if self._refresh_on_active:
# Start/Restart timer
self._refresh_timer.start()
# Refresh
self.refresh()
elif not self._refresh_timer.isActive():
self._refresh_timer.start()
if self._first_show:
self._first_show = False
# Set stylesheet
self.setStyleSheet(load_stylesheet())
# Resize dialog if there is not content
if not self._is_content_visible():
size = self.size()
size.setWidth(size.width() + size.width() / 3)
self.resize(size)
def changeEvent(self, event):
if event.type() == QtCore.QEvent.ActivationChange:
self._window_is_active = self.isActiveWindow()
if self._window_is_active and self._refresh_on_active:
self._refresh_timer.start()
self.refresh()
super(ExperimentalToolsDialog, self).changeEvent(event)
def _on_refresh_timeout(self):
# Stop timer if window is not visible
if not self.isVisible():
self._refresh_on_active = True
self._refresh_timer.stop()
# Skip refreshing if window is not active
elif not self._window_is_active:
self._refresh_on_active = True
# Window is active and visible so we're refreshing buttons
else:
self.refresh()

View file

@ -0,0 +1,142 @@
import os
from openpype.settings import get_local_settings
# Constant key under which local settings are stored
LOCAL_EXPERIMENTAL_KEY = "experimental_tools"
class ExperimentalTool:
"""Definition of experimental tool.
Definition is used in local settings and in experimental tools dialog.
Args:
identifier (str): String identifier of tool (unique).
label (str): Label shown in UI.
callback (function): Callback for UI button.
tooltip (str): Tooltip showed on button.
hosts_filter (list): List of host names for which is tool available.
Some tools may not be available in all hosts.
"""
def __init__(
self, identifier, label, callback, tooltip, hosts_filter=None
):
self.identifier = identifier
self.label = label
self.callback = callback
self.tooltip = tooltip
self.hosts_filter = hosts_filter
self._enabled = True
def is_available_for_host(self, host_name):
if self.hosts_filter:
return host_name in self.hosts_filter
return True
@property
def enabled(self):
"""Is tool enabled and button is clickable."""
return self._enabled
def set_enabled(self, enabled=True):
"""Change if tool is enabled."""
self._enabled = enabled
def execute(self):
"""Trigger registerd callback."""
self.callback()
class ExperimentalTools:
"""Wrapper around experimental tools.
To add/remove experimental tool just add/remove tool to
`experimental_tools` variable in __init__ function.
Args:
parent (QtWidgets.QWidget): Parent widget for tools.
host_name (str): Name of host in which context we're now. Environment
value 'AVALON_APP' is used when not passed.
filter_hosts (bool): Should filter tools. By default is set to 'True'
when 'host_name' is passed. Is always set to 'False' if 'host_name'
is not defined.
"""
def __init__(self, parent=None, host_name=None, filter_hosts=None):
# Definition of experimental tools
experimental_tools = []
# --- Example tool (callback will just print on click) ---
# def example_callback(*args):
# print("Triggered tool")
#
# experimental_tools = [
# ExperimentalTool(
# "example",
# "Example experimental tool",
# example_callback,
# "Example tool tooltip."
# )
# ]
# Try to get host name from env variable `AVALON_APP`
if not host_name:
host_name = os.environ.get("AVALON_APP")
# Decide if filtering by host name should happen
if filter_hosts is None:
filter_hosts = host_name is not None
if filter_hosts and not host_name:
filter_hosts = False
# Filter tools by host name
if filter_hosts:
experimental_tools = [
tool
for tool in experimental_tools
if tool.is_available_for_host(host_name)
]
# Store tools by identifier
tools_by_identifier = {}
for tool in experimental_tools:
if tool.identifier in tools_by_identifier:
raise KeyError((
"Duplicated experimental tool identifier \"{}\""
).format(tool.identifier))
tools_by_identifier[tool.identifier] = tool
self._tools_by_identifier = tools_by_identifier
self._tools = experimental_tools
self._parent_widget = parent
@property
def tools(self):
"""Tools in list.
Returns:
list: Tools filtered by host name if filtering was enabled
on initialization.
"""
return self._tools
@property
def tools_by_identifier(self):
"""Tools by their identifier.
Returns:
dict: Tools by identifier filtered by host name if filtering
was enabled on initialization.
"""
return self._tools_by_identifier
def refresh_availability(self):
"""Reload local settings and check if any tool changed ability."""
local_settings = get_local_settings()
experimental_settings = (
local_settings.get(LOCAL_EXPERIMENTAL_KEY)
) or {}
for identifier, eperimental_tool in self.tools_by_identifier.items():
enabled = experimental_settings.get(identifier, False)
eperimental_tool.set_enabled(enabled)

View file

@ -2,8 +2,8 @@ import sys
from Qt import QtWidgets, QtCore, QtGui
from avalon import style
from avalon.api import AvalonMongoDB
from openpype import style
from openpype.tools.utils import lib as tools_lib
from openpype.tools.loader.widgets import (
ThumbnailWidget,
@ -28,155 +28,182 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
tool_title = "Library Loader 0.5"
tool_name = "library_loader"
message_timeout = 5000
def __init__(
self, parent=None, icon=None, show_projects=False, show_libraries=True
):
super(LibraryLoaderWindow, self).__init__(parent)
self._initial_refresh = False
self._ignore_project_change = False
# Enable minimize and maximize for app
# Window modifications
self.setWindowTitle(self.tool_title)
window_flags = QtCore.Qt.Window
if not parent:
window_flags |= QtCore.Qt.WindowStaysOnTopHint
self.setWindowFlags(window_flags)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
if icon is not None:
self.setWindowIcon(icon)
# self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
body = QtWidgets.QWidget()
footer = QtWidgets.QWidget()
footer.setFixedHeight(20)
icon = QtGui.QIcon(style.app_icon_path())
self.setWindowIcon(icon)
container = QtWidgets.QWidget()
self._first_show = True
self._initial_refresh = False
self._ignore_project_change = False
self.dbcon = AvalonMongoDB()
self.dbcon.install()
self.dbcon.Session["AVALON_PROJECT"] = None
dbcon = AvalonMongoDB()
dbcon.install()
dbcon.Session["AVALON_PROJECT"] = None
self.dbcon = dbcon
self.show_projects = show_projects
self.show_libraries = show_libraries
# Groups config
self.groups_config = tools_lib.GroupsConfig(self.dbcon)
self.family_config_cache = tools_lib.FamilyConfigCache(self.dbcon)
self.groups_config = tools_lib.GroupsConfig(dbcon)
self.family_config_cache = tools_lib.FamilyConfigCache(dbcon)
assets = AssetWidget(
self.dbcon, multiselection=True, parent=self
# UI initialization
main_splitter = QtWidgets.QSplitter(self)
# --- Left part ---
left_side_splitter = QtWidgets.QSplitter(main_splitter)
left_side_splitter.setOrientation(QtCore.Qt.Vertical)
# Project combobox
projects_combobox = QtWidgets.QComboBox(left_side_splitter)
combobox_delegate = QtWidgets.QStyledItemDelegate(self)
projects_combobox.setItemDelegate(combobox_delegate)
# Assets widget
assets_widget = AssetWidget(
dbcon, multiselection=True, parent=left_side_splitter
)
families = FamilyListView(
self.dbcon, self.family_config_cache, parent=self
# Families widget
families_filter_view = FamilyListView(
dbcon, self.family_config_cache, left_side_splitter
)
subsets = LibrarySubsetWidget(
self.dbcon,
left_side_splitter.addWidget(projects_combobox)
left_side_splitter.addWidget(assets_widget)
left_side_splitter.addWidget(families_filter_view)
left_side_splitter.setStretchFactor(1, 65)
left_side_splitter.setStretchFactor(2, 35)
# --- Middle part ---
# Subsets widget
subsets_widget = LibrarySubsetWidget(
dbcon,
self.groups_config,
self.family_config_cache,
tool_name=self.tool_name,
parent=self
)
version = VersionWidget(self.dbcon)
thumbnail = ThumbnailWidget(self.dbcon)
# Project
self.combo_projects = QtWidgets.QComboBox()
# Create splitter to show / hide family filters
asset_filter_splitter = QtWidgets.QSplitter()
asset_filter_splitter.setOrientation(QtCore.Qt.Vertical)
asset_filter_splitter.addWidget(self.combo_projects)
asset_filter_splitter.addWidget(assets)
asset_filter_splitter.addWidget(families)
asset_filter_splitter.setStretchFactor(1, 65)
asset_filter_splitter.setStretchFactor(2, 35)
manager = ModulesManager()
sync_server = manager.modules_by_name["sync_server"]
representations = RepresentationWidget(self.dbcon)
thumb_ver_splitter = QtWidgets.QSplitter()
# --- Right part ---
thumb_ver_splitter = QtWidgets.QSplitter(main_splitter)
thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical)
thumb_ver_splitter.addWidget(thumbnail)
thumb_ver_splitter.addWidget(version)
if sync_server.enabled:
thumb_ver_splitter.addWidget(representations)
thumbnail_widget = ThumbnailWidget(dbcon, parent=thumb_ver_splitter)
version_info_widget = VersionWidget(dbcon, parent=thumb_ver_splitter)
thumb_ver_splitter.addWidget(thumbnail_widget)
thumb_ver_splitter.addWidget(version_info_widget)
thumb_ver_splitter.setStretchFactor(0, 30)
thumb_ver_splitter.setStretchFactor(1, 35)
container_layout = QtWidgets.QHBoxLayout(container)
container_layout.setContentsMargins(0, 0, 0, 0)
split = QtWidgets.QSplitter()
split.addWidget(asset_filter_splitter)
split.addWidget(subsets)
split.addWidget(thumb_ver_splitter)
split.setSizes([180, 950, 200])
container_layout.addWidget(split)
manager = ModulesManager()
sync_server = manager.modules_by_name.get("sync_server")
sync_server_enabled = False
if sync_server is not None:
sync_server_enabled = sync_server.enabled
body_layout = QtWidgets.QHBoxLayout(body)
body_layout.addWidget(container)
body_layout.setContentsMargins(0, 0, 0, 0)
repres_widget = None
if sync_server_enabled:
repres_widget = RepresentationWidget(
dbcon, self.tool_name, parent=thumb_ver_splitter
)
thumb_ver_splitter.addWidget(repres_widget)
message = QtWidgets.QLabel()
message.hide()
main_splitter.addWidget(left_side_splitter)
main_splitter.addWidget(subsets_widget)
main_splitter.addWidget(thumb_ver_splitter)
if sync_server_enabled:
main_splitter.setSizes([250, 1000, 550])
else:
main_splitter.setSizes([250, 850, 200])
footer_layout = QtWidgets.QVBoxLayout(footer)
footer_layout.addWidget(message)
# --- Footer ---
footer_widget = QtWidgets.QWidget(self)
footer_widget.setFixedHeight(20)
message_label = QtWidgets.QLabel(footer_widget)
footer_layout = QtWidgets.QVBoxLayout(footer_widget)
footer_layout.setContentsMargins(0, 0, 0, 0)
footer_layout.addWidget(message_label)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(body)
layout.addWidget(footer)
layout.addWidget(main_splitter)
layout.addWidget(footer_widget)
self.data = {
"widgets": {
"families": families,
"assets": assets,
"subsets": subsets,
"version": version,
"thumbnail": thumbnail,
"representations": representations
},
"label": {
"message": message,
},
"state": {
"assetIds": None
}
}
families.active_changed.connect(subsets.set_family_filters)
assets.selection_changed.connect(self.on_assetschanged)
assets.refresh_triggered.connect(self.on_assetschanged)
assets.view.clicked.connect(self.on_assetview_click)
subsets.active_changed.connect(self.on_subsetschanged)
subsets.version_changed.connect(self.on_versionschanged)
subsets.refreshed.connect(self._on_subset_refresh)
self.combo_projects.currentTextChanged.connect(self.on_project_change)
message_timer = QtCore.QTimer()
message_timer.setInterval(self.message_timeout)
message_timer.setSingleShot(True)
message_timer.timeout.connect(self._on_message_timeout)
families_filter_view.active_changed.connect(
self._on_family_filter_change
)
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)
projects_combobox.currentTextChanged.connect(self.on_project_change)
self.sync_server = sync_server
self._sync_server_enabled = sync_server_enabled
# Set default thumbnail on start
thumbnail.set_thumbnail(None)
self._combobox_delegate = combobox_delegate
self._projects_combobox = projects_combobox
self._assets_widget = assets_widget
self._families_filter_view = families_filter_view
# Defaults
if sync_server.enabled:
split.setSizes([250, 1000, 550])
self.resize(1800, 900)
else:
split.setSizes([250, 850, 200])
self.resize(1300, 700)
self._subsets_widget = subsets_widget
self._version_info_widget = version_info_widget
self._thumbnail_widget = thumbnail_widget
self._repres_widget = repres_widget
self._message_label = message_label
self._message_timer = message_timer
def showEvent(self, event):
super(LibraryLoaderWindow, self).showEvent(event)
if self._first_show:
self._first_show = False
self.setStyleSheet(style.load_stylesheet())
if self._sync_server_enabled:
self.resize(1800, 900)
else:
self.resize(1300, 700)
if not self._initial_refresh:
self._initial_refresh = True
self.refresh()
def on_assetview_click(self, *args):
subsets_widget = self.data["widgets"]["subsets"]
selection_model = subsets_widget.view.selectionModel()
selection_model = self._subsets_widget.view.selectionModel()
if selection_model.selectedIndexes():
selection_model.clearSelection()
@ -187,7 +214,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
self._ignore_project_change = True
# Cleanup
self.combo_projects.clear()
self._projects_combobox.clear()
# Fill combobox with projects
select_project_item = QtGui.QStandardItem("< Select project >")
@ -202,18 +229,18 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
item.setData(project_name, QtCore.Qt.UserRole + 1)
combobox_items.append(item)
root_item = self.combo_projects.model().invisibleRootItem()
root_item = self._projects_combobox.model().invisibleRootItem()
root_item.appendRows(combobox_items)
index = 0
self._ignore_project_change = False
if old_project_name:
index = self.combo_projects.findText(
index = self._projects_combobox.findText(
old_project_name, QtCore.Qt.MatchFixedString
)
self.combo_projects.setCurrentIndex(index)
self._projects_combobox.setCurrentIndex(index)
def get_filtered_projects(self):
projects = list()
@ -231,8 +258,8 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
if self._ignore_project_change:
return
row = self.combo_projects.currentIndex()
index = self.combo_projects.model().index(row, 0)
row = self._projects_combobox.currentIndex()
index = self._projects_combobox.model().index(row, 0)
project_name = index.data(QtCore.Qt.UserRole + 1)
self.dbcon.Session["AVALON_PROJECT"] = project_name
@ -245,11 +272,9 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
"Config `%s` has no function `install`" % _config.__name__
)
subsets = self.data["widgets"]["subsets"]
representations = self.data["widgets"]["representations"]
subsets.on_project_change(self.dbcon.Session["AVALON_PROJECT"])
representations.on_project_change(self.dbcon.Session["AVALON_PROJECT"])
self._subsets_widget.on_project_change(project_name)
if self._repres_widget:
self._repres_widget.on_project_change(project_name)
self.family_config_cache.refresh()
self.groups_config.refresh()
@ -263,13 +288,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
@property
def current_project(self):
if (
not self.dbcon.active_project() or
self.dbcon.active_project() == ""
):
return None
return self.dbcon.active_project()
return self.dbcon.active_project() or None
# -------------------------------
# Delay calling blocking methods
@ -292,12 +311,11 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
tools_lib.schedule(self._versionschanged, 150, channel="mongo")
def _on_subset_refresh(self, has_item):
subsets_widget = self.data["widgets"]["subsets"]
families_view = self.data["widgets"]["families"]
subsets_widget.set_loading_state(loading=False, empty=not has_item)
families = subsets_widget.get_subsets_families()
families_view.set_enabled_families(families)
self._subsets_widget.set_loading_state(
loading=False, empty=not has_item
)
families = self._subsets_widget.get_subsets_families()
self._families_filter_view.set_enabled_families(families)
def set_context(self, context, refresh=True):
self.echo("Setting context: {}".format(context))
@ -307,6 +325,9 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
)
# ------------------------------
def _on_family_filter_change(self, families):
self._subsets_widget.set_family_filters(families)
def _refresh(self):
if not self._initial_refresh:
self._initial_refresh = True
@ -322,74 +343,69 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
)
assert project_doc, "This is a bug"
assets_widget = self.data["widgets"]["assets"]
families_view = self.data["widgets"]["families"]
families_view.set_enabled_families(set())
families_view.refresh()
self._families_filter_view.set_enabled_families(set())
self._families_filter_view.refresh()
assets_widget.model.stop_fetch_thread()
assets_widget.refresh()
assets_widget.setFocus()
self._assets_widget.model.stop_fetch_thread()
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_widget = self.data["widgets"]["assets"]
id_role = assets_widget.model.ObjectIdRole
assets_model = self._assets_widget.model
id_role = assets_model.ObjectIdRole
for index in tools_lib.iter_model_rows(assets_widget.model, 0):
for index in tools_lib.iter_model_rows(assets_model, 0):
if index.data(id_role) not in last_asset_ids:
continue
assets_widget.model.setData(
index, [], assets_widget.model.subsetColorsRole
assets_model.setData(
index, [], assets_model.subsetColorsRole
)
def _assetschanged(self):
"""Selected assets have changed"""
assets_widget = self.data["widgets"]["assets"]
subsets_widget = self.data["widgets"]["subsets"]
subsets_model = subsets_widget.model
subsets_model = self._subsets_widget.model
subsets_model.clear()
self.clear_assets_underlines()
if not self.dbcon.Session.get("AVALON_PROJECT"):
subsets_widget.set_loading_state(
self._subsets_widget.set_loading_state(
loading=False,
empty=True
)
return
# filter None docs they are silo
asset_docs = assets_widget.get_selected_assets()
asset_docs = self._assets_widget.get_selected_assets()
if len(asset_docs) == 0:
return
asset_ids = [asset_doc["_id"] for asset_doc in asset_docs]
# Start loading
subsets_widget.set_loading_state(
self._subsets_widget.set_loading_state(
loading=bool(asset_ids),
empty=True
)
subsets_model.set_assets(asset_ids)
subsets_widget.view.setColumnHidden(
self._subsets_widget.view.setColumnHidden(
subsets_model.Columns.index("asset"),
len(asset_ids) < 2
)
# Clear the version information on asset change
self.data["widgets"]["version"].set_version(None)
self.data["widgets"]["thumbnail"].set_thumbnail(asset_docs)
self._version_info_widget.set_version(None)
self._thumbnail_widget.set_thumbnail(asset_docs)
self.data["state"]["assetIds"] = asset_ids
representations = self.data["widgets"]["representations"]
# reset repre list
representations.set_version_ids([])
self._repres_widget.set_version_ids([])
def _subsetschanged(self):
asset_ids = self.data["state"]["assetIds"]
@ -398,8 +414,9 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
self._versionschanged()
return
subsets = self.data["widgets"]["subsets"]
selected_subsets = subsets.selected_subsets(_merged=True, _other=False)
selected_subsets = self._subsets_widget.selected_subsets(
_merged=True, _other=False
)
asset_models = {}
asset_ids = []
@ -420,26 +437,24 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
self.clear_assets_underlines()
assets_widget = self.data["widgets"]["assets"]
indexes = assets_widget.view.selectionModel().selectedRows()
indexes = self._assets_widget.view.selectionModel().selectedRows()
assets_model = self._assets_widget.model
for index in indexes:
id = index.data(assets_widget.model.ObjectIdRole)
id = index.data(assets_model.ObjectIdRole)
if id not in asset_models:
continue
assets_widget.model.setData(
index, asset_models[id], assets_widget.model.subsetColorsRole
assets_model.setData(
index, asset_models[id], assets_model.subsetColorsRole
)
# Trigger repaint
assets_widget.view.updateGeometries()
self._assets_widget.view.updateGeometries()
# Set version in Version Widget
self._versionschanged()
def _versionschanged(self):
subsets = self.data["widgets"]["subsets"]
selection = subsets.view.selectionModel()
selection = self._subsets_widget.view.selectionModel()
# Active must be in the selected rows otherwise we
# assume it's not actually an "active" current index.
@ -448,7 +463,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
active = selection.currentIndex()
rows = selection.selectedRows(column=active.column())
if active and active in rows:
item = active.data(subsets.model.ItemRole)
item = active.data(self._subsets_widget.model.ItemRole)
if (
item is not None
and not (item.get("isGroup") or item.get("isMerged"))
@ -460,7 +475,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
for index in rows:
if not index or not index.isValid():
continue
item = index.data(subsets.model.ItemRole)
item = index.data(self._subsets_widget.model.ItemRole)
if (
item is None
or item.get("isGroup")
@ -469,20 +484,18 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
continue
version_docs.append(item["version_document"])
self.data["widgets"]["version"].set_version(version_doc)
self._version_info_widget.set_version(version_doc)
thumbnail_docs = version_docs
if not thumbnail_docs:
assets_widget = self.data["widgets"]["assets"]
asset_docs = assets_widget.get_selected_assets()
asset_docs = self._assets_widget.get_selected_assets()
if len(asset_docs) > 0:
thumbnail_docs = asset_docs
self.data["widgets"]["thumbnail"].set_thumbnail(thumbnail_docs)
self._thumbnail_widget.set_thumbnail(thumbnail_docs)
representations = self.data["widgets"]["representations"]
version_ids = [doc["_id"] for doc in version_docs or []]
representations.set_version_ids(version_ids)
self._repres_widget.set_version_ids(version_ids)
def _set_context(self, context, refresh=True):
"""Set the selection in the interface using a context.
@ -510,16 +523,15 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
# scheduled refresh and the silo tabs are not shown.
self._refresh_assets()
asset_widget = self.data["widgets"]["assets"]
asset_widget.select_assets(asset)
self._assets_widget.select_assets(asset)
def _on_message_timeout(self):
self._message_label.setText("")
def echo(self, message):
widget = self.data["label"]["message"]
widget.setText(str(message))
widget.show()
self._message_label.setText(str(message))
print(message)
tools_lib.schedule(widget.hide, 5000, channel="message")
self._message_timer.start()
def closeEvent(self, event):
# Kill on holding SHIFT
@ -576,7 +588,6 @@ def show(
window = LibraryLoaderWindow(
parent, icon, show_projects, show_libraries
)
window.setStyleSheet(style.load_stylesheet())
window.show()
module.window = window

View file

@ -1,10 +1,10 @@
import sys
from Qt import QtWidgets, QtCore
from avalon import api, io, style, pipeline
from avalon import api, io, pipeline
from openpype import style
from openpype.tools.utils.widgets import AssetWidget
from openpype.tools.utils import lib
from .widgets import (
@ -37,6 +37,7 @@ class LoaderWindow(QtWidgets.QDialog):
"""Asset loader interface"""
tool_name = "loader"
message_timeout = 5000
def __init__(self, parent=None):
super(LoaderWindow, self).__init__(parent)
@ -57,83 +58,85 @@ class LoaderWindow(QtWidgets.QDialog):
self.setWindowFlags(window_flags)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
body = QtWidgets.QWidget()
footer = QtWidgets.QWidget()
footer.setFixedHeight(20)
main_splitter = QtWidgets.QSplitter(self)
container = QtWidgets.QWidget()
# --- Left part ---
left_side_splitter = QtWidgets.QSplitter(main_splitter)
left_side_splitter.setOrientation(QtCore.Qt.Vertical)
assets = AssetWidget(io, multiselection=True, parent=self)
assets.set_current_asset_btn_visibility(True)
# Assets widget
assets_widget = AssetWidget(
io, multiselection=True, parent=left_side_splitter
)
assets_widget.set_current_asset_btn_visibility(True)
families = FamilyListView(io, self.family_config_cache, self)
subsets = SubsetWidget(
# Families widget
families_filter_view = FamilyListView(
io, self.family_config_cache, left_side_splitter
)
left_side_splitter.addWidget(assets_widget)
left_side_splitter.addWidget(families_filter_view)
left_side_splitter.setStretchFactor(0, 65)
left_side_splitter.setStretchFactor(1, 35)
# --- Middle part ---
# Subsets widget
subsets_widget = SubsetWidget(
io,
self.groups_config,
self.family_config_cache,
tool_name=self.tool_name,
parent=self
parent=main_splitter
)
version = VersionWidget(io)
thumbnail = ThumbnailWidget(io)
representations = RepresentationWidget(io, self.tool_name)
manager = ModulesManager()
sync_server = manager.modules_by_name["sync_server"]
thumb_ver_splitter = QtWidgets.QSplitter()
# --- Right part ---
thumb_ver_splitter = QtWidgets.QSplitter(main_splitter)
thumb_ver_splitter.setOrientation(QtCore.Qt.Vertical)
thumb_ver_splitter.addWidget(thumbnail)
thumb_ver_splitter.addWidget(version)
if sync_server.enabled:
thumb_ver_splitter.addWidget(representations)
thumbnail_widget = ThumbnailWidget(io, parent=thumb_ver_splitter)
version_info_widget = VersionWidget(io, parent=thumb_ver_splitter)
thumb_ver_splitter.addWidget(thumbnail_widget)
thumb_ver_splitter.addWidget(version_info_widget)
thumb_ver_splitter.setStretchFactor(0, 30)
thumb_ver_splitter.setStretchFactor(1, 35)
# Create splitter to show / hide family filters
asset_filter_splitter = QtWidgets.QSplitter()
asset_filter_splitter.setOrientation(QtCore.Qt.Vertical)
asset_filter_splitter.addWidget(assets)
asset_filter_splitter.addWidget(families)
asset_filter_splitter.setStretchFactor(0, 65)
asset_filter_splitter.setStretchFactor(1, 35)
manager = ModulesManager()
sync_server = manager.modules_by_name.get("sync_server")
sync_server_enabled = False
if sync_server is not None:
sync_server_enabled = sync_server.enabled
container_layout = QtWidgets.QHBoxLayout(container)
container_layout.setContentsMargins(0, 0, 0, 0)
split = QtWidgets.QSplitter()
split.addWidget(asset_filter_splitter)
split.addWidget(subsets)
split.addWidget(thumb_ver_splitter)
repres_widget = None
if sync_server_enabled:
repres_widget = RepresentationWidget(
io, self.tool_name, parent=thumb_ver_splitter
)
thumb_ver_splitter.addWidget(repres_widget)
container_layout.addWidget(split)
main_splitter.addWidget(left_side_splitter)
main_splitter.addWidget(subsets_widget)
main_splitter.addWidget(thumb_ver_splitter)
body_layout = QtWidgets.QHBoxLayout(body)
body_layout.addWidget(container)
body_layout.setContentsMargins(0, 0, 0, 0)
if sync_server_enabled:
main_splitter.setSizes([250, 1000, 550])
else:
main_splitter.setSizes([250, 850, 200])
message = QtWidgets.QLabel()
message.hide()
footer_widget = QtWidgets.QWidget(self)
footer_layout = QtWidgets.QVBoxLayout(footer)
footer_layout.addWidget(message)
message_label = QtWidgets.QLabel(footer_widget)
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
footer_layout.setContentsMargins(0, 0, 0, 0)
footer_layout.addWidget(message_label, 1)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(body)
layout.addWidget(footer)
layout.addWidget(main_splitter, 1)
layout.addWidget(footer_widget, 0)
self.data = {
"widgets": {
"families": families,
"assets": assets,
"subsets": subsets,
"version": version,
"thumbnail": thumbnail,
"representations": representations
},
"label": {
"message": message,
},
"state": {
"assetIds": None
}
@ -142,19 +145,44 @@ class LoaderWindow(QtWidgets.QDialog):
overlay_frame = OverlayFrame("Loading...", self)
overlay_frame.setVisible(False)
families.active_changed.connect(subsets.set_family_filters)
assets.selection_changed.connect(self.on_assetschanged)
assets.refresh_triggered.connect(self.on_assetschanged)
assets.view.clicked.connect(self.on_assetview_click)
subsets.active_changed.connect(self.on_subsetschanged)
subsets.version_changed.connect(self.on_versionschanged)
subsets.refreshed.connect(self._on_subset_refresh)
message_timer = QtCore.QTimer()
message_timer.setInterval(self.message_timeout)
message_timer.setSingleShot(True)
subsets.load_started.connect(self._on_load_start)
subsets.load_ended.connect(self._on_load_end)
representations.load_started.connect(self._on_load_start)
representations.load_ended.connect(self._on_load_end)
message_timer.timeout.connect(self._on_message_timeout)
families_filter_view.active_changed.connect(
self._on_family_filter_change
)
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)
subsets_widget.load_started.connect(self._on_load_start)
subsets_widget.load_ended.connect(self._on_load_end)
if repres_widget:
repres_widget.load_started.connect(self._on_load_start)
repres_widget.load_ended.connect(self._on_load_end)
self._sync_server_enabled = sync_server_enabled
self._assets_widget = assets_widget
self._families_filter_view = families_filter_view
self._subsets_widget = subsets_widget
self._version_info_widget = version_info_widget
self._thumbnail_widget = thumbnail_widget
self._repres_widget = repres_widget
self._message_label = message_label
self._message_timer = message_timer
# TODO add overlay using stack widget
self._overlay_frame = overlay_frame
self.family_config_cache.refresh()
@ -163,13 +191,7 @@ class LoaderWindow(QtWidgets.QDialog):
self._refresh()
self._assetschanged()
# Defaults
if sync_server.enabled:
split.setSizes([250, 1000, 550])
self.resize(1800, 900)
else:
split.setSizes([250, 850, 200])
self.resize(1300, 700)
self._first_show = True
def resizeEvent(self, event):
super(LoaderWindow, self).resizeEvent(event)
@ -179,13 +201,23 @@ class LoaderWindow(QtWidgets.QDialog):
super(LoaderWindow, self).moveEvent(event)
self._overlay_frame.move(0, 0)
def showEvent(self, event):
super(LoaderWindow, self).showEvent(event)
if self._first_show:
self._first_show = False
self.setStyleSheet(style.load_stylesheet())
if self._sync_server_enabled:
self.resize(1800, 900)
else:
self.resize(1300, 700)
# -------------------------------
# Delay calling blocking methods
# -------------------------------
def on_assetview_click(self, *args):
subsets_widget = self.data["widgets"]["subsets"]
selection_model = subsets_widget.view.selectionModel()
# TODO do not touch inner attributes of subset widget
selection_model = self._subsets_widget.view.selectionModel()
if selection_model.selectedIndexes():
selection_model.clearSelection()
@ -219,12 +251,11 @@ class LoaderWindow(QtWidgets.QDialog):
self._overlay_frame.setVisible(False)
def _on_subset_refresh(self, has_item):
subsets_widget = self.data["widgets"]["subsets"]
families_view = self.data["widgets"]["families"]
subsets_widget.set_loading_state(loading=False, empty=not has_item)
families = subsets_widget.get_subsets_families()
families_view.set_enabled_families(families)
self._subsets_widget.set_loading_state(
loading=False, empty=not has_item
)
families = self._subsets_widget.get_subsets_families()
self._families_filter_view.set_enabled_families(families)
def _on_load_end(self):
# Delay hiding as click events happened during loading should be
@ -232,14 +263,14 @@ class LoaderWindow(QtWidgets.QDialog):
QtCore.QTimer.singleShot(100, self._hide_overlay)
# ------------------------------
def _on_family_filter_change(self, families):
self._subsets_widget.set_family_filters(families)
def on_context_task_change(self, *args, **kwargs):
assets_widget = self.data["widgets"]["assets"]
families_view = self.data["widgets"]["families"]
# Refresh families config
families_view.refresh()
self._families_filter_view.refresh()
# Change to context asset on context change
assets_widget.select_assets(io.Session["AVALON_ASSET"])
self._assets_widget.select_assets(io.Session["AVALON_ASSET"])
def _refresh(self):
"""Load assets from database"""
@ -248,12 +279,10 @@ class LoaderWindow(QtWidgets.QDialog):
project = io.find_one({"type": "project"}, {"type": 1})
assert project, "Project was not found! This is a bug"
assets_widget = self.data["widgets"]["assets"]
assets_widget.refresh()
assets_widget.setFocus()
self._assets_widget.refresh()
self._assets_widget.setFocus()
families_view = self.data["widgets"]["families"]
families_view.refresh()
self._families_filter_view.refresh()
def clear_assets_underlines(self):
"""Clear colors from asset data to remove colored underlines
@ -261,11 +290,12 @@ class LoaderWindow(QtWidgets.QDialog):
own selected subsets. These colors must be cleared from asset data
on selection change so they match current selection.
"""
last_asset_ids = self.data["state"]["assetIds"]
# 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.data["widgets"]["assets"]
assets_widget = self._assets_widget
id_role = assets_widget.model.ObjectIdRole
for index in lib.iter_model_rows(assets_widget.model, 0):
@ -278,15 +308,15 @@ class LoaderWindow(QtWidgets.QDialog):
def _assetschanged(self):
"""Selected assets have changed"""
assets_widget = self.data["widgets"]["assets"]
subsets_widget = self.data["widgets"]["subsets"]
subsets_widget = self._subsets_widget
# TODO do not touch subset widget inner attributes
subsets_model = subsets_widget.model
subsets_model.clear()
self.clear_assets_underlines()
# filter None docs they are silo
asset_docs = assets_widget.get_selected_assets()
asset_docs = self._assets_widget.get_selected_assets()
asset_ids = [asset_doc["_id"] for asset_doc in asset_docs]
# Start loading
@ -302,14 +332,14 @@ class LoaderWindow(QtWidgets.QDialog):
)
# Clear the version information on asset change
self.data["widgets"]["version"].set_version(None)
self.data["widgets"]["thumbnail"].set_thumbnail(asset_docs)
self._thumbnail_widget.set_thumbnail(asset_docs)
self._version_info_widget.set_version(None)
self.data["state"]["assetIds"] = asset_ids
representations = self.data["widgets"]["representations"]
# reset repre list
representations.set_version_ids([])
if self._repres_widget is not None:
self._repres_widget.set_version_ids([])
def _subsetschanged(self):
asset_ids = self.data["state"]["assetIds"]
@ -318,8 +348,9 @@ class LoaderWindow(QtWidgets.QDialog):
self._versionschanged()
return
subsets = self.data["widgets"]["subsets"]
selected_subsets = subsets.selected_subsets(_merged=True, _other=False)
selected_subsets = self._subsets_widget.selected_subsets(
_merged=True, _other=False
)
asset_models = {}
asset_ids = []
@ -340,7 +371,8 @@ class LoaderWindow(QtWidgets.QDialog):
self.clear_assets_underlines()
assets_widget = self.data["widgets"]["assets"]
# TODO do not use inner attributes of asset widget
assets_widget = self._assets_widget
indexes = assets_widget.view.selectionModel().selectedRows()
for index in indexes:
@ -357,7 +389,7 @@ class LoaderWindow(QtWidgets.QDialog):
self._versionschanged()
def _versionschanged(self):
subsets = self.data["widgets"]["subsets"]
subsets = self._subsets_widget
selection = subsets.view.selectionModel()
# Active must be in the selected rows otherwise we
@ -389,23 +421,24 @@ class LoaderWindow(QtWidgets.QDialog):
else:
version_docs.append(item["version_document"])
self.data["widgets"]["version"].set_version(version_doc)
self._version_info_widget.set_version(version_doc)
thumbnail_docs = version_docs
assets_widget = self.data["widgets"]["assets"]
asset_docs = assets_widget.get_selected_assets()
asset_docs = self._assets_widget.get_selected_assets()
if not thumbnail_docs:
if len(asset_docs) > 0:
thumbnail_docs = asset_docs
self.data["widgets"]["thumbnail"].set_thumbnail(thumbnail_docs)
self._thumbnail_widget.set_thumbnail(thumbnail_docs)
representations = self.data["widgets"]["representations"]
version_ids = [doc["_id"] for doc in version_docs or []]
representations.set_version_ids(version_ids)
if self._repres_widget is not None:
version_ids = [doc["_id"] for doc in version_docs or []]
self._repres_widget.set_version_ids(version_ids)
# representations.change_visibility("subset", len(rows) > 1)
# representations.change_visibility("asset", len(asset_docs) > 1)
# self._repres_widget.change_visibility("subset", len(rows) > 1)
# self._repres_widget.change_visibility(
# "asset", len(asset_docs) > 1
# )
def _set_context(self, context, refresh=True):
"""Set the selection in the interface using a context.
@ -438,16 +471,15 @@ class LoaderWindow(QtWidgets.QDialog):
# scheduled refresh and the silo tabs are not shown.
self._refresh()
asset_widget = self.data["widgets"]["assets"]
asset_widget.select_assets(asset)
self._assets_widget.select_assets(asset)
def _on_message_timeout(self):
self._message_label.setText("")
def echo(self, message):
widget = self.data["label"]["message"]
widget.setText(str(message))
widget.show()
self._message_label.setText(str(message))
print(message)
lib.schedule(widget.hide, 5000, channel="message")
self._message_timer.start()
def closeEvent(self, event):
# Kill on holding SHIFT
@ -475,7 +507,7 @@ class LoaderWindow(QtWidgets.QDialog):
event.setAccepted(True) # Avoid interfering other widgets
def show_grouping_dialog(self):
subsets = self.data["widgets"]["subsets"]
subsets = self._subsets_widget
if not subsets.is_groupable():
self.echo("Grouping not enabled.")
return
@ -514,7 +546,8 @@ class SubsetGroupingDialog(QtWidgets.QDialog):
self.items = items
self.groups_config = groups_config
self.subsets = parent.data["widgets"]["subsets"]
# TODO do not touch inner attributes
self.subsets = parent._subsets_widget
self.asset_ids = parent.data["state"]["assetIds"]
name = QtWidgets.QLineEdit()
@ -633,7 +666,6 @@ def show(debug=False, parent=None, use_context=False):
with lib.application():
window = LoaderWindow(parent)
window.setStyleSheet(style.load_stylesheet())
window.show()
if use_context:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5 KiB

Before After
Before After

View file

@ -37,12 +37,13 @@ class OverlayFrame(QtWidgets.QFrame):
super(OverlayFrame, self).__init__(parent)
label_widget = QtWidgets.QLabel(label, self)
label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter)
self.label_widget = label_widget
label_widget.setStyleSheet("background: transparent;")
self.setStyleSheet((
"background: rgba(0, 0, 0, 127);"
"font-size: 60pt;"
@ -159,36 +160,40 @@ class SubsetWidget(QtWidgets.QWidget):
grouping=enable_grouping
)
proxy = SubsetFilterProxyModel()
proxy.setSourceModel(model)
proxy.setDynamicSortFilter(True)
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
family_proxy = FamiliesFilterProxyModel()
family_proxy.setSourceModel(proxy)
subset_filter = QtWidgets.QLineEdit()
subset_filter = QtWidgets.QLineEdit(self)
subset_filter.setPlaceholderText("Filter subsets..")
groupable = QtWidgets.QCheckBox("Enable Grouping")
groupable.setChecked(enable_grouping)
group_checkbox = QtWidgets.QCheckBox("Enable Grouping", self)
group_checkbox.setChecked(enable_grouping)
top_bar_layout = QtWidgets.QHBoxLayout()
top_bar_layout.addWidget(subset_filter)
top_bar_layout.addWidget(groupable)
top_bar_layout.addWidget(group_checkbox)
view = TreeViewSpinner()
view = TreeViewSpinner(self)
view.setModel(family_proxy)
view.setObjectName("SubsetView")
view.setIndentation(20)
view.setStyleSheet("""
QTreeView::item{
padding: 5px 1px;
border: 0px;
}
""")
view.setAllColumnsShowFocus(True)
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
view.setSortingEnabled(True)
view.sortByColumn(1, QtCore.Qt.AscendingOrder)
view.setAlternatingRowColors(True)
# Set view delegates
version_delegate = VersionDelegate(self.dbcon)
version_delegate = VersionDelegate(self.dbcon, view)
column = model.Columns.index("version")
view.setItemDelegateForColumn(column, version_delegate)
time_delegate = PrettyTimeDelegate()
time_delegate = PrettyTimeDelegate(view)
column = model.Columns.index("time")
view.setItemDelegateForColumn(column, time_delegate)
@ -197,54 +202,39 @@ class SubsetWidget(QtWidgets.QWidget):
layout.addLayout(top_bar_layout)
layout.addWidget(view)
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
view.setSortingEnabled(True)
view.sortByColumn(1, QtCore.Qt.AscendingOrder)
view.setAlternatingRowColors(True)
self.data = {
"delegates": {
"version": version_delegate,
"time": time_delegate
},
"state": {
"groupable": groupable
}
}
self.proxy = proxy
self.model = model
self.view = view
self.filter = subset_filter
self.family_proxy = family_proxy
# settings and connections
self.proxy.setSourceModel(self.model)
self.proxy.setDynamicSortFilter(True)
self.proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.view.setModel(self.family_proxy)
self.view.customContextMenuRequested.connect(self.on_context_menu)
for column_name, width in self.default_widths:
idx = model.Columns.index(column_name)
view.setColumnWidth(idx, width)
self.model = model
self.view = view
actual_project = dbcon.Session["AVALON_PROJECT"]
self.on_project_change(actual_project)
view.customContextMenuRequested.connect(self.on_context_menu)
selection = view.selectionModel()
selection.selectionChanged.connect(self.active_changed)
version_delegate.version_changed.connect(self.version_changed)
groupable.stateChanged.connect(self.set_grouping)
group_checkbox.stateChanged.connect(self.set_grouping)
self.filter.textChanged.connect(self.proxy.setFilterRegExp)
self.filter.textChanged.connect(self.view.expandAll)
subset_filter.textChanged.connect(proxy.setFilterRegExp)
subset_filter.textChanged.connect(view.expandAll)
model.refreshed.connect(self.refreshed)
self.proxy = proxy
self.family_proxy = family_proxy
self._subset_filter = subset_filter
self._group_checkbox = group_checkbox
self._version_delegate = version_delegate
self._time_delegate = time_delegate
self.model.refresh()
def get_subsets_families(self):
@ -254,7 +244,7 @@ class SubsetWidget(QtWidgets.QWidget):
self.family_proxy.setFamiliesFilter(families)
def is_groupable(self):
return self.data["state"]["groupable"].checkState()
return self._group_checkbox.isChecked()
def set_grouping(self, state):
with tools_lib.preserve_selection(tree_view=self.view,
@ -755,6 +745,7 @@ class ThumbnailWidget(QtWidgets.QLabel):
"default_thumbnail.png"
)
self.default_pix = QtGui.QPixmap(default_pix_path)
self.set_pixmap()
def height(self):
width = self.width()
@ -1131,7 +1122,8 @@ class RepresentationWidget(QtWidgets.QWidget):
label = QtWidgets.QLabel("Representations", self)
tree_view = DeselectableTreeView()
tree_view = DeselectableTreeView(parent=self)
tree_view.setObjectName("RepresentationView")
tree_view.setModel(proxy_model)
tree_view.setAllColumnsShowFocus(True)
tree_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
@ -1141,12 +1133,6 @@ class RepresentationWidget(QtWidgets.QWidget):
tree_view.sortByColumn(1, QtCore.Qt.AscendingOrder)
tree_view.setAlternatingRowColors(True)
tree_view.setIndentation(20)
tree_view.setStyleSheet("""
QTreeView::item{
padding: 5px 1px;
border: 0px;
}
""")
tree_view.collapseAll()
for column_name, width in self.default_widths:

View file

@ -2,20 +2,27 @@ import sys
import time
import logging
from Qt import QtWidgets, QtCore
from openpype.hosts.maya.api.lib import assign_look_by_version
from avalon import style, io
from avalon.tools import lib
from avalon.vendor.Qt import QtWidgets, QtCore
from maya import cmds
# old api for MFileIO
import maya.OpenMaya
import maya.api.OpenMaya as om
from . import widgets
from . import commands
from . vray_proxies import vrayproxy_assign_look
from .widgets import (
AssetOutliner,
LookOutliner
)
from .commands import (
get_workfile,
remove_unused_looks
)
from .vray_proxies import vrayproxy_assign_look
module = sys.modules[__name__]
@ -32,7 +39,7 @@ class App(QtWidgets.QWidget):
# Store callback references
self._callbacks = []
filename = commands.get_workfile()
filename = get_workfile()
self.setObjectName("lookManager")
self.setWindowTitle("Look Manager 1.3.0 - [{}]".format(filename))
@ -57,13 +64,13 @@ class App(QtWidgets.QWidget):
"""Build the UI"""
# Assets (left)
asset_outliner = widgets.AssetOutliner()
asset_outliner = AssetOutliner()
# Looks (right)
looks_widget = QtWidgets.QWidget()
looks_layout = QtWidgets.QVBoxLayout(looks_widget)
look_outliner = widgets.LookOutliner() # Database look overview
look_outliner = LookOutliner() # Database look overview
assign_selected = QtWidgets.QCheckBox("Assign to selected only")
assign_selected.setToolTip("Whether to assign only to selected nodes "
@ -124,7 +131,7 @@ class App(QtWidgets.QWidget):
lambda: self.echo("Loaded assets.."))
self.look_outliner.menu_apply_action.connect(self.on_process_selected)
self.remove_unused.clicked.connect(commands.remove_unused_looks)
self.remove_unused.clicked.connect(remove_unused_looks)
# Maya renderlayer switch callback
callback = om.MEventMessage.addEventCallback(

View file

@ -9,7 +9,7 @@ from openpype.hosts.maya.api import lib
from avalon import io, api
import vray_proxies
from .vray_proxies import get_alembic_ids_cache
log = logging.getLogger(__name__)
@ -146,7 +146,7 @@ def create_items_from_nodes(nodes):
vray_proxy_nodes = cmds.ls(nodes, type="VRayProxy")
for vp in vray_proxy_nodes:
path = cmds.getAttr("{}.fileName".format(vp))
ids = vray_proxies.get_alembic_ids_cache(path)
ids = get_alembic_ids_cache(path)
parent_id = {}
for k, _ in ids.items():
pid = k.split(":")[0]

View file

@ -1,7 +1,8 @@
from collections import defaultdict
from avalon.tools import models
from avalon.vendor.Qt import QtCore
from Qt import QtCore
from avalon.tools import models
from avalon.vendor import qtawesome
from avalon.style import colors

View file

@ -1,4 +1,4 @@
from avalon.vendor.Qt import QtWidgets, QtCore
from Qt import QtWidgets, QtCore
DEFAULT_COLOR = "#fb9c15"

View file

@ -1,13 +1,16 @@
import logging
from collections import defaultdict
from avalon.vendor.Qt import QtWidgets, QtCore
from Qt import QtWidgets, QtCore
# TODO: expose this better in avalon core
from avalon.tools import lib
from avalon.tools.models import TreeModel
from . import models
from .models import (
AssetModel,
LookModel
)
from . import commands
from . import views
@ -30,7 +33,7 @@ class AssetOutliner(QtWidgets.QWidget):
title.setAlignment(QtCore.Qt.AlignCenter)
title.setStyleSheet("font-weight: bold; font-size: 12px")
model = models.AssetModel()
model = AssetModel()
view = views.View()
view.setModel(model)
view.customContextMenuRequested.connect(self.right_mouse_menu)
@ -201,7 +204,7 @@ class LookOutliner(QtWidgets.QWidget):
title.setStyleSheet("font-weight: bold; font-size: 12px")
title.setAlignment(QtCore.Qt.AlignCenter)
model = models.LookModel()
model = LookModel()
# Proxy for dynamic sorting
proxy = QtCore.QSortFilterProxyModel()
@ -257,5 +260,3 @@ class LookOutliner(QtWidgets.QWidget):
menu.addAction(apply_action)
menu.exec_(globalpos)

View file

@ -0,0 +1,65 @@
from Qt import QtWidgets
from openpype.tools.experimental_tools import (
ExperimentalTools,
LOCAL_EXPERIMENTAL_KEY
)
__all__ = (
"LocalExperimentalToolsWidgets",
"LOCAL_EXPERIMENTAL_KEY"
)
class LocalExperimentalToolsWidgets(QtWidgets.QWidget):
def __init__(self, parent):
super(LocalExperimentalToolsWidgets, self).__init__(parent)
self._loading_local_settings = False
layout = QtWidgets.QFormLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# Label that says there are no experimental tools available
empty_label = QtWidgets.QLabel(self)
empty_label.setText(
"There are no experimental tools available..."
)
layout.addRow(empty_label)
experimental_defs = ExperimentalTools(filter_hosts=False)
checkboxes_by_identifier = {}
for tool in experimental_defs.tools:
checkbox = QtWidgets.QCheckBox(self)
label_widget = QtWidgets.QLabel(tool.label, self)
checkbox.setToolTip(tool.tooltip)
label_widget.setToolTip(tool.tooltip)
layout.addRow(label_widget, checkbox)
checkboxes_by_identifier[tool.identifier] = checkbox
empty_label.setVisible(len(checkboxes_by_identifier) == 0)
self._empty_label = empty_label
self._checkboxes_by_identifier = checkboxes_by_identifier
self._experimental_defs = experimental_defs
def update_local_settings(self, value):
self._loading_local_settings = True
value = value or {}
for identifier, checkbox in self._checkboxes_by_identifier.items():
checked = value.get(identifier, False)
checkbox.setChecked(checked)
self._loading_local_settings = False
def settings_value(self):
# Add changed
# If these have changed then
output = {}
for identifier, checkbox in self._checkboxes_by_identifier.items():
if checkbox.isChecked():
output[identifier] = True
return output

View file

@ -20,6 +20,10 @@ from .widgets import (
)
from .mongo_widget import OpenPypeMongoWidget
from .general_widget import LocalGeneralWidgets
from .experimental_widget import (
LocalExperimentalToolsWidgets,
LOCAL_EXPERIMENTAL_KEY
)
from .apps_widget import LocalApplicationsWidgets
from .projects_widget import ProjectSettingsWidget
@ -44,11 +48,13 @@ class LocalSettingsWidget(QtWidgets.QWidget):
self.pype_mongo_widget = None
self.general_widget = None
self.experimental_widget = None
self.apps_widget = None
self.projects_widget = None
self._create_pype_mongo_ui()
self._create_general_ui()
self._create_experimental_ui()
self._create_app_ui()
self._create_project_ui()
@ -85,6 +91,26 @@ class LocalSettingsWidget(QtWidgets.QWidget):
self.general_widget = general_widget
def _create_experimental_ui(self):
# General
experimental_expand_widget = ExpandingWidget(
"Experimental tools", self
)
experimental_content = QtWidgets.QWidget(self)
experimental_layout = QtWidgets.QVBoxLayout(experimental_content)
experimental_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0)
experimental_expand_widget.set_content_widget(experimental_content)
experimental_widget = LocalExperimentalToolsWidgets(
experimental_content
)
experimental_layout.addWidget(experimental_widget)
self.main_layout.addWidget(experimental_expand_widget)
self.experimental_widget = experimental_widget
def _create_app_ui(self):
# Applications
app_expand_widget = ExpandingWidget("Applications", self)
@ -135,6 +161,9 @@ class LocalSettingsWidget(QtWidgets.QWidget):
self.projects_widget.update_local_settings(
value.get(LOCAL_PROJECTS_KEY)
)
self.experimental_widget.update_local_settings(
value.get(LOCAL_EXPERIMENTAL_KEY)
)
def settings_value(self):
output = {}
@ -149,6 +178,10 @@ class LocalSettingsWidget(QtWidgets.QWidget):
projects_value = self.projects_widget.settings_value()
if projects_value:
output[LOCAL_PROJECTS_KEY] = projects_value
experimental_value = self.experimental_widget.settings_value()
if experimental_value:
output[LOCAL_EXPERIMENTAL_KEY] = experimental_value
return output

View file

@ -142,18 +142,23 @@ class ConsoleTrayApp:
self.tray_reconnect = False
ConsoleTrayApp.webserver_client.close()
def _send_text(self, new_text):
def _send_text_queue(self):
"""Sends lines and purges queue"""
lines = tuple(self.new_text)
self.new_text.clear()
if lines:
self._send_lines(lines)
def _send_lines(self, lines):
""" Send console content. """
if not ConsoleTrayApp.webserver_client:
return
if isinstance(new_text, str):
new_text = collections.deque(new_text.split("\n"))
payload = {
"host": self.host_id,
"action": host_console_listener.MsgAction.ADD,
"text": "\n".join(new_text)
"text": "\n".join(lines)
}
self._send(payload)
@ -174,14 +179,7 @@ class ConsoleTrayApp:
if self.tray_reconnect:
self._connect() # reconnect
if ConsoleTrayApp.webserver_client and self.new_text:
self._send_text(self.new_text)
self.new_text = collections.deque()
if self.new_text: # no webserver_client, text keeps stashing
start = max(len(self.new_text) - self.MAX_LINES, 0)
self.new_text = itertools.islice(self.new_text,
start, self.MAX_LINES)
self._send_text_queue()
if not self.initialized:
if self.initializing:
@ -191,7 +189,7 @@ class ConsoleTrayApp:
elif not host_connected:
text = "{} process is not alive. Exiting".format(self.host)
print(text)
self._send_text([text])
self._send_lines([text])
ConsoleTrayApp.websocket_server.stop()
sys.exit(1)
elif host_connected:
@ -205,14 +203,15 @@ class ConsoleTrayApp:
self.initializing = True
self.launch_method(*self.subprocess_args)
elif ConsoleTrayApp.process.poll() is not None:
self.exit()
elif ConsoleTrayApp.callback_queue:
elif ConsoleTrayApp.callback_queue and \
not ConsoleTrayApp.callback_queue.empty():
try:
callback = ConsoleTrayApp.callback_queue.get(block=False)
callback()
except queue.Empty:
pass
elif ConsoleTrayApp.process.poll() is not None:
self.exit()
@classmethod
def execute_in_main_thread(cls, func_to_call_from_main_thread):
@ -232,8 +231,9 @@ class ConsoleTrayApp:
self._close()
if ConsoleTrayApp.websocket_server:
ConsoleTrayApp.websocket_server.stop()
ConsoleTrayApp.process.kill()
ConsoleTrayApp.process.wait()
if ConsoleTrayApp.process:
ConsoleTrayApp.process.kill()
ConsoleTrayApp.process.wait()
if self.timer:
self.timer.stop()
QtCore.QCoreApplication.exit()

View file

@ -7,6 +7,7 @@ import Qt
from Qt import QtWidgets, QtGui, QtCore
from avalon.lib import HeroVersionType
from openpype.style import get_objected_colors
from .models import (
AssetModel,
TreeModel
@ -24,6 +25,19 @@ log = logging.getLogger(__name__)
class AssetDelegate(QtWidgets.QItemDelegate):
bar_height = 3
def __init__(self, *args, **kwargs):
super(AssetDelegate, 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):
result = super(AssetDelegate, self).sizeHint(option, index)
height = result.height()
@ -66,17 +80,20 @@ class AssetDelegate(QtWidgets.QItemDelegate):
counter += 1
# Background
bg_color = QtGui.QColor(60, 60, 60)
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.setRgb(70, 70, 70)
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.setAlpha(100)
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

View file

@ -28,6 +28,7 @@ class HostToolsHelper:
self._scene_inventory_tool = None
self._library_loader_tool = None
self._look_assigner_tool = None
self._experimental_tools_dialog = None
@property
def log(self):
@ -40,7 +41,6 @@ class HostToolsHelper:
def get_workfiles_tool(self, parent):
"""Create, cache and return workfiles tool window."""
if self._workfiles_tool is None:
from avalon import style
from openpype.tools.workfiles.app import (
Window, validate_host_requirements
)
@ -49,13 +49,14 @@ class HostToolsHelper:
validate_host_requirements(host)
workfiles_window = Window(parent=parent)
workfiles_window.setStyleSheet(style.load_stylesheet())
self._workfiles_tool = workfiles_window
return self._workfiles_tool
def show_workfiles(self, parent=None, use_context=None, save=None):
"""Workfiles tool for changing context and saving workfiles."""
from avalon import style
if use_context is None:
use_context = True
@ -79,24 +80,28 @@ class HostToolsHelper:
# Pull window to the front.
workfiles_tool.raise_()
workfiles_tool.activateWindow()
workfiles_tool.setStyleSheet(style.load_stylesheet())
def get_loader_tool(self, parent):
"""Create, cache and return loader tool window."""
if self._loader_tool is None:
from avalon import style
from openpype.tools.loader import LoaderWindow
loader_window = LoaderWindow(parent=parent or self._parent)
loader_window.setStyleSheet(style.load_stylesheet())
self._loader_tool = loader_window
return self._loader_tool
def show_loader(self, parent=None, use_context=None):
"""Loader tool for loading representations."""
loader_tool = self.get_loader_tool(parent)
loader_tool.show()
loader_tool.raise_()
loader_tool.activateWindow()
if use_context is None:
use_context = False
loader_tool = self.get_loader_tool(parent)
if use_context:
context = {"asset": avalon.api.Session["AVALON_ASSET"]}
@ -104,29 +109,26 @@ class HostToolsHelper:
else:
loader_tool.refresh()
loader_tool.show()
loader_tool.raise_()
loader_tool.activateWindow()
loader_tool.refresh()
def get_creator_tool(self, parent):
"""Create, cache and return creator tool window."""
if self._creator_tool is None:
from avalon import style
from avalon.tools.creator.app import Window
creator_window = Window(parent=parent or self._parent)
creator_window.setStyleSheet(style.load_stylesheet())
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()
@ -134,20 +136,22 @@ class HostToolsHelper:
def get_subset_manager_tool(self, parent):
"""Create, cache and return subset manager tool window."""
if self._subset_manager_tool is None:
from avalon import style
from avalon.tools.subsetmanager import Window
subset_manager_window = Window(parent=parent or self._parent)
subset_manager_window.setStyleSheet(style.load_stylesheet())
self._subset_manager_tool = subset_manager_window
return self._subset_manager_tool
def show_subset_manager(self, parent=None):
"""Show tool display/remove existing created instances."""
from avalon import style
subset_manager_tool = self.get_subset_manager_tool(parent)
subset_manager_tool.show()
subset_manager_tool.setStyleSheet(style.load_stylesheet())
# Pull window to the front.
subset_manager_tool.raise_()
subset_manager_tool.activateWindow()
@ -155,20 +159,21 @@ class HostToolsHelper:
def get_scene_inventory_tool(self, parent):
"""Create, cache and return scene inventory tool window."""
if self._scene_inventory_tool is None:
from avalon import style
from avalon.tools.sceneinventory.app import Window
scene_inventory_window = Window(parent=parent or self._parent)
scene_inventory_window.setStyleSheet(style.load_stylesheet())
self._scene_inventory_tool = scene_inventory_window
return self._scene_inventory_tool
def show_scene_inventory(self, parent=None):
"""Show tool maintain loaded containers."""
from avalon import style
scene_inventory_tool = self.get_scene_inventory_tool(parent)
scene_inventory_tool.show()
scene_inventory_tool.refresh()
scene_inventory_tool.setStyleSheet(style.load_stylesheet())
# Pull window to the front.
scene_inventory_tool.raise_()
@ -177,13 +182,11 @@ class HostToolsHelper:
def get_library_loader_tool(self, parent):
"""Create, cache and return library loader tool window."""
if self._library_loader_tool is None:
from avalon import style
from openpype.tools.libraryloader import LibraryLoaderWindow
library_window = LibraryLoaderWindow(
parent=parent or self._parent
)
library_window.setStyleSheet(style.load_stylesheet())
self._library_loader_tool = library_window
return self._library_loader_tool
@ -205,18 +208,46 @@ class HostToolsHelper:
def get_look_assigner_tool(self, parent):
"""Create, cache and return look assigner tool window."""
if self._look_assigner_tool is None:
from avalon import style
import mayalookassigner
mayalookassigner_window = mayalookassigner.App(parent)
mayalookassigner_window.setStyleSheet(style.load_stylesheet())
self._look_assigner_tool = mayalookassigner_window
return self._look_assigner_tool
def show_look_assigner(self, parent=None):
"""Look manager is Maya specific tool for look management."""
from avalon import style
look_assigner_tool = self.get_look_assigner_tool(parent)
look_assigner_tool.show()
look_assigner_tool.setStyleSheet(style.load_stylesheet())
def get_experimental_tools_dialog(self, parent=None):
"""Dialog of experimental tools.
For some hosts it is not easy to modify menu of tools. For
those cases was addded experimental tools dialog which is Qt based
and can dynamically filled by experimental tools so
host need only single "Experimental tools" button to see them.
Dialog can be also empty with a message that there are not available
experimental tools.
"""
if self._experimental_tools_dialog is None:
from openpype.tools.experimental_tools import (
ExperimentalToolsDialog
)
self._experimental_tools_dialog = ExperimentalToolsDialog(parent)
return self._experimental_tools_dialog
def show_experimental_tools_dialog(self, parent=None):
"""Show dialog with experimental tools."""
dialog = self.get_experimental_tools_dialog(parent)
dialog.show()
dialog.raise_()
dialog.activateWindow()
def get_tool_by_name(self, tool_name, parent=None, *args, **kwargs):
"""Show tool by it's name.
@ -247,6 +278,9 @@ class HostToolsHelper:
elif tool_name == "publish":
self.log.info("Can't return publish tool window.")
elif tool_name == "experimental_tools":
return self.get_experimental_tools_dialog(parent, *args, **kwargs)
else:
self.log.warning(
"Can't show unknown tool name: \"{}\"".format(tool_name)
@ -281,6 +315,9 @@ class HostToolsHelper:
elif tool_name == "publish":
self.show_publish(parent, *args, **kwargs)
elif tool_name == "experimental_tools":
self.show_experimental_tools_dialog(parent, *args, **kwargs)
else:
self.log.warning(
"Can't show unknown tool name: \"{}\"".format(tool_name)
@ -355,3 +392,7 @@ def show_look_assigner(parent=None):
def show_publish(parent=None):
_SingletonPoint.show_tool_by_name("publish", parent)
def show_experimental_tools_dialog(parent=None):
_SingletonPoint.show_tool_by_name("experimental_tools", parent)

View file

@ -68,8 +68,8 @@ class AssetsView(TreeViewSpinner, DeselectableTreeView):
This implements a context menu.
"""
def __init__(self):
super(AssetsView, self).__init__()
def __init__(self, parent=None):
super(AssetsView, self).__init__(parent)
self.setIndentation(15)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.setHeaderHidden(True)

View file

@ -35,28 +35,19 @@ class AssetWidget(QtWidgets.QWidget):
self.dbcon = dbcon
self.setContentsMargins(0, 0, 0, 0)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
# Tree View
model = AssetModel(dbcon=self.dbcon, parent=self)
proxy = RecursiveSortFilterProxyModel()
proxy.setSourceModel(model)
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
view = AssetsView()
view = AssetsView(self)
view.setModel(proxy)
if multiselection:
asset_delegate = AssetDelegate()
view.setSelectionMode(view.ExtendedSelection)
view.setItemDelegate(asset_delegate)
# Header
header = QtWidgets.QHBoxLayout()
icon = qtawesome.icon("fa.arrow-down", color=style.colors.light)
set_current_asset_btn = QtWidgets.QPushButton(icon, "")
set_current_asset_btn.setToolTip("Go to Asset from current Session")
@ -64,22 +55,28 @@ class AssetWidget(QtWidgets.QWidget):
set_current_asset_btn.setVisible(False)
icon = qtawesome.icon("fa.refresh", color=style.colors.light)
refresh = QtWidgets.QPushButton(icon, "")
refresh = QtWidgets.QPushButton(icon, "", parent=self)
refresh.setToolTip("Refresh items")
filter = QtWidgets.QLineEdit()
filter.textChanged.connect(proxy.setFilterFixedString)
filter.setPlaceholderText("Filter assets..")
filter_input = QtWidgets.QLineEdit(self)
filter_input.setPlaceholderText("Filter assets..")
header.addWidget(filter)
header.addWidget(set_current_asset_btn)
header.addWidget(refresh)
# Header
header_layout = QtWidgets.QHBoxLayout()
header_layout.addWidget(filter_input)
header_layout.addWidget(set_current_asset_btn)
header_layout.addWidget(refresh)
# Layout
layout.addLayout(header)
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(proxy.setFilterFixedString)
selection = view.selectionModel()
selection.selectionChanged.connect(self.selection_changed)
selection.currentChanged.connect(self.current_changed)
@ -313,7 +310,6 @@ class OptionalMenu(QtWidgets.QMenu):
actions that were instances of `QtWidgets.QWidgetAction`.
"""
def mouseReleaseEvent(self, event):
"""Emit option clicked signal if mouse released on it"""
active = self.actionAt(event.pos())
@ -352,6 +348,7 @@ class OptionalAction(QtWidgets.QWidgetAction):
self.use_option = use_option
self.option_tip = ""
self.optioned = False
self.widget = None
def createWidget(self, parent):
widget = OptionalActionWidget(self.label, parent)
@ -377,20 +374,10 @@ class OptionalAction(QtWidgets.QWidgetAction):
self.optioned = True
def set_highlight(self, state, global_pos=None):
body = self.widget.body
option = self.widget.option
role = QtGui.QPalette.Highlight if state else QtGui.QPalette.Window
body.setBackgroundRole(role)
body.setAutoFillBackground(state)
if not self.use_option:
return
state = option.is_hovered(global_pos)
role = QtGui.QPalette.Highlight if state else QtGui.QPalette.Window
option.setBackgroundRole(role)
option.setAutoFillBackground(state)
option_state = False
if self.use_option:
option_state = self.widget.option.is_hovered(global_pos)
self.widget.set_hover_properties(state, option_state)
class OptionalActionWidget(QtWidgets.QWidget):
@ -399,30 +386,33 @@ class OptionalActionWidget(QtWidgets.QWidget):
def __init__(self, label, parent=None):
super(OptionalActionWidget, self).__init__(parent)
body = QtWidgets.QWidget()
body.setStyleSheet("background: transparent;")
body_widget = QtWidgets.QWidget(self)
body_widget.setObjectName("OptionalActionBody")
icon = QtWidgets.QLabel()
label = QtWidgets.QLabel(label)
option = OptionBox(body)
icon = QtWidgets.QLabel(body_widget)
label = QtWidgets.QLabel(label, body_widget)
# (NOTE) For removing ugly QLable shadow FX when highlighted in Nuke.
# See https://stackoverflow.com/q/52838690/4145300
label.setStyle(QtWidgets.QStyleFactory.create("Plastique"))
option = OptionBox(body_widget)
option.setObjectName("OptionalActionOption")
icon.setFixedSize(24, 16)
option.setFixedSize(30, 30)
layout = QtWidgets.QHBoxLayout(body)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(2)
layout.addWidget(icon)
layout.addWidget(label)
layout.addSpacing(6)
body_layout = QtWidgets.QHBoxLayout(body_widget)
body_layout.setContentsMargins(4, 0, 4, 0)
body_layout.setSpacing(2)
body_layout.addWidget(icon)
body_layout.addWidget(label)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(6, 1, 2, 1)
layout.setContentsMargins(2, 1, 2, 1)
layout.setSpacing(0)
layout.addWidget(body)
layout.addWidget(body_widget)
layout.addWidget(option)
body.setMouseTracking(True)
body_widget.setMouseTracking(True)
label.setMouseTracking(True)
option.setMouseTracking(True)
self.setMouseTracking(True)
@ -431,11 +421,24 @@ class OptionalActionWidget(QtWidgets.QWidget):
self.icon = icon
self.label = label
self.option = option
self.body = body
self.body = body_widget
# (NOTE) For removing ugly QLable shadow FX when highlighted in Nuke.
# See https://stackoverflow.com/q/52838690/4145300
label.setStyle(QtWidgets.QStyleFactory.create("Plastique"))
def set_hover_properties(self, hovered, option_hovered):
body_state = ""
option_state = ""
if hovered:
body_state = "hover"
if option_hovered:
option_state = "hover"
if self.body.property("state") != body_state:
self.body.setProperty("state", body_state)
self.body.style().polish(self.body)
if self.option.property("state") != option_state:
self.option.setProperty("state", option_state)
self.option.style().polish(self.option)
def setIcon(self, icon):
pixmap = icon.pixmap(16, 16)
@ -456,8 +459,6 @@ class OptionBox(QtWidgets.QLabel):
pixmap = icon.pixmap(18, 18)
self.setPixmap(pixmap)
self.setStyleSheet("background: transparent;")
def is_hovered(self, global_pos):
if global_pos is None:
return False
@ -476,20 +477,20 @@ class OptionDialog(QtWidgets.QDialog):
def create(self, options):
parser = qargparse.QArgumentParser(arguments=options)
decision = QtWidgets.QWidget()
accept = QtWidgets.QPushButton("Accept")
cancel = QtWidgets.QPushButton("Cancel")
decision_widget = QtWidgets.QWidget(self)
accept_btn = QtWidgets.QPushButton("Accept", decision_widget)
cancel_btn = QtWidgets.QPushButton("Cancel", decision_widget)
layout = QtWidgets.QHBoxLayout(decision)
layout.addWidget(accept)
layout.addWidget(cancel)
decision_layout = QtWidgets.QHBoxLayout(decision_widget)
decision_layout.addWidget(accept_btn)
decision_layout.addWidget(cancel_btn)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(parser)
layout.addWidget(decision)
layout.addWidget(decision_widget)
accept.clicked.connect(self.accept)
cancel.clicked.connect(self.reject)
accept_btn.clicked.connect(self.accept)
cancel_btn.clicked.connect(self.reject)
parser.changed.connect(self.on_changed)
def on_changed(self, argument):

View file

@ -161,37 +161,62 @@ def capture(camera=None,
cmds.currentTime(cmds.currentTime(query=True))
padding = 10 # Extend panel to accommodate for OS window manager
with _independent_panel(width=width + padding,
height=height + padding,
off_screen=off_screen) as panel:
cmds.setFocus(panel)
with contextlib.nested(
_disabled_inview_messages(),
_maintain_camera(panel, camera),
_applied_viewport_options(viewport_options, panel),
_applied_camera_options(camera_options, panel),
_applied_display_options(display_options),
_applied_viewport2_options(viewport2_options),
_isolated_nodes(isolate, panel),
_maintained_time()):
all_playblast_kwargs = {
"compression": compression,
"format": format,
"percent": 100,
"quality": quality,
"viewer": viewer,
"startTime": start_frame,
"endTime": end_frame,
"offScreen": off_screen,
"showOrnaments": show_ornaments,
"forceOverwrite": overwrite,
"filename": filename,
"widthHeight": [width, height],
"rawFrameNumbers": raw_frame_numbers,
"framePadding": frame_padding
}
all_playblast_kwargs.update(playblast_kwargs)
output = cmds.playblast(
compression=compression,
format=format,
percent=100,
quality=quality,
viewer=viewer,
startTime=start_frame,
endTime=end_frame,
offScreen=off_screen,
showOrnaments=show_ornaments,
forceOverwrite=overwrite,
filename=filename,
widthHeight=[width, height],
rawFrameNumbers=raw_frame_numbers,
framePadding=frame_padding,
**playblast_kwargs)
if getattr(contextlib, "nested", None):
with contextlib.nested(
_disabled_inview_messages(),
_maintain_camera(panel, camera),
_applied_viewport_options(viewport_options, panel),
_applied_camera_options(camera_options, panel),
_applied_display_options(display_options),
_applied_viewport2_options(viewport2_options),
_isolated_nodes(isolate, panel),
_maintained_time()
):
output = cmds.playblast(**all_playblast_kwargs)
else:
with contextlib.ExitStack() as stack:
stack.enter_context(_disabled_inview_messages())
stack.enter_context(_maintain_camera(panel, camera))
stack.enter_context(
_applied_viewport_options(viewport_options, panel)
)
stack.enter_context(
_applied_camera_options(camera_options, panel)
)
stack.enter_context(
_applied_display_options(display_options)
)
stack.enter_context(
_applied_viewport2_options(viewport2_options)
)
stack.enter_context(_isolated_nodes(isolate, panel))
stack.enter_context(_maintained_time())
output = cmds.playblast(**all_playblast_kwargs)
return output
@ -364,7 +389,8 @@ def apply_view(panel, **options):
# Display options
display_options = options.get("display_options", {})
for key, value in display_options.iteritems():
_iteritems = getattr(display_options, "iteritems", display_options.items)
for key, value in _iteritems():
if key in _DisplayOptionsRGB:
cmds.displayRGBColor(key, *value)
else:
@ -372,16 +398,21 @@ def apply_view(panel, **options):
# Camera options
camera_options = options.get("camera_options", {})
for key, value in camera_options.iteritems():
_iteritems = getattr(camera_options, "iteritems", camera_options.items)
for key, value in _iteritems:
cmds.setAttr("{0}.{1}".format(camera, key), value)
# Viewport options
viewport_options = options.get("viewport_options", {})
for key, value in viewport_options.iteritems():
_iteritems = getattr(viewport_options, "iteritems", viewport_options.items)
for key, value in _iteritems():
cmds.modelEditor(panel, edit=True, **{key: value})
viewport2_options = options.get("viewport2_options", {})
for key, value in viewport2_options.iteritems():
_iteritems = getattr(
viewport2_options, "iteritems", viewport2_options.items
)
for key, value in _iteritems():
attr = "hardwareRenderingGlobals.{0}".format(key)
cmds.setAttr(attr, value)
@ -629,14 +660,16 @@ def _applied_camera_options(options, panel):
"for capture: %s" % opt)
options.pop(opt)
for opt, value in options.iteritems():
_iteritems = getattr(options, "iteritems", options.items)
for opt, value in _iteritems():
cmds.setAttr(camera + "." + opt, value)
try:
yield
finally:
if old_options:
for opt, value in old_options.iteritems():
_iteritems = getattr(old_options, "iteritems", old_options.items)
for opt, value in _iteritems():
cmds.setAttr(camera + "." + opt, value)
@ -722,14 +755,16 @@ def _applied_viewport2_options(options):
options.pop(opt)
# Apply settings
for opt, value in options.iteritems():
_iteritems = getattr(options, "iteritems", options.items)
for opt, value in _iteritems():
cmds.setAttr("hardwareRenderingGlobals." + opt, value)
try:
yield
finally:
# Restore previous settings
for opt, value in original.iteritems():
_iteritems = getattr(original, "iteritems", original.items)
for opt, value in _iteritems():
cmds.setAttr("hardwareRenderingGlobals." + opt, value)
@ -769,7 +804,8 @@ def _maintain_camera(panel, camera):
try:
yield
finally:
for camera, renderable in state.iteritems():
_iteritems = getattr(state, "iteritems", state.items)
for camera, renderable in _iteritems():
cmds.setAttr(camera + ".rnd", renderable)

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.5.0"
__version__ = "3.6.0-nightly.1"

94
poetry.lock generated
View file

@ -782,7 +782,7 @@ six = "*"
[[package]]
name = "pillow"
version = "8.2.0"
version = "8.3.2"
description = "Python Imaging Library (Fork)"
category = "main"
optional = false
@ -1538,7 +1538,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt
[metadata]
lock-version = "1.1"
python-versions = "3.7.*"
content-hash = "ff2bfa35a7304378917a0c25d7d7af9f81a130288d95789bdf7429f071e80b69"
content-hash = "fb6db80d126fe7ef2d1d06d0381b6d11445d6d3e54b33585f6b0a0b6b0b9d372"
[metadata.files]
acre = []
@ -2058,40 +2058,59 @@ pathlib2 = [
{file = "pathlib2-2.3.5.tar.gz", hash = "sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868"},
]
pillow = [
{file = "Pillow-8.2.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:dc38f57d8f20f06dd7c3161c59ca2c86893632623f33a42d592f097b00f720a9"},
{file = "Pillow-8.2.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a013cbe25d20c2e0c4e85a9daf438f85121a4d0344ddc76e33fd7e3965d9af4b"},
{file = "Pillow-8.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8bb1e155a74e1bfbacd84555ea62fa21c58e0b4e7e6b20e4447b8d07990ac78b"},
{file = "Pillow-8.2.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c5236606e8570542ed424849f7852a0ff0bce2c4c8d0ba05cc202a5a9c97dee9"},
{file = "Pillow-8.2.0-cp36-cp36m-win32.whl", hash = "sha256:12e5e7471f9b637762453da74e390e56cc43e486a88289995c1f4c1dc0bfe727"},
{file = "Pillow-8.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5afe6b237a0b81bd54b53f835a153770802f164c5570bab5e005aad693dab87f"},
{file = "Pillow-8.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:cb7a09e173903541fa888ba010c345893cd9fc1b5891aaf060f6ca77b6a3722d"},
{file = "Pillow-8.2.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0d19d70ee7c2ba97631bae1e7d4725cdb2ecf238178096e8c82ee481e189168a"},
{file = "Pillow-8.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:083781abd261bdabf090ad07bb69f8f5599943ddb539d64497ed021b2a67e5a9"},
{file = "Pillow-8.2.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c6b39294464b03457f9064e98c124e09008b35a62e3189d3513e5148611c9388"},
{file = "Pillow-8.2.0-cp37-cp37m-win32.whl", hash = "sha256:01425106e4e8cee195a411f729cff2a7d61813b0b11737c12bd5991f5f14bcd5"},
{file = "Pillow-8.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3b570f84a6161cf8865c4e08adf629441f56e32f180f7aa4ccbd2e0a5a02cba2"},
{file = "Pillow-8.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:031a6c88c77d08aab84fecc05c3cde8414cd6f8406f4d2b16fed1e97634cc8a4"},
{file = "Pillow-8.2.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:66cc56579fd91f517290ab02c51e3a80f581aba45fd924fcdee01fa06e635812"},
{file = "Pillow-8.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c32cc3145928c4305d142ebec682419a6c0a8ce9e33db900027ddca1ec39178"},
{file = "Pillow-8.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:624b977355cde8b065f6d51b98497d6cd5fbdd4f36405f7a8790e3376125e2bb"},
{file = "Pillow-8.2.0-cp38-cp38-win32.whl", hash = "sha256:5cbf3e3b1014dddc45496e8cf38b9f099c95a326275885199f427825c6522232"},
{file = "Pillow-8.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:463822e2f0d81459e113372a168f2ff59723e78528f91f0bd25680ac185cf797"},
{file = "Pillow-8.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:95d5ef984eff897850f3a83883363da64aae1000e79cb3c321915468e8c6add5"},
{file = "Pillow-8.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b91c36492a4bbb1ee855b7d16fe51379e5f96b85692dc8210831fbb24c43e484"},
{file = "Pillow-8.2.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d68cb92c408261f806b15923834203f024110a2e2872ecb0bd2a110f89d3c602"},
{file = "Pillow-8.2.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f217c3954ce5fd88303fc0c317af55d5e0204106d86dea17eb8205700d47dec2"},
{file = "Pillow-8.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5b70110acb39f3aff6b74cf09bb4169b167e2660dabc304c1e25b6555fa781ef"},
{file = "Pillow-8.2.0-cp39-cp39-win32.whl", hash = "sha256:a7d5e9fad90eff8f6f6106d3b98b553a88b6f976e51fce287192a5d2d5363713"},
{file = "Pillow-8.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:238c197fc275b475e87c1453b05b467d2d02c2915fdfdd4af126145ff2e4610c"},
{file = "Pillow-8.2.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:0e04d61f0064b545b989126197930807c86bcbd4534d39168f4aa5fda39bb8f9"},
{file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:63728564c1410d99e6d1ae8e3b810fe012bc440952168af0a2877e8ff5ab96b9"},
{file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:c03c07ed32c5324939b19e36ae5f75c660c81461e312a41aea30acdd46f93a7c"},
{file = "Pillow-8.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:4d98abdd6b1e3bf1a1cbb14c3895226816e666749ac040c4e2554231068c639b"},
{file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:aac00e4bc94d1b7813fe882c28990c1bc2f9d0e1aa765a5f2b516e8a6a16a9e4"},
{file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:22fd0f42ad15dfdde6c581347eaa4adb9a6fc4b865f90b23378aa7914895e120"},
{file = "Pillow-8.2.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:e98eca29a05913e82177b3ba3d198b1728e164869c613d76d0de4bde6768a50e"},
{file = "Pillow-8.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8b56553c0345ad6dcb2e9b433ae47d67f95fc23fe28a0bde15a120f25257e291"},
{file = "Pillow-8.2.0.tar.gz", hash = "sha256:a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1"},
{file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"},
{file = "Pillow-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2"},
{file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f"},
{file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c"},
{file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a"},
{file = "Pillow-8.3.2-cp310-cp310-win32.whl", hash = "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228"},
{file = "Pillow-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875"},
{file = "Pillow-8.3.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2"},
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"},
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"},
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"},
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"},
{file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"},
{file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"},
{file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"},
{file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"},
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"},
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"},
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"},
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"},
{file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"},
{file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"},
{file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"},
{file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"},
{file = "Pillow-8.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3"},
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"},
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"},
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"},
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"},
{file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"},
{file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"},
{file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"},
{file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"},
{file = "Pillow-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630"},
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"},
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"},
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"},
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"},
{file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"},
{file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"},
{file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"},
{file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"},
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"},
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"},
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"},
{file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"},
{file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"},
{file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
@ -2294,6 +2313,7 @@ pynput = [
pyobjc-core = [
{file = "pyobjc-core-7.3.tar.gz", hash = "sha256:5081aedf8bb40aac1a8ad95adac9e44e148a882686ded614adf46bb67fd67574"},
{file = "pyobjc_core-7.3-1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a1f1e6b457127cbf2b5bd2b94520a7c89fb590b739911eadb2b0499a3a5b0e6f"},
{file = "pyobjc_core-7.3-1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:ed708cc47bae8b711f81f252af09898a5f986c7a38cec5ad5623d571d328bff8"},
{file = "pyobjc_core-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e93ad769a20b908778fe950f62a843a6d8f0fa71996e5f3cc9fab5ae7d17771"},
{file = "pyobjc_core-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f63fd37bbf3785af4ddb2f86cad5ca81c62cfc7d1c0099637ca18343c3656c1"},
{file = "pyobjc_core-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9b1311f72f2e170742a7ee3a8149f52c35158dc024a21e88d6f1e52ba5d718b"},
@ -2303,6 +2323,7 @@ pyobjc-core = [
pyobjc-framework-cocoa = [
{file = "pyobjc-framework-Cocoa-7.3.tar.gz", hash = "sha256:b18d05e7a795a3455ad191c3e43d6bfa673c2a4fd480bb1ccf57191051b80b7e"},
{file = "pyobjc_framework_Cocoa-7.3-1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1e31376806e5de883a1d7c7c87d9ff2a8b09fc05d267e0dfce6e42409fb70c67"},
{file = "pyobjc_framework_Cocoa-7.3-1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d999387927284346035cb63ebb51f86331abc41f9376f9a6970e7f18207db392"},
{file = "pyobjc_framework_Cocoa-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9edffdfa6dd1f71f21b531c3e61fdd3e4d5d3bf6c5a528c98e88828cd60bac11"},
{file = "pyobjc_framework_Cocoa-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:35a6340437a4e0109a302150b7d1f6baf57004ccf74834f9e6062fcafe2fd8d7"},
{file = "pyobjc_framework_Cocoa-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7c3886f2608ab3ed02482f8b2ebf9f782b324c559e84b52cfd92dba8a1109872"},
@ -2312,6 +2333,7 @@ pyobjc-framework-cocoa = [
pyobjc-framework-quartz = [
{file = "pyobjc-framework-Quartz-7.3.tar.gz", hash = "sha256:98812844c34262def980bdf60923a875cd43428a8375b6fd53bd2cd800eccf0b"},
{file = "pyobjc_framework_Quartz-7.3-1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1139bc6874c0f8b58f0b8602015e0994198bc506a6bcec1071208de32b55ed26"},
{file = "pyobjc_framework_Quartz-7.3-1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d94a3ed7051266c52392ec07d3b5adbf28d4be83341a24df0d88639344dcd84f"},
{file = "pyobjc_framework_Quartz-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ef18f5a16511ded65980bf4f5983ea5d35c88224dbad1b3112abd29c60413ea"},
{file = "pyobjc_framework_Quartz-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b41eec8d4b10c7c7e011e2f9051367f5499ef315ba52dfbae573c3a2e05469c"},
{file = "pyobjc_framework_Quartz-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c65456ed045dfe1711d0298734e5a3ad670f8c770f7eb3b19979256c388bdd2"},

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
version = "3.0.0"
version = "3.6.0-nightly.1" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team <info@openpype.io>"]
license = "MIT License"
@ -45,7 +45,7 @@ jsonschema = "^3.2.0"
keyring = "^22.0.1"
log4mongo = "^1.7"
pathlib2= "^2.3.5" # deadline submit publish job only (single place, maybe not needed?)
Pillow = "^8.1" # only used for slates prototype
Pillow = "^8.3" # only used for slates prototype
pyblish-base = "^1.8.8"
pynput = "^1.7.2" # idle manager in tray
pymongo = "^3.11.2"

View file

@ -0,0 +1,94 @@
import pytest
import os
import shutil
from tests.lib.testing_classes import PublishTest
class TestPublishInPhotoshop(PublishTest):
"""Basic test case for publishing in Photoshop
Uses generic TestCase to prepare fixtures for test data, testing DBs,
env vars.
Opens Maya, run publish on prepared workile.
Then checks content of DB (if subset, version, representations were
created.
Checks tmp folder if all expected files were published.
"""
PERSIST = True
TEST_FILES = [
("1Bciy2pCwMKl1UIpxuPnlX_LHMo_Xkq0K", "test_photoshop_publish.zip", "")
]
APP = "photoshop"
APP_VARIANT = "2020"
APP_NAME = "{}/{}".format(APP, APP_VARIANT)
TIMEOUT = 120 # publish timeout
@pytest.fixture(scope="module")
def last_workfile_path(self, download_test_data):
"""Get last_workfile_path from source data.
Maya expects workfile in proper folder, so copy is done first.
"""
src_path = os.path.join(download_test_data,
"input",
"workfile",
"test_project_test_asset_TestTask_v001.psd")
dest_folder = os.path.join(download_test_data,
self.PROJECT,
self.ASSET,
"work",
self.TASK)
os.makedirs(dest_folder)
dest_path = os.path.join(dest_folder,
"test_project_test_asset_TestTask_v001.psd")
shutil.copy(src_path, dest_path)
yield dest_path
@pytest.fixture(scope="module")
def startup_scripts(self, monkeypatch_session, download_test_data):
"""Points Maya to userSetup file from input data"""
os.environ["IS_HEADLESS"] = "true"
def test_db_asserts(self, dbcon, publish_finished):
"""Host and input data dependent expected results in DB."""
print("test_db_asserts")
assert 5 == dbcon.count_documents({"type": "version"}), \
"Not expected no of versions"
assert 0 == dbcon.count_documents({"type": "version",
"name": {"$ne": 1}}), \
"Only versions with 1 expected"
assert 1 == dbcon.count_documents({"type": "subset",
"name": "modelMain"}), \
"modelMain subset must be present"
assert 1 == dbcon.count_documents({"type": "subset",
"name": "workfileTest_task"}), \
"workfileTest_task subset must be present"
assert 11 == dbcon.count_documents({"type": "representation"}), \
"Not expected no of representations"
assert 2 == dbcon.count_documents({"type": "representation",
"context.subset": "modelMain",
"context.ext": "abc"}), \
"Not expected no of representations with ext 'abc'"
assert 2 == dbcon.count_documents({"type": "representation",
"context.subset": "modelMain",
"context.ext": "ma"}), \
"Not expected no of representations with ext 'abc'"
if __name__ == "__main__":
test_case = TestPublishInPhotoshop()

Some files were not shown because too many files have changed in this diff Show more