Merge branch 'develop' of github.com:pypeclub/pype into feature/delivery_in_library_loader

This commit is contained in:
Petr Kalis 2021-05-24 11:17:50 +02:00
commit 68d452844b
103 changed files with 8312 additions and 408 deletions

View file

@ -1,5 +1,37 @@
# Changelog
## [2.18.0](https://github.com/pypeclub/openpype/tree/2.18.0) (2021-05-18)
[Full Changelog](https://github.com/pypeclub/openpype/compare/2.17.3...2.18.0)
**Enhancements:**
- Use SubsetLoader and multiple contexts for delete_old_versions [\#1484](ttps://github.com/pypeclub/OpenPype/pull/1484))
- TVPaint: Increment workfile version on successfull publish. [\#1489](https://github.com/pypeclub/OpenPype/pull/1489)
- Maya: Use of multiple deadline servers [\#1483](https://github.com/pypeclub/OpenPype/pull/1483)
**Fixed bugs:**
- Use instance frame start instead of timeline. [\#1486](https://github.com/pypeclub/OpenPype/pull/1486)
- Maya: Redshift - set proper start frame on proxy [\#1480](https://github.com/pypeclub/OpenPype/pull/1480)
- Maya: wrong collection of playblasted frames [\#1517](https://github.com/pypeclub/OpenPype/pull/1517)
- Existing subsets hints in creator [\#1502](https://github.com/pypeclub/OpenPype/pull/1502)
## [2.17.3](https://github.com/pypeclub/openpype/tree/2.17.3) (2021-05-06)
[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.3...2.17.3)
**Fixed bugs:**
- Nuke: workfile version synced to db version always [\#1479](https://github.com/pypeclub/OpenPype/pull/1479)
## [2.17.2](https://github.com/pypeclub/openpype/tree/2.17.2) (2021-05-04)
[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.1...2.17.2)
**Enhancements:**
- Forward/Backward compatible apps and tools with OpenPype 3 [\#1463](https://github.com/pypeclub/OpenPype/pull/1463)
## [2.17.1](https://github.com/pypeclub/openpype/tree/2.17.1) (2021-04-30)
@ -7,28 +39,30 @@
**Enhancements:**
- Nuke: deadline submission with gpu [\#1414](https://github.com/pypeclub/OpenPype/pull/1414)
- TVPaint frame range definition [\#1424](https://github.com/pypeclub/OpenPype/pull/1424)
- PS - group all published instances [\#1415](https://github.com/pypeclub/OpenPype/pull/1415)
- Nuke: deadline submission with gpu [\#1414](https://github.com/pypeclub/OpenPype/pull/1414)
- Add task name to context pop up. [\#1383](https://github.com/pypeclub/OpenPype/pull/1383)
- AE add duration validation [\#1363](https://github.com/pypeclub/OpenPype/pull/1363)
- Maya: Support for Redshift proxies [\#1360](https://github.com/pypeclub/OpenPype/pull/1360)
- Enhance review letterbox feature. [\#1371](https://github.com/pypeclub/OpenPype/pull/1371)
**Fixed bugs:**
- Nuke: fixing undo for loaded mov and sequence [\#1433](https://github.com/pypeclub/OpenPype/pull/1433)
- AE - validation for duration was 1 frame shorter [\#1426](https://github.com/pypeclub/OpenPype/pull/1426)
- Houdini menu filename [\#1417](https://github.com/pypeclub/OpenPype/pull/1417)
- Maya: Vray - problem getting all file nodes for look publishing [\#1399](https://github.com/pypeclub/OpenPype/pull/1399)
- AE - validation for duration was 1 frame shorter [\#1426](https://github.com/pypeclub/OpenPype/pull/1426)
**Merged pull requests:**
- Maya: Vray - problem getting all file nodes for look publishing [\#1399](https://github.com/pypeclub/OpenPype/pull/1399)
- Maya: Support for Redshift proxies [\#1360](https://github.com/pypeclub/OpenPype/pull/1360)
## [2.17.0](https://github.com/pypeclub/openpype/tree/2.17.0) (2021-04-20)
[Full Changelog](https://github.com/pypeclub/openpype/compare/3.0.0-beta2...2.17.0)
[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-beta.2...2.17.0)
**Enhancements:**
- Forward compatible ftrack group [\#1243](https://github.com/pypeclub/OpenPype/pull/1243)
- Settings in mongo as dict [\#1221](https://github.com/pypeclub/OpenPype/pull/1221)
- Maya: Make tx option configurable with presets [\#1328](https://github.com/pypeclub/OpenPype/pull/1328)
- TVPaint asset name validation [\#1302](https://github.com/pypeclub/OpenPype/pull/1302)
- TV Paint: Set initial project settings. [\#1299](https://github.com/pypeclub/OpenPype/pull/1299)
@ -56,35 +90,6 @@
- Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/OpenPype/pull/1178)
## [2.16.1](https://github.com/pypeclub/pype/tree/2.16.1) (2021-04-13)
[Full Changelog](https://github.com/pypeclub/pype/compare/2.16.0...2.16.1)
**Enhancements:**
- Nuke: comp renders mix up [\#1301](https://github.com/pypeclub/pype/pull/1301)
- Validate project settings [\#1297](https://github.com/pypeclub/pype/pull/1297)
- After Effects: added SubsetManager [\#1234](https://github.com/pypeclub/pype/pull/1234)
**Fixed bugs:**
- Ftrack custom attributes in bulks [\#1312](https://github.com/pypeclub/pype/pull/1312)
- Ftrack optional pypclub role [\#1303](https://github.com/pypeclub/pype/pull/1303)
- AE remove orphaned instance from workfile - fix self.stub [\#1282](https://github.com/pypeclub/pype/pull/1282)
- Avalon schema names [\#1242](https://github.com/pypeclub/pype/pull/1242)
- Handle duplication of Task name [\#1226](https://github.com/pypeclub/pype/pull/1226)
- Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/pype/pull/1217)
- Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/pype/pull/1214)
- Bulk mov strict task [\#1204](https://github.com/pypeclub/pype/pull/1204)
- Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/pype/pull/1202)
- Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/pype/pull/1199)
- Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/pype/pull/1178)
**Merged pull requests:**
- Forward compatible ftrack group [\#1243](https://github.com/pypeclub/pype/pull/1243)
- Error message in pyblish UI [\#1206](https://github.com/pypeclub/pype/pull/1206)
- Nuke: deadline submission with search replaced env values from preset [\#1194](https://github.com/pypeclub/pype/pull/1194)
## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) (2021-03-22)
@ -1145,4 +1150,7 @@ A large cleanup release. Most of the change are under the hood.
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*

View file

@ -224,6 +224,11 @@ def launch(app, project, asset, task,
PypeCommands().run_application(app, project, asset, task, tools, arguments)
@main.command(context_settings={"ignore_unknown_options": True})
def projectmanager():
PypeCommands().launch_project_manager()
@main.command(
context_settings=dict(
ignore_unknown_options=True,

View file

@ -0,0 +1,9 @@
def add_implementation_envs(env, _app):
"""Modify environments to contain all required for implementation."""
defaults = {
"OPENPYPE_LOG_NO_COLORS": "True",
"WEBSOCKET_URL": "ws://localhost:8097/ws/"
}
for key, value in defaults.items():
if not env.get(key):
env[key] = value

View file

@ -0,0 +1,55 @@
import os
def add_implementation_envs(env, _app):
"""Modify environments to contain all required for implementation."""
# Prepare path to implementation script
implementation_user_script_path = os.path.join(
os.environ["OPENPYPE_REPOS_ROOT"],
"repos",
"avalon-core",
"setup",
"blender"
)
# Add blender implementation script path to PYTHONPATH
python_path = env.get("PYTHONPATH") or ""
python_path_parts = [
path
for path in python_path.split(os.pathsep)
if path
]
python_path_parts.insert(0, implementation_user_script_path)
env["PYTHONPATH"] = os.pathsep.join(python_path_parts)
# Modify Blender user scripts path
previous_user_scripts = set()
# Implementation path is added to set for easier paths check inside loops
# - will be removed at the end
previous_user_scripts.add(implementation_user_script_path)
openpype_blender_user_scripts = (
env.get("OPENPYPE_BLENDER_USER_SCRIPTS") or ""
)
for path in openpype_blender_user_scripts.split(os.pathsep):
if path and os.path.exists(path):
previous_user_scripts.add(os.path.normpath(path))
blender_user_scripts = env.get("BLENDER_USER_SCRIPTS") or ""
for path in blender_user_scripts.split(os.pathsep):
if path and os.path.exists(path):
previous_user_scripts.add(os.path.normpath(path))
# Remove implementation path from user script paths as is set to
# `BLENDER_USER_SCRIPTS`
previous_user_scripts.remove(implementation_user_script_path)
env["BLENDER_USER_SCRIPTS"] = implementation_user_script_path
# Set custom user scripts env
env["OPENPYPE_BLENDER_USER_SCRIPTS"] = os.pathsep.join(
previous_user_scripts
)
# Define Qt binding if not defined
if not env.get("QT_PREFERRED_BINDING"):
env["QT_PREFERRED_BINDING"] = "PySide2"

View file

@ -4,6 +4,8 @@ import traceback
import bpy
from .lib import append_user_scripts
from avalon import api as avalon
from pyblish import api as pyblish
@ -29,7 +31,7 @@ def install():
pyblish.register_plugin_path(str(PUBLISH_PATH))
avalon.register_plugin_path(avalon.Loader, str(LOAD_PATH))
avalon.register_plugin_path(avalon.Creator, str(CREATE_PATH))
append_user_scripts()
avalon.on("new", on_new)
avalon.on("open", on_open)

View file

@ -0,0 +1,127 @@
import os
import traceback
import importlib
import bpy
import addon_utils
def load_scripts(paths):
"""Copy of `load_scripts` from Blender's implementation.
It is possible that whis function will be changed in future and usage will
be based on Blender version.
"""
import bpy_types
loaded_modules = set()
previous_classes = [
cls
for cls in bpy.types.bpy_struct.__subclasses__()
]
def register_module_call(mod):
register = getattr(mod, "register", None)
if register:
try:
register()
except:
traceback.print_exc()
else:
print("\nWarning! '%s' has no register function, "
"this is now a requirement for registerable scripts" %
mod.__file__)
def unregister_module_call(mod):
unregister = getattr(mod, "unregister", None)
if unregister:
try:
unregister()
except:
traceback.print_exc()
def test_reload(mod):
# reloading this causes internal errors
# because the classes from this module are stored internally
# possibly to refresh internal references too but for now, best not to.
if mod == bpy_types:
return mod
try:
return importlib.reload(mod)
except:
traceback.print_exc()
def test_register(mod):
if mod:
register_module_call(mod)
bpy.utils._global_loaded_modules.append(mod.__name__)
from bpy_restrict_state import RestrictBlend
with RestrictBlend():
for base_path in paths:
for path_subdir in bpy.utils._script_module_dirs:
path = os.path.join(base_path, path_subdir)
if not os.path.isdir(path):
continue
bpy.utils._sys_path_ensure_prepend(path)
# Only add to 'sys.modules' unless this is 'startup'.
if path_subdir != "startup":
continue
for mod in bpy.utils.modules_from_path(path, loaded_modules):
test_register(mod)
addons_paths = []
for base_path in paths:
addons_path = os.path.join(base_path, "addons")
if not os.path.exists(addons_path):
continue
addons_paths.append(addons_path)
addons_module_path = os.path.join(addons_path, "modules")
if os.path.exists(addons_module_path):
bpy.utils._sys_path_ensure_prepend(addons_module_path)
if addons_paths:
# Fake addons
origin_paths = addon_utils.paths
def new_paths():
paths = origin_paths() + addons_paths
return paths
addon_utils.paths = new_paths
addon_utils.modules_refresh()
# load template (if set)
if any(bpy.utils.app_template_paths()):
import bl_app_template_utils
bl_app_template_utils.reset(reload_scripts=False)
del bl_app_template_utils
for cls in bpy.types.bpy_struct.__subclasses__():
if cls in previous_classes:
continue
if not getattr(cls, "is_registered", False):
continue
for subcls in cls.__subclasses__():
if not subcls.is_registered:
print(
"Warning, unregistered class: %s(%s)" %
(subcls.__name__, cls.__name__)
)
def append_user_scripts():
user_scripts = os.environ.get("OPENPYPE_BLENDER_USER_SCRIPTS")
if not user_scripts:
return
try:
load_scripts(user_scripts.split(os.pathsep))
except Exception:
print("Couldn't load user scripts \"{}\"".format(user_scripts))
traceback.print_exc()

View file

@ -20,21 +20,9 @@ class CreateLayout(openpype.hosts.blender.api.plugin.Creator):
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)
collection = bpy.context.collection
collection.name = name
self.data['task'] = api.Session.get('AVALON_TASK')
lib.imprint(collection, self.data)
# Add the rig object and all the children meshes to
# a set and link them all at the end to avoid duplicates.
# Blender crashes if trying to link an object that is already linked.
# This links automatically the children meshes if they were not
# selected, and doesn't link them twice if they, insted,
# were manually selected by the user.
objects_to_link = set()
if (self.options or {}).get("useSelection"):
for obj in lib.get_selection():
collection.children.link(obj.users_collection[0])
return collection

View file

@ -367,13 +367,13 @@ class UnrealLayoutLoader(plugin.AssetLoader):
# Y axis mirrored
obj.location = (
location.get('x'),
-location.get('y'),
location.get('y'),
location.get('z')
)
obj.rotation_euler = (
rotation.get('x') + math.pi / 2,
-rotation.get('y'),
-rotation.get('z')
rotation.get('x'),
rotation.get('y'),
rotation.get('z')
)
obj.scale = (
scale.get('x'),

View file

@ -108,19 +108,21 @@ class BlendModelLoader(plugin.AssetLoader):
self.__class__.__name__,
)
container_metadata = container.get(
blender.pipeline.AVALON_PROPERTY)
metadata = container.get(blender.pipeline.AVALON_PROPERTY)
container_metadata["libpath"] = libpath
container_metadata["lib_container"] = lib_container
metadata["libpath"] = libpath
metadata["lib_container"] = lib_container
obj_container = self._process(
libpath, lib_container, container_name, None)
container_metadata["obj_container"] = obj_container
metadata["obj_container"] = obj_container
# Save the list of objects in the metadata container
container_metadata["objects"] = obj_container.all_objects
metadata["objects"] = obj_container.all_objects
metadata["parent"] = str(context["representation"]["parent"])
metadata["family"] = context["representation"]["context"]["family"]
nodes = list(container.objects)
nodes.append(container)

View file

@ -155,18 +155,20 @@ class BlendRigLoader(plugin.AssetLoader):
self.__class__.__name__,
)
container_metadata = container.get(
blender.pipeline.AVALON_PROPERTY)
metadata = container.get(blender.pipeline.AVALON_PROPERTY)
container_metadata["libpath"] = libpath
container_metadata["lib_container"] = lib_container
metadata["libpath"] = libpath
metadata["lib_container"] = lib_container
obj_container = self._process(
libpath, lib_container, collection_name, None, None)
container_metadata["obj_container"] = obj_container
metadata["obj_container"] = obj_container
# Save the list of objects in the metadata container
container_metadata["objects"] = obj_container.all_objects
metadata["objects"] = obj_container.all_objects
metadata["parent"] = str(context["representation"]["parent"])
metadata["family"] = context["representation"]["context"]["family"]
nodes = list(container.objects)
nodes.append(container)

View file

@ -0,0 +1,92 @@
import os
import json
import bpy
from avalon import blender, io
import openpype.api
class ExtractLayout(openpype.api.Extractor):
"""Extract a layout."""
label = "Extract Layout"
hosts = ["blender"]
families = ["layout"]
optional = True
def process(self, instance):
# Define extract output file path
stagingdir = self.staging_dir(instance)
# Perform extraction
self.log.info("Performing extraction..")
json_data = []
for collection in instance:
for asset in collection.children:
collection = bpy.data.collections[asset.name]
container = bpy.data.collections[asset.name + '_CON']
metadata = container.get(blender.pipeline.AVALON_PROPERTY)
parent = metadata["parent"]
family = metadata["family"]
self.log.debug("Parent: {}".format(parent))
blend = io.find_one(
{
"type": "representation",
"parent": io.ObjectId(parent),
"name": "blend"
},
projection={"_id": True})
blend_id = blend["_id"]
json_element = {}
json_element["reference"] = str(blend_id)
json_element["family"] = family
json_element["instance_name"] = asset.name
json_element["asset_name"] = metadata["lib_container"]
json_element["file_path"] = metadata["libpath"]
obj = collection.objects[0]
json_element["transform"] = {
"translation": {
"x": obj.location.x,
"y": obj.location.y,
"z": obj.location.z
},
"rotation": {
"x": obj.rotation_euler.x,
"y": obj.rotation_euler.y,
"z": obj.rotation_euler.z,
},
"scale": {
"x": obj.scale.x,
"y": obj.scale.y,
"z": obj.scale.z
}
}
json_data.append(json_element)
json_filename = "{}.json".format(instance.name)
json_path = os.path.join(stagingdir, json_filename)
with open(json_path, "w+") as file:
json.dump(json_data, fp=file, indent=2)
if "representations" not in instance.data:
instance.data["representations"] = []
representation = {
'name': 'json',
'ext': 'json',
'files': json_filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(representation)
self.log.info("Extracted instance '%s' to: %s",
instance.name, representation)

View file

@ -0,0 +1,10 @@
import os
def add_implementation_envs(env, _app):
"""Modify environments to contain all required for implementation."""
openharmony_path = os.path.join(
os.environ["OPENPYPE_REPOS_ROOT"], "pype", "vendor", "OpenHarmony"
)
# TODO check if is already set? What to do if is already set?
env["LIB_OPENHARMONY_PATH"] = openharmony_path

View file

@ -0,0 +1,40 @@
import os
import platform
def add_implementation_envs(env, _app):
# Add requirements to HIERO_PLUGIN_PATH
pype_root = os.environ["OPENPYPE_REPOS_ROOT"]
new_hiero_paths = [
os.path.join(pype_root, "openpype", "hosts", "hiero", "startup")
]
old_hiero_path = env.get("HIERO_PLUGIN_PATH") or ""
for path in old_hiero_path.split(os.pathsep):
if not path or not os.path.exists(path):
continue
norm_path = os.path.normpath(path)
if norm_path not in new_hiero_paths:
new_hiero_paths.append(norm_path)
env["HIERO_PLUGIN_PATH"] = os.pathsep.join(new_hiero_paths)
# Try to add QuickTime to PATH
quick_time_path = "C:/Program Files (x86)/QuickTime/QTSystem"
if platform.system() == "windows" and os.path.exists(quick_time_path):
path_value = env.get("PATH") or ""
path_paths = [
path
for path in path_value.split(os.pathsep)
if path
]
path_paths.append(quick_time_path)
env["PATH"] = os.pathsep.join(path_paths)
# Set default values if are not already set via settings
defaults = {
"LOGLEVEL": "DEBUG"
}
for key, value in defaults.items():
if not env.get(key):
env[key] = value

View file

@ -0,0 +1,38 @@
import os
def add_implementation_envs(env, _app):
# Add requirements to HOUDINI_PATH and HOUDINI_MENU_PATH
pype_root = os.environ["OPENPYPE_REPOS_ROOT"]
startup_path = os.path.join(
pype_root, "openpype", "hosts", "houdini", "startup"
)
new_houdini_path = [startup_path]
new_houdini_menu_path = [startup_path]
old_houdini_path = env.get("HOUDINI_PATH") or ""
old_houdini_menu_path = env.get("HOUDINI_MENU_PATH") or ""
for path in old_houdini_path.split(os.pathsep):
if not path or not os.path.exists(path):
continue
norm_path = os.path.normpath(path)
if norm_path not in new_houdini_path:
new_houdini_path.append(norm_path)
for path in old_houdini_menu_path.split(os.pathsep):
if not path or not os.path.exists(path):
continue
norm_path = os.path.normpath(path)
if norm_path not in new_houdini_menu_path:
new_houdini_menu_path.append(norm_path)
# Add ampersand for unknown reason (Maybe is needed in Houdini?)
new_houdini_path.append("&")
new_houdini_menu_path.append("&")
env["HOUDINI_PATH"] = os.pathsep.join(new_houdini_path)
env["HOUDINI_MENU_PATH"] = os.pathsep.join(new_houdini_menu_path)

View file

@ -0,0 +1,29 @@
import os
def add_implementation_envs(env, _app):
# Add requirements to PYTHONPATH
pype_root = os.environ["OPENPYPE_REPOS_ROOT"]
new_python_paths = [
os.path.join(pype_root, "openpype", "hosts", "maya", "startup"),
os.path.join(pype_root, "repos", "avalon-core", "setup", "maya"),
os.path.join(pype_root, "tools", "mayalookassigner")
]
old_python_path = env.get("PYTHONPATH") or ""
for path in old_python_path.split(os.pathsep):
if not path or not os.path.exists(path):
continue
norm_path = os.path.normpath(path)
if norm_path not in new_python_paths:
new_python_paths.append(norm_path)
env["PYTHONPATH"] = os.pathsep.join(new_python_paths)
# Set default values if are not already set via settings
defaults = {
"OPENPYPE_LOG_NO_COLORS": "Yes"
}
for key, value in defaults.items():
if not env.get(key):
env[key] = value

View file

@ -0,0 +1,43 @@
import os
import platform
def add_implementation_envs(env, _app):
# Add requirements to NUKE_PATH
pype_root = os.environ["OPENPYPE_REPOS_ROOT"]
new_nuke_paths = [
os.path.join(pype_root, "openpype", "hosts", "nuke", "startup"),
os.path.join(
pype_root, "repos", "avalon-core", "setup", "nuke", "nuke_path"
)
]
old_nuke_path = env.get("NUKE_PATH") or ""
for path in old_nuke_path.split(os.pathsep):
if not path or not os.path.exists(path):
continue
norm_path = os.path.normpath(path)
if norm_path not in new_nuke_paths:
new_nuke_paths.append(norm_path)
env["NUKE_PATH"] = os.pathsep.join(new_nuke_paths)
# Try to add QuickTime to PATH
quick_time_path = "C:/Program Files (x86)/QuickTime/QTSystem"
if platform.system() == "windows" and os.path.exists(quick_time_path):
path_value = env.get("PATH") or ""
path_paths = [
path
for path in path_value.split(os.pathsep)
if path
]
path_paths.append(quick_time_path)
env["PATH"] = os.pathsep.join(path_paths)
# Set default values if are not already set via settings
defaults = {
"LOGLEVEL": "DEBUG"
}
for key, value in defaults.items():
if not env.get(key):
env[key] = value

View file

@ -53,11 +53,10 @@ def get_created_node_imageio_setting(**kwarg):
imageio_node = None
for node in imageio_nodes:
log.info(node)
if (node["nukeNodeClass"] != nodeclass) and (
creator not in node["plugins"]):
continue
imageio_node = node
if (nodeclass in node["nukeNodeClass"]) and (
creator in node["plugins"]):
imageio_node = node
break
log.info("ImageIO node: {}".format(imageio_node))
return imageio_node
@ -340,9 +339,9 @@ def create_write_node(name, data, input=None, prenodes=None, review=True):
nuke.message(msg)
# build file path to workfiles
fpath = str(anatomy_filled["work"]["folder"]).replace("\\", "/")
fdir = str(anatomy_filled["work"]["folder"]).replace("\\", "/")
fpath = data["fpath_template"].format(
work=fpath, version=data["version"], subset=data["subset"],
work=fdir, version=data["version"], subset=data["subset"],
frame=data["frame"],
ext=representation
)

View file

@ -0,0 +1,9 @@
def add_implementation_envs(env, _app):
"""Modify environments to contain all required for implementation."""
defaults = {
"OPENPYPE_LOG_NO_COLORS": "True",
"WEBSOCKET_URL": "ws://localhost:8099/ws/"
}
for key, value in defaults.items():
if not env.get(key):
env[key] = value

View file

@ -9,6 +9,7 @@ class CreateImage(openpype.api.Creator):
name = "imageDefault"
label = "Image"
family = "image"
defaults = ["Main"]
def process(self):
groups = []
@ -16,7 +17,9 @@ class CreateImage(openpype.api.Creator):
create_group = False
stub = photoshop.stub()
useSelection = False
if (self.options or {}).get("useSelection"):
useSelection = True
multiple_instances = False
selection = stub.get_selected_layers()
self.log.info("selection {}".format(selection))
@ -61,7 +64,9 @@ class CreateImage(openpype.api.Creator):
# No selection creates an empty group.
create_group = True
else:
create_group = True
stub.select_layers(stub.get_layers())
group = stub.group_selected_layers(self.name)
groups.append(group)
if create_group:
group = stub.create_group(self.name)
@ -77,13 +82,19 @@ class CreateImage(openpype.api.Creator):
group.name = group.name.replace(stub.PUBLISH_ICON, ''). \
replace(stub.LOADED_ICON, '')
if useSelection:
subset_name = self.data["subset"] + group.name
else:
# use value provided by user from Creator
subset_name = self.data["subset"]
if group.long_name:
for directory in group.long_name[::-1]:
name = directory.replace(stub.PUBLISH_ICON, '').\
replace(stub.LOADED_ICON, '')
long_names.append(name)
self.data.update({"subset": "image" + group.name})
self.data.update({"subset": subset_name})
self.data.update({"uuid": str(group.id)})
self.data.update({"long_name": "_".join(long_names)})
stub.imprint(group, self.data)

View file

@ -35,21 +35,16 @@ class ExtractImage(openpype.api.Extractor):
if layer.visible and layer.id not in extract_ids:
stub.set_visible(layer.id, False)
save_options = []
if "png" in self.formats:
save_options.append('png')
if "jpg" in self.formats:
save_options.append('jpg')
file_basename = os.path.splitext(
stub.get_active_document_name()
)[0]
for extension in save_options:
for extension in self.formats:
_filename = "{}.{}".format(file_basename, extension)
files[extension] = _filename
full_filename = os.path.join(staging_dir, _filename)
stub.saveAs(full_filename, extension, True)
self.log.info(f"Extracted: {extension}")
representations = []
for extension, filename in files.items():

View file

@ -6,7 +6,12 @@ from avalon import photoshop
class ExtractReview(openpype.api.Extractor):
"""Produce a flattened image file from all instances."""
"""
Produce a flattened image file from all 'image' instances.
If no 'image' instance is created, it produces flattened image from
all visible layers.
"""
label = "Extract Review"
hosts = ["photoshop"]
@ -30,14 +35,15 @@ class ExtractReview(openpype.api.Extractor):
)
output_image_path = os.path.join(staging_dir, output_image)
with photoshop.maintained_visibility():
# Hide all other layers.
extract_ids = set([ll.id for ll in stub.
get_layers_in_layers(layers)])
self.log.info("extract_ids {}".format(extract_ids))
for layer in stub.get_layers():
# limit unnecessary calls to client
if layer.visible and layer.id not in extract_ids:
stub.set_visible(layer.id, False)
if layers:
# Hide all other layers.
extract_ids = set([ll.id for ll in stub.
get_layers_in_layers(layers)])
self.log.debug("extract_ids {}".format(extract_ids))
for layer in stub.get_layers():
# limit unnecessary calls to client
if layer.visible and layer.id not in extract_ids:
stub.set_visible(layer.id, False)
stub.saveAs(output_image_path, 'jpg', True)

View file

@ -0,0 +1,8 @@
def add_implementation_envs(env, _app):
"""Modify environments to contain all required for implementation."""
defaults = {
"OPENPYPE_LOG_NO_COLORS": "True"
}
for key, value in defaults.items():
if not env.get(key):
env[key] = value

View file

@ -0,0 +1,18 @@
import os
def add_implementation_envs(env, _app):
"""Modify environments to contain all required for implementation."""
# Set AVALON_UNREAL_PLUGIN required for Unreal implementation
unreal_plugin_path = os.path.join(
os.environ["OPENPYPE_REPOS_ROOT"], "repos", "avalon-unreal-integration"
)
env["AVALON_UNREAL_PLUGIN"] = unreal_plugin_path
# Set default environments if are not set via settings
defaults = {
"OPENPYPE_LOG_NO_COLORS": "True"
}
for key, value in defaults.items():
if not env.get(key):
env[key] = value

View file

@ -78,14 +78,14 @@ class ExtractLayout(openpype.api.Extractor):
json_element["transform"] = {
"translation": {
"x": transform.translation.x,
"x": -transform.translation.x,
"y": transform.translation.y,
"z": transform.translation.z
},
"rotation": {
"x": math.radians(transform.rotation.euler().x),
"x": math.radians(transform.rotation.euler().x + 90.0),
"y": math.radians(transform.rotation.euler().y),
"z": math.radians(transform.rotation.euler().z),
"z": math.radians(180.0 - transform.rotation.euler().z)
},
"scale": {
"x": transform.scale3d.x,

View file

@ -58,6 +58,10 @@ from .python_module_tools import (
)
from .avalon_context import (
CURRENT_DOC_SCHEMAS,
PROJECT_NAME_ALLOWED_SYMBOLS,
PROJECT_NAME_REGEX,
create_project,
is_latest,
any_outdated,
get_asset,
@ -163,6 +167,10 @@ __all__ = [
"recursive_bases_from_class",
"classes_from_module",
"CURRENT_DOC_SCHEMAS",
"PROJECT_NAME_ALLOWED_SYMBOLS",
"PROJECT_NAME_REGEX",
"create_project",
"is_latest",
"any_outdated",
"get_asset",

View file

@ -3,7 +3,6 @@ import re
import copy
import json
import platform
import getpass
import collections
import inspect
import subprocess
@ -362,7 +361,6 @@ class ApplicationManager:
context = ApplicationLaunchContext(
app, executable, **data
)
# TODO pass context through launch hooks
return context.launch()
@ -626,7 +624,7 @@ class ApplicationLaunchContext:
# Logger
logger_name = "{}-{}".format(self.__class__.__name__, self.app_name)
self.log = PypeLogger().get_logger(logger_name)
self.log = PypeLogger.get_logger(logger_name)
self.executable = executable
@ -1033,7 +1031,7 @@ def _merge_env(env, current_env):
return result
def prepare_host_environments(data):
def prepare_host_environments(data, implementation_envs=True):
"""Modify launch environments based on launched app and context.
Args:
@ -1089,7 +1087,24 @@ def prepare_host_environments(data):
# Merge dictionaries
env_values = _merge_env(tool_env, env_values)
final_env = _merge_env(acre.compute(env_values), data["env"])
loaded_env = _merge_env(acre.compute(env_values), data["env"])
final_env = None
# Add host specific environments
if app.host_name and implementation_envs:
module = __import__("openpype.hosts", fromlist=[app.host_name])
host_module = getattr(module, app.host_name, None)
add_implementation_envs = None
if host_module:
add_implementation_envs = getattr(
host_module, "add_implementation_envs", None
)
if add_implementation_envs:
# Function may only modify passed dict without returning value
final_env = add_implementation_envs(loaded_env, app)
if final_env is None:
final_env = loaded_env
# Update env
data["env"].update(final_env)

View file

@ -17,6 +17,99 @@ avalon = None
log = logging.getLogger("AvalonContext")
CURRENT_DOC_SCHEMAS = {
"project": "openpype:project-3.0",
"asset": "openpype:asset-3.0",
"config": "openpype:config-2.0"
}
PROJECT_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_"
PROJECT_NAME_REGEX = re.compile(
"^[{}]+$".format(PROJECT_NAME_ALLOWED_SYMBOLS)
)
def create_project(
project_name, project_code, library_project=False, dbcon=None
):
"""Create project using OpenPype settings.
This project creation function is not validating project document on
creation. It is because project document is created blindly with only
minimum required information about project which is it's name, code, type
and schema.
Entered project name must be unique and project must not exist yet.
Args:
project_name(str): New project name. Should be unique.
project_code(str): Project's code should be unique too.
library_project(bool): Project is library project.
dbcon(AvalonMongoDB): Object of connection to MongoDB.
Raises:
ValueError: When project name already exists in MongoDB.
Returns:
dict: Created project document.
"""
from openpype.settings import ProjectSettings, SaveWarningExc
from avalon.api import AvalonMongoDB
from avalon.schema import validate
if dbcon is None:
dbcon = AvalonMongoDB()
if not PROJECT_NAME_REGEX.match(project_name):
raise ValueError((
"Project name \"{}\" contain invalid characters"
).format(project_name))
database = dbcon.database
project_doc = database[project_name].find_one(
{"type": "project"},
{"name": 1}
)
if project_doc:
raise ValueError("Project with name \"{}\" already exists".format(
project_name
))
project_doc = {
"type": "project",
"name": project_name,
"data": {
"code": project_code,
"library_project": library_project
},
"schema": CURRENT_DOC_SCHEMAS["project"]
}
# Insert document with basic data
database[project_name].insert_one(project_doc)
# Load ProjectSettings for the project and save it to store all attributes
# and Anatomy
try:
project_settings_entity = ProjectSettings(project_name)
project_settings_entity.save()
except SaveWarningExc as exc:
print(str(exc))
except Exception:
database[project_name].delete_one({"type": "project"})
raise
project_doc = database[project_name].find_one({"type": "project"})
try:
# Validate created project document
validate(project_doc)
except Exception:
# Remove project if is not valid
database[project_name].delete_one({"type": "project"})
raise
return project_doc
def with_avalon(func):
@functools.wraps(func)
def wrap_avalon(*args, **kwargs):

View file

@ -26,9 +26,7 @@ from openpype.modules.ftrack.lib import (
BaseEvent
)
from openpype.modules.ftrack.lib.avalon_sync import (
EntitySchemas
)
from openpype.lib import CURRENT_DOC_SCHEMAS
class SyncToAvalonEvent(BaseEvent):
@ -1128,7 +1126,7 @@ class SyncToAvalonEvent(BaseEvent):
"_id": mongo_id,
"name": name,
"type": "asset",
"schema": EntitySchemas["asset"],
"schema": CURRENT_DOC_SCHEMAS["asset"],
"parent": proj["_id"],
"data": {
"ftrackId": ftrack_ent["id"],

View file

@ -34,7 +34,7 @@ log = Logger.get_logger(__name__)
# Current schemas for avalon types
EntitySchemas = {
CURRENT_DOC_SCHEMAS = {
"project": "openpype:project-3.0",
"asset": "openpype:asset-3.0",
"config": "openpype:config-2.0"
@ -1862,7 +1862,7 @@ class SyncEntitiesFactory:
item["_id"] = new_id
item["parent"] = self.avalon_project_id
item["schema"] = EntitySchemas["asset"]
item["schema"] = CURRENT_DOC_SCHEMAS["asset"]
item["data"]["visualParent"] = avalon_parent
new_id_str = str(new_id)
@ -2003,8 +2003,8 @@ class SyncEntitiesFactory:
project_item["_id"] = new_id
project_item["parent"] = None
project_item["schema"] = EntitySchemas["project"]
project_item["config"]["schema"] = EntitySchemas["config"]
project_item["schema"] = CURRENT_DOC_SCHEMAS["project"]
project_item["config"]["schema"] = CURRENT_DOC_SCHEMAS["config"]
self.ftrack_avalon_mapper[self.ft_project_id] = new_id
self.avalon_ftrack_mapper[new_id] = self.ft_project_id

View file

@ -170,7 +170,10 @@ class LocalDriveHandler(AbstractProvider):
site=site,
progress=status_val
)
target_file_size = os.path.getsize(target_path)
try:
target_file_size = os.path.getsize(target_path)
except FileNotFoundError:
pass
time.sleep(0.5)
def _normalize_site_name(self, site_name):

View file

@ -501,6 +501,8 @@ class SyncServerModule(PypeModule, ITrayModule):
items = self.get_configurable_items_for_site(project_name,
site_name,
scope)
# Local Settings need 'local' instead of real value
site_name = site_name.replace(get_local_site_id(), 'local')
editable[site_name] = items
return editable
@ -591,8 +593,6 @@ class SyncServerModule(PypeModule, ITrayModule):
else:
item["value"] = val
editable.append(item)
return editable
@ -877,7 +877,7 @@ class SyncServerModule(PypeModule, ITrayModule):
}
all_sites = {self.DEFAULT_SITE: studio_config}
if sync_enabled:
all_sites['local'] = {'provider': 'local_drive'}
all_sites[get_local_site_id()] = {'provider': 'local_drive'}
return all_sites
def get_provider_for_site(self, project_name=None, site=None):

View file

@ -65,12 +65,19 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin):
"username": context.data["user"]
}
app_manager = ApplicationManager()
app_name = os.environ.get("AVALON_APP_NAME")
if app_name:
app = app_manager.applications.get(app_name)
if app:
context_data["app"] = app.host_name
# Use AVALON_APP as first if available it is the same as host name
# - only if is not defined use AVALON_APP_NAME (e.g. on Farm) and
# set it back to AVALON_APP env variable
host_name = os.environ.get("AVALON_APP")
if not host_name:
app_manager = ApplicationManager()
app_name = os.environ.get("AVALON_APP_NAME")
if app_name:
app = app_manager.applications.get(app_name)
if app:
host_name = app.host_name
os.environ["AVALON_APP"] = host_name
context_data["app"] = host_name
datetime_data = context.data.get("datetimeData") or {}
context_data.update(datetime_data)

View file

@ -110,6 +110,12 @@ class PypeCommands:
with open(output_json_path, "w") as file_stream:
json.dump(env, file_stream, indent=4)
@staticmethod
def launch_project_manager():
from openpype.tools import project_manager
project_manager.main()
def texture_copy(self, project, asset, path):
pass

View file

@ -0,0 +1,17 @@
{
"create": {
"CreateImage": {
"defaults": [
"Main"
]
}
},
"publish": {
"ExtractImage": {
"formats": [
"png",
"jpg"
]
}
}
}

View file

@ -16,5 +16,21 @@
"active": true
}
},
"load": {
"LoadImage": {
"defaults": {
"stretch": true,
"timestretch": true,
"preload": true
}
},
"ImportImage": {
"defaults": {
"stretch": true,
"timestretch": true,
"preload": true
}
}
},
"filters": {}
}

View file

@ -5,18 +5,11 @@
"icon": "{}/app_icons/maya.png",
"host_name": "maya",
"environment": {
"PYTHONPATH": [
"{OPENPYPE_REPOS_ROOT}/openpype/hosts/maya/startup",
"{OPENPYPE_REPOS_ROOT}/repos/avalon-core/setup/maya",
"{OPENPYPE_REPOS_ROOT}/repos/maya-look-assigner",
"{PYTHONPATH}"
],
"MAYA_DISABLE_CLIC_IPM": "Yes",
"MAYA_DISABLE_CIP": "Yes",
"MAYA_DISABLE_CER": "Yes",
"PYMEL_SKIP_MEL_INIT": "Yes",
"LC_ALL": "C",
"OPENPYPE_LOG_NO_COLORS": "Yes"
"LC_ALL": "C"
},
"variants": {
"2022": {
@ -110,15 +103,7 @@
"icon": "{}/app_icons/nuke.png",
"host_name": "nuke",
"environment": {
"NUKE_PATH": [
"{OPENPYPE_REPOS_ROOT}/repos/avalon-core/setup/nuke/nuke_path",
"{OPENPYPE_REPOS_ROOT}/openpype/hosts/nuke/startup",
"{OPENPYPE_STUDIO_PLUGINS}/nuke"
],
"PATH": {
"windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}"
},
"LOGLEVEL": "DEBUG"
"NUKE_PATH": "{OPENPYPE_STUDIO_PLUGINS}/nuke"
},
"variants": {
"13-0": {
@ -224,15 +209,7 @@
"icon": "{}/app_icons/nuke.png",
"host_name": "nuke",
"environment": {
"NUKE_PATH": [
"{OPENPYPE_REPOS_ROOT}/repos/avalon-core/setup/nuke/nuke_path",
"{OPENPYPE_REPOS_ROOT}/openpype/hosts/nuke/startup",
"{OPENPYPE_STUDIO_PLUGINS}/nuke"
],
"PATH": {
"windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}"
},
"LOGLEVEL": "DEBUG"
"NUKE_PATH": "{OPENPYPE_STUDIO_PLUGINS}/nuke"
},
"variants": {
"13-0": {
@ -368,15 +345,8 @@
"icon": "{}/app_icons/nuke.png",
"host_name": "hiero",
"environment": {
"HIERO_PLUGIN_PATH": [
"{OPENPYPE_REPOS_ROOT}/openpype/hosts/hiero/startup"
],
"PATH": {
"windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}"
},
"WORKFILES_STARTUP": "0",
"TAG_ASSETBUILD_STARTUP": "0",
"LOGLEVEL": "DEBUG"
"TAG_ASSETBUILD_STARTUP": "0"
},
"variants": {
"13-0": {
@ -510,15 +480,8 @@
"icon": "{}/app_icons/hiero.png",
"host_name": "hiero",
"environment": {
"HIERO_PLUGIN_PATH": [
"{OPENPYPE_REPOS_ROOT}/openpype/hosts/hiero/startup"
],
"PATH": {
"windows": "C:/Program Files (x86)/QuickTime/QTSystem/;{PATH}"
},
"WORKFILES_STARTUP": "0",
"TAG_ASSETBUILD_STARTUP": "0",
"LOGLEVEL": "DEBUG"
"TAG_ASSETBUILD_STARTUP": "0"
},
"variants": {
"13-0": {
@ -783,18 +746,7 @@
"label": "Houdini",
"icon": "{}/app_icons/houdini.png",
"host_name": "houdini",
"environment": {
"HOUDINI_PATH": {
"darwin": "{OPENPYPE_REPOS_ROOT}/openpype/hosts/houdini/startup:&",
"linux": "{OPENPYPE_REPOS_ROOT}/openpype/hosts/houdini/startup:&",
"windows": "{OPENPYPE_REPOS_ROOT}/openpype/hosts/houdini/startup;&"
},
"HOUDINI_MENU_PATH": {
"darwin": "{OPENPYPE_REPOS_ROOT}/openpype/hosts/houdini/startup:&",
"linux": "{OPENPYPE_REPOS_ROOT}/openpype/hosts/houdini/startup:&",
"windows": "{OPENPYPE_REPOS_ROOT}/openpype/hosts/houdini/startup;&"
}
},
"environment": {},
"variants": {
"18-5": {
"use_python_2": true,
@ -852,14 +804,7 @@
"label": "Blender",
"icon": "{}/app_icons/blender.png",
"host_name": "blender",
"environment": {
"BLENDER_USER_SCRIPTS": "{OPENPYPE_REPOS_ROOT}/repos/avalon-core/setup/blender",
"PYTHONPATH": [
"{OPENPYPE_REPOS_ROOT}/repos/avalon-core/setup/blender",
"{PYTHONPATH}"
],
"QT_PREFERRED_BINDING": "PySide2"
},
"environment": {},
"variants": {
"2-83": {
"use_python_2": false,
@ -940,8 +885,7 @@
"icon": "{}/app_icons/harmony.png",
"host_name": "harmony",
"environment": {
"AVALON_HARMONY_WORKFILES_ON_LAUNCH": "1",
"LIB_OPENHARMONY_PATH": "{OPENPYPE_REPOS_ROOT}/pype/vendor/OpenHarmony"
"AVALON_HARMONY_WORKFILES_ON_LAUNCH": "1"
},
"variants": {
"20": {
@ -985,9 +929,7 @@
"label": "TVPaint",
"icon": "{}/app_icons/tvpaint.png",
"host_name": "tvpaint",
"environment": {
"OPENPYPE_LOG_NO_COLORS": "True"
},
"environment": {},
"variants": {
"animation_11-64bits": {
"use_python_2": false,
@ -1034,8 +976,6 @@
"host_name": "photoshop",
"environment": {
"AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH": "1",
"OPENPYPE_LOG_NO_COLORS": "Yes",
"WEBSOCKET_URL": "ws://localhost:8099/ws/",
"WORKFILES_SAVE_AS": "Yes"
},
"variants": {
@ -1084,8 +1024,6 @@
"host_name": "aftereffects",
"environment": {
"AVALON_AFTEREFFECTS_WORKFILES_ON_LAUNCH": "1",
"OPENPYPE_LOG_NO_COLORS": "Yes",
"WEBSOCKET_URL": "ws://localhost:8097/ws/",
"WORKFILES_SAVE_AS": "Yes"
},
"variants": {
@ -1159,10 +1097,7 @@
"label": "Unreal Editor",
"icon": "{}/app_icons/ue4.png'",
"host_name": "unreal",
"environment": {
"AVALON_UNREAL_PLUGIN": "{OPENPYPE_REPOS_ROOT}/repos/avalon-unreal-integration",
"OPENPYPE_LOG_NO_COLORS": "True"
},
"environment": {},
"variants": {
"4-26": {
"use_python_2": false,

View file

@ -57,6 +57,7 @@ from .exceptions import (
SchemaError,
DefaultsNotDefined,
StudioDefaultsNotDefined,
BaseInvalidValueType,
InvalidValueType,
InvalidKeySymbols,
SchemaMissingFileInfo,
@ -96,7 +97,7 @@ from .input_entities import (
PathInput,
RawJsonEntity
)
from .color_entity import ColorEntity
from .enum_entity import (
BaseEnumEntity,
EnumEntity,
@ -115,6 +116,7 @@ from .anatomy_entities import AnatomyEntity
__all__ = (
"DefaultsNotDefined",
"StudioDefaultsNotDefined",
"BaseInvalidValueType",
"InvalidValueType",
"InvalidKeySymbols",
"SchemaMissingFileInfo",
@ -146,6 +148,8 @@ __all__ = (
"PathInput",
"RawJsonEntity",
"ColorEntity",
"BaseEnumEntity",
"EnumEntity",
"AppsEnumEntity",

View file

@ -9,6 +9,7 @@ from .lib import (
)
from .exceptions import (
BaseInvalidValueType,
InvalidValueType,
SchemeGroupHierarchyBug,
EntitySchemaError
@ -377,7 +378,7 @@ class BaseItemEntity(BaseEntity):
try:
new_value = self.convert_to_valid_type(value)
except InvalidValueType:
except BaseInvalidValueType:
new_value = NOT_SET
if new_value is not NOT_SET:
@ -846,6 +847,13 @@ class ItemEntity(BaseItemEntity):
)
raise EntitySchemaError(self, reason)
if self.is_file and self.file_item is not None:
reason = (
"Entity has set `is_file` to true but"
" it's parent is already marked as file item."
)
raise EntitySchemaError(self, reason)
super(ItemEntity, self).schema_validations()
def create_schema_object(self, *args, **kwargs):

View file

@ -0,0 +1,54 @@
from .lib import STRING_TYPE
from .input_entities import InputEntity
from .exceptions import (
BaseInvalidValueType,
InvalidValueType
)
class ColorEntity(InputEntity):
schema_types = ["color"]
def _item_initalization(self):
self.valid_value_types = (list, )
self.value_on_not_set = [0, 0, 0, 255]
def convert_to_valid_type(self, value):
"""Conversion to valid type.
Complexity of entity requires to override BaseEntity implementation.
"""
# Convertion to valid value type `list`
if isinstance(value, (set, tuple)):
value = list(value)
# Skip other validations if is not `list`
if not isinstance(value, list):
raise InvalidValueType(
self.valid_value_types, type(value), self.path
)
# Allow list of len 3 (last aplha is set to max)
if len(value) == 3:
value.append(255)
if len(value) != 4:
reason = "Color entity expect 4 items in list got {}".format(
len(value)
)
raise BaseInvalidValueType(reason, self.path)
new_value = []
for item in value:
if not isinstance(item, int):
if isinstance(item, (STRING_TYPE, float)):
item = int(item)
is_valid = isinstance(item, int) and -1 < item < 256
if not is_valid:
reason = (
"Color entity expect 4 integers in range 0-255 got {}"
).format(value)
raise BaseInvalidValueType(reason, self.path)
new_value.append(item)
return new_value

View file

@ -15,20 +15,22 @@ class StudioDefaultsNotDefined(Exception):
super(StudioDefaultsNotDefined, self).__init__(msg)
class InvalidValueType(Exception):
msg_template = "{}"
class BaseInvalidValueType(Exception):
def __init__(self, reason, path):
msg = "Path \"{}\". {}".format(path, reason)
self.msg = msg
super(BaseInvalidValueType, self).__init__(msg)
class InvalidValueType(BaseInvalidValueType):
def __init__(self, valid_types, invalid_type, path):
msg = "Path \"{}\". ".format(path)
joined_types = ", ".join(
[str(valid_type) for valid_type in valid_types]
)
msg += "Got invalid type \"{}\". Expected: {}".format(
msg = "Got invalid type \"{}\". Expected: {}".format(
invalid_type, joined_types
)
self.msg = msg
super(InvalidValueType, self).__init__(msg)
super(InvalidValueType, self).__init__(msg, path)
class RequiredKeyModified(KeyError):

View file

@ -420,6 +420,18 @@
}
```
### color
- preimplemented entity to store and load color values
- entity store and expect list of 4 integers in range 0-255
- integers represents rgba [Red, Green, Blue, Alpha]
```
{
"type": "color",
"key": "bg_color",
"label": "Background Color"
}
```
## Noninteractive widgets
- have nothing to do with data

View file

@ -82,6 +82,10 @@
"type": "schema",
"name": "schema_project_aftereffects"
},
{
"type": "schema",
"name": "schema_project_photoshop"
},
{
"type": "schema",
"name": "schema_project_harmony"

View file

@ -10,7 +10,6 @@
"collapsible": true,
"key": "publish",
"label": "Publish plugins",
"is_file": true,
"children": [
{
"type": "dict",

View file

@ -603,7 +603,6 @@
"collapsible": true,
"key": "publish",
"label": "Publish plugins",
"is_file": true,
"children": [
{
"type": "dict",

View file

@ -0,0 +1,57 @@
{
"type": "dict",
"collapsible": true,
"key": "photoshop",
"label": "Photoshop",
"is_file": true,
"children": [
{
"type": "dict",
"collapsible": true,
"key": "create",
"label": "Creator plugins",
"children": [
{
"type": "dict",
"collapsible": true,
"key": "CreateImage",
"label": "Create Image",
"children": [
{
"type": "list",
"key": "defaults",
"label": "Default Subsets",
"object_type": "text"
}
]
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "publish",
"label": "Publish plugins",
"children": [
{
"type": "dict",
"collapsible": true,
"key": "ExtractImage",
"label": "Extract Image",
"children": [
{
"type": "label",
"label": "Currently only jpg and png are supported"
},
{
"type": "list",
"key": "formats",
"label": "Extract Formats",
"object_type": "text"
}
]
}
]
}
]
}

View file

@ -11,7 +11,6 @@
"key": "create",
"label": "Creator plugins",
"collapsible_key": true,
"is_file": true,
"object_type": {
"type": "dict",
"children": [
@ -56,7 +55,6 @@
"collapsible": true,
"key": "publish",
"label": "Publish plugins",
"is_file": true,
"children": [
{
"type": "dict",

View file

@ -4,7 +4,6 @@
"label": "Site Sync (beta testing)",
"collapsible": true,
"checkbox_key": "enabled",
"is_file": true,
"children": [
{
"type": "boolean",
@ -44,7 +43,6 @@
"key": "sites",
"label": "Sites",
"collapsible_key": false,
"is_file": true,
"object_type":
{
"type": "dict",

View file

@ -10,7 +10,6 @@
"collapsible": true,
"key": "publish",
"label": "Publish plugins",
"is_file": true,
"children": [
{
"type": "schema_template",
@ -47,6 +46,72 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "load",
"label": "Loader plugins",
"children": [
{
"type": "dict",
"collapsible": true,
"key": "LoadImage",
"label": "Load Image",
"children": [
{
"key": "defaults",
"type": "dict",
"children": [
{
"type": "boolean",
"key": "stretch",
"label": "Stretch"
},
{
"type": "boolean",
"key": "timestretch",
"label": "TimeStretch"
},
{
"type": "boolean",
"key": "preload",
"label": "Preload"
}
]
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "ImportImage",
"label": "Import Image",
"children": [
{
"key": "defaults",
"type": "dict",
"children": [
{
"type": "boolean",
"key": "stretch",
"label": "Stretch"
},
{
"type": "boolean",
"key": "timestretch",
"label": "TimeStretch"
},
{
"type": "boolean",
"key": "preload",
"label": "Preload"
}
]
}
]
}
]
},
{
"type": "schema",
"name": "schema_publish_gui_filter"

View file

@ -228,14 +228,9 @@
]
},
{
"type": "schema_template",
"name": "template_rgba_color",
"template_data": [
{
"label": "Fill Color",
"name": "fill_color"
}
]
"type": "color",
"label": "Fill Color",
"key": "fill_color"
},
{
"key": "line_thickness",
@ -245,14 +240,9 @@
"maximum": 1000
},
{
"type": "schema_template",
"name": "template_rgba_color",
"template_data": [
{
"label": "Line Color",
"name": "line_color"
}
]
"type": "color",
"label": "Line Color",
"key": "line_color"
}
]
}

View file

@ -4,6 +4,11 @@
"type": "dict",
"is_file": true,
"children": [
{
"key": "color",
"label": "Color input",
"type": "color"
},
{
"type": "dict",
"key": "schema_template_exaples",

View file

@ -97,7 +97,6 @@
"key": "sites",
"label": "Sites",
"collapsible_key": false,
"is_file": true,
"object_type":
{
"type": "dict",
@ -156,8 +155,7 @@
},
"is_group": true,
"key": "templates_mapping",
"label": "Templates mapping",
"is_file": true
"label": "Templates mapping"
}
]
},

View file

@ -192,7 +192,7 @@ class App(QtWidgets.QWidget):
for i, (asset, item) in enumerate(asset_nodes.items()):
# Label prefix
prefix = "({}/{})".format(i+1, len(asset_nodes))
prefix = "({}/{})".format(i + 1, len(asset_nodes))
# Assign the first matching look relevant for this asset
# (since assigning multiple to the same nodes makes no sense)
@ -212,18 +212,19 @@ class App(QtWidgets.QWidget):
self.echo("{} Assigning {} to {}\t".format(prefix,
subset_name,
asset))
nodes = item["nodes"]
self.echo("Getting vray proxy nodes ...")
vray_proxies = set(cmds.ls(type="VRayProxy"))
nodes = set(item["nodes"]).difference(vray_proxies)
if cmds.pluginInfo('vrayformaya', query=True, loaded=True):
self.echo("Getting vray proxy nodes ...")
vray_proxies = set(cmds.ls(type="VRayProxy"))
nodes = list(set(item["nodes"]).difference(vray_proxies))
if vray_proxies:
for vp in vray_proxies:
vrayproxy_assign_look(vp, subset_name)
# Assign look
# Assign look
if nodes:
assign_look_by_version([nodes], version_id=version["_id"])
if vray_proxies:
for vp in vray_proxies:
vrayproxy_assign_look(vp, subset_name)
assign_look_by_version(nodes, version_id=version["_id"])
end = time.time()

View file

@ -0,0 +1,10 @@
from .project_manager import (
ProjectManagerWindow,
main
)
__all__ = (
"ProjectManagerWindow",
"main"
)

View file

@ -0,0 +1,5 @@
from project_manager import main
if __name__ == "__main__":
main()

View file

@ -0,0 +1,50 @@
__all__ = (
"IDENTIFIER_ROLE",
"HierarchyView",
"ProjectModel",
"CreateProjectDialog",
"HierarchyModel",
"HierarchySelectionModel",
"BaseItem",
"RootItem",
"ProjectItem",
"AssetItem",
"TaskItem",
"ProjectManagerWindow",
"main"
)
from .constants import (
IDENTIFIER_ROLE
)
from .widgets import CreateProjectDialog
from .view import HierarchyView
from .model import (
ProjectModel,
HierarchyModel,
HierarchySelectionModel,
BaseItem,
RootItem,
ProjectItem,
AssetItem,
TaskItem
)
from .window import ProjectManagerWindow
def main():
import sys
from Qt import QtWidgets
app = QtWidgets.QApplication([])
window = ProjectManagerWindow()
window.show()
sys.exit(app.exec_())

View file

@ -0,0 +1,13 @@
import re
from Qt import QtCore
IDENTIFIER_ROLE = QtCore.Qt.UserRole + 1
DUPLICATED_ROLE = QtCore.Qt.UserRole + 2
HIERARCHY_CHANGE_ABLE_ROLE = QtCore.Qt.UserRole + 3
REMOVED_ROLE = QtCore.Qt.UserRole + 4
ITEM_TYPE_ROLE = QtCore.Qt.UserRole + 5
EDITOR_OPENED_ROLE = QtCore.Qt.UserRole + 6
NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_"
NAME_REGEX = re.compile("^[" + NAME_ALLOWED_SYMBOLS + "]*$")

View file

@ -0,0 +1,159 @@
from Qt import QtWidgets, QtCore
from .widgets import (
NameTextEdit,
FilterComboBox
)
from .multiselection_combobox import MultiSelectionComboBox
class ResizeEditorDelegate(QtWidgets.QStyledItemDelegate):
@staticmethod
def _q_smart_min_size(editor):
min_size_hint = editor.minimumSizeHint()
size_policy = editor.sizePolicy()
width = 0
height = 0
if size_policy.horizontalPolicy() != QtWidgets.QSizePolicy.Ignored:
if (
size_policy.horizontalPolicy()
& QtWidgets.QSizePolicy.ShrinkFlag
):
width = min_size_hint.width()
else:
width = max(
editor.sizeHint().width(),
min_size_hint.width()
)
if size_policy.verticalPolicy() != QtWidgets.QSizePolicy.Ignored:
if size_policy.verticalPolicy() & QtWidgets.QSizePolicy.ShrinkFlag:
height = min_size_hint.height()
else:
height = max(
editor.sizeHint().height(),
min_size_hint.height()
)
output = QtCore.QSize(width, height).boundedTo(editor.maximumSize())
min_size = editor.minimumSize()
if min_size.width() > 0:
output.setWidth(min_size.width())
if min_size.height() > 0:
output.setHeight(min_size.height())
return output.expandedTo(QtCore.QSize(0, 0))
def updateEditorGeometry(self, editor, option, index):
self.initStyleOption(option, index)
option.showDecorationSelected = editor.style().styleHint(
QtWidgets.QStyle.SH_ItemView_ShowDecorationSelected, None, editor
)
widget = option.widget
style = widget.style() if widget else QtWidgets.QApplication.style()
geo = style.subElementRect(
QtWidgets.QStyle.SE_ItemViewItemText, option, widget
)
delta = self._q_smart_min_size(editor).width() - geo.width()
if delta > 0:
if editor.layoutDirection() == QtCore.Qt.RightToLeft:
geo.adjust(-delta, 0, 0, 0)
else:
geo.adjust(0, 0, delta, 0)
editor.setGeometry(geo)
class NumberDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, minimum, maximum, decimals, *args, **kwargs):
super(NumberDelegate, self).__init__(*args, **kwargs)
self.minimum = minimum
self.maximum = maximum
self.decimals = decimals
def createEditor(self, parent, option, index):
if self.decimals > 0:
editor = QtWidgets.QDoubleSpinBox(parent)
else:
editor = QtWidgets.QSpinBox(parent)
editor.setObjectName("NumberEditor")
editor.setMinimum(self.minimum)
editor.setMaximum(self.maximum)
editor.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
value = index.data(QtCore.Qt.EditRole)
if value is not None:
try:
if isinstance(value, str):
value = float(value)
editor.setValue(value)
except Exception:
print("Couldn't set invalid value \"{}\"".format(str(value)))
return editor
class NameDelegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = NameTextEdit(parent)
editor.setObjectName("NameEditor")
value = index.data(QtCore.Qt.EditRole)
if value is not None:
editor.setText(str(value))
return editor
class TypeDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, project_doc_cache, *args, **kwargs):
self._project_doc_cache = project_doc_cache
super(TypeDelegate, self).__init__(*args, **kwargs)
def createEditor(self, parent, option, index):
editor = FilterComboBox(parent)
editor.setObjectName("TypeEditor")
editor.style().polish(editor)
if not self._project_doc_cache.project_doc:
return editor
task_type_defs = self._project_doc_cache.project_doc["config"]["tasks"]
editor.addItems(list(task_type_defs.keys()))
return editor
def setEditorData(self, editor, index):
value = index.data(QtCore.Qt.EditRole)
index = editor.findText(value)
if index >= 0:
editor.setCurrentIndex(index)
def setModelData(self, editor, model, index):
editor.value_cleanup()
super(TypeDelegate, self).setModelData(editor, model, index)
class ToolsDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, tools_cache, *args, **kwargs):
self._tools_cache = tools_cache
super(ToolsDelegate, self).__init__(*args, **kwargs)
def createEditor(self, parent, option, index):
editor = MultiSelectionComboBox(parent)
editor.setObjectName("ToolEditor")
if not self._tools_cache.tools_data:
return editor
for key, label in self._tools_cache.tools_data:
editor.addItem(label, key)
return editor
def setEditorData(self, editor, index):
value = index.data(QtCore.Qt.EditRole)
editor.set_value(value)
def setModelData(self, editor, model, index):
model.setData(index, editor.value(), QtCore.Qt.EditRole)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,215 @@
from Qt import QtCore, QtGui, QtWidgets
class ComboItemDelegate(QtWidgets.QStyledItemDelegate):
"""
Helper styled delegate (mostly based on existing private Qt's
delegate used by the QtWidgets.QComboBox). Used to style the popup like a
list view (e.g windows style).
"""
def paint(self, painter, option, index):
option = QtWidgets.QStyleOptionViewItem(option)
option.showDecorationSelected = True
# option.state &= (
# ~QtWidgets.QStyle.State_HasFocus
# & ~QtWidgets.QStyle.State_MouseOver
# )
super(ComboItemDelegate, self).paint(painter, option, index)
class MultiSelectionComboBox(QtWidgets.QComboBox):
value_changed = QtCore.Signal()
ignored_keys = {
QtCore.Qt.Key_Up,
QtCore.Qt.Key_Down,
QtCore.Qt.Key_PageDown,
QtCore.Qt.Key_PageUp,
QtCore.Qt.Key_Home,
QtCore.Qt.Key_End
}
def __init__(self, parent=None, **kwargs):
super(MultiSelectionComboBox, self).__init__(parent=parent, **kwargs)
self.setObjectName("MultiSelectionComboBox")
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self._popup_is_shown = False
self._block_mouse_release_timer = QtCore.QTimer(self, singleShot=True)
self._initial_mouse_pos = None
self._delegate = ComboItemDelegate(self)
self.setItemDelegate(self._delegate)
def mousePressEvent(self, event):
"""Reimplemented."""
self._popup_is_shown = False
super(MultiSelectionComboBox, self).mousePressEvent(event)
if self._popup_is_shown:
self._initial_mouse_pos = self.mapToGlobal(event.pos())
self._block_mouse_release_timer.start(
QtWidgets.QApplication.doubleClickInterval()
)
def showPopup(self):
"""Reimplemented."""
super(MultiSelectionComboBox, self).showPopup()
view = self.view()
view.installEventFilter(self)
view.viewport().installEventFilter(self)
self._popup_is_shown = True
def hidePopup(self):
"""Reimplemented."""
self.view().removeEventFilter(self)
self.view().viewport().removeEventFilter(self)
self._popup_is_shown = False
self._initial_mouse_pos = None
super(MultiSelectionComboBox, self).hidePopup()
self.view().clearFocus()
def _event_popup_shown(self, obj, event):
if not self._popup_is_shown:
return
current_index = self.view().currentIndex()
model = self.model()
if event.type() == QtCore.QEvent.MouseMove:
if (
self.view().isVisible()
and self._initial_mouse_pos is not None
and self._block_mouse_release_timer.isActive()
):
diff = obj.mapToGlobal(event.pos()) - self._initial_mouse_pos
if diff.manhattanLength() > 9:
self._block_mouse_release_timer.stop()
return
index_flags = current_index.flags()
state = current_index.data(QtCore.Qt.CheckStateRole)
new_state = None
if event.type() == QtCore.QEvent.MouseButtonRelease:
if (
self._block_mouse_release_timer.isActive()
or not current_index.isValid()
or not self.view().isVisible()
or not self.view().rect().contains(event.pos())
or not index_flags & QtCore.Qt.ItemIsSelectable
or not index_flags & QtCore.Qt.ItemIsEnabled
or not index_flags & QtCore.Qt.ItemIsUserCheckable
):
return
if state == QtCore.Qt.Unchecked:
new_state = QtCore.Qt.Checked
else:
new_state = QtCore.Qt.Unchecked
elif event.type() == QtCore.QEvent.KeyPress:
# TODO: handle QtCore.Qt.Key_Enter, Key_Return?
if event.key() == QtCore.Qt.Key_Space:
# toogle the current items check state
if (
index_flags & QtCore.Qt.ItemIsUserCheckable
and index_flags & QtCore.Qt.ItemIsTristate
):
new_state = QtCore.Qt.CheckState((int(state) + 1) % 3)
elif index_flags & QtCore.Qt.ItemIsUserCheckable:
if state != QtCore.Qt.Checked:
new_state = QtCore.Qt.Checked
else:
new_state = QtCore.Qt.Unchecked
if new_state is not None:
model.setData(current_index, new_state, QtCore.Qt.CheckStateRole)
self.view().update(current_index)
self.value_changed.emit()
return True
def eventFilter(self, obj, event):
"""Reimplemented."""
result = self._event_popup_shown(obj, event)
if result is not None:
return result
return super(MultiSelectionComboBox, self).eventFilter(obj, event)
def addItem(self, *args, **kwargs):
idx = self.count()
super(MultiSelectionComboBox, self).addItem(*args, **kwargs)
self.model().item(idx).setCheckable(True)
def paintEvent(self, event):
"""Reimplemented."""
painter = QtWidgets.QStylePainter(self)
option = QtWidgets.QStyleOptionComboBox()
self.initStyleOption(option)
painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, option)
# draw the icon and text
items = self.checked_items_text()
if not items:
return
text_rect = self.style().subControlRect(
QtWidgets.QStyle.CC_ComboBox,
option,
QtWidgets.QStyle.SC_ComboBoxEditField
)
text = ", ".join(items)
new_text = self.fontMetrics().elidedText(
text, QtCore.Qt.ElideRight, text_rect.width()
)
painter.drawText(
text_rect,
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
new_text
)
def setItemCheckState(self, index, state):
self.setItemData(index, state, QtCore.Qt.CheckStateRole)
def set_value(self, values):
for idx in range(self.count()):
value = self.itemData(idx, role=QtCore.Qt.UserRole)
if value in values:
check_state = QtCore.Qt.Checked
else:
check_state = QtCore.Qt.Unchecked
self.setItemData(idx, check_state, QtCore.Qt.CheckStateRole)
def value(self):
items = list()
for idx in range(self.count()):
state = self.itemData(idx, role=QtCore.Qt.CheckStateRole)
if state == QtCore.Qt.Checked:
items.append(
self.itemData(idx, role=QtCore.Qt.UserRole)
)
return items
def checked_items_text(self):
items = list()
for idx in range(self.count()):
state = self.itemData(idx, role=QtCore.Qt.CheckStateRole)
if state == QtCore.Qt.Checked:
items.append(self.itemText(idx))
return items
def wheelEvent(self, event):
event.ignore()
def keyPressEvent(self, event):
if (
event.key() == QtCore.Qt.Key_Down
and event.modifiers() & QtCore.Qt.AltModifier
):
return self.showPopup()
if event.key() in self.ignored_keys:
return event.ignore()
return super(MultiSelectionComboBox, self).keyPressEvent(event)

View file

@ -0,0 +1,98 @@
import os
from openpype import resources
from avalon.vendor import qtawesome
class ResourceCache:
colors = {
"standard": "#333333",
"new": "#2d9a4c",
"warning": "#c83232"
}
icons = None
@classmethod
def get_icon(cls, *keys):
output = cls.get_icons()
for key in keys:
output = output[key]
return output
@classmethod
def get_icons(cls):
if cls.icons is None:
cls.icons = {
"asset": {
"default": qtawesome.icon(
"fa.folder",
color=cls.colors["standard"]
),
"new": qtawesome.icon(
"fa.folder",
color=cls.colors["new"]
),
"invalid": qtawesome.icon(
"fa.exclamation-triangle",
color=cls.colors["warning"]
),
"removed": qtawesome.icon(
"fa.trash",
color=cls.colors["warning"]
)
},
"task": {
"default": qtawesome.icon(
"fa.check-circle-o",
color=cls.colors["standard"]
),
"new": qtawesome.icon(
"fa.check-circle",
color=cls.colors["new"]
),
"invalid": qtawesome.icon(
"fa.exclamation-circle",
color=cls.colors["warning"]
),
"removed": qtawesome.icon(
"fa.trash",
color=cls.colors["warning"]
)
},
"refresh": qtawesome.icon(
"fa.refresh",
color=cls.colors["standard"]
)
}
return cls.icons
@classmethod
def get_color(cls, color_name):
return cls.colors[color_name]
@classmethod
def style_fill_data(cls):
output = {}
for color_name, color_value in cls.colors.items():
key = "color:{}".format(color_name)
output[key] = color_value
return output
def load_stylesheet():
from . import qrc_resources
qrc_resources.qInitResources()
current_dir = os.path.dirname(os.path.abspath(__file__))
style_path = os.path.join(current_dir, "style.css")
with open(style_path, "r") as style_file:
stylesheet = style_file.read()
for key, value in ResourceCache.style_fill_data().items():
replacement_key = "{" + key + "}"
stylesheet = stylesheet.replace(replacement_key, value)
return stylesheet
def app_icon_path():
return resources.pype_icon_filepath()

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

View file

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
# Resource object code
#
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2)
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore
qt_resource_data = b"\
\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\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\
"
qt_resource_name = b"\
\x00\x08\
\x06\xc5\x8e\xa5\
\x00\x6f\
\x00\x70\x00\x65\x00\x6e\x00\x70\x00\x79\x00\x70\x00\x65\
\x00\x06\
\x07\x03\x7d\xc3\
\x00\x69\
\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\
\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\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\
"
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\x02\x00\x00\x00\x03\
\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x00\x52\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa9\
"
qt_resource_struct_v2 = 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\x02\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x28\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x76\x41\x9d\xa2\x35\
\x00\x00\x00\x52\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa9\
\x00\x00\x01\x76\x41\x9d\xa2\x35\
"
qt_version = [int(v) for v in QtCore.qVersion().split('.')]
if qt_version < [5, 8, 0]:
rcc_version = 1
qt_resource_struct = qt_resource_struct_v1
else:
rcc_version = 2
qt_resource_struct = qt_resource_struct_v2
def qInitResources():
QtCore.qRegisterResourceData(
rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data
)
def qCleanupResources():
QtCore.qUnregisterResourceData(
rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data
)

View file

@ -0,0 +1,84 @@
# Resource object code (Python 3)
# Created by: object code
# Created by: The Resource Compiler for Qt version 5.15.2
# WARNING! All changes made in this file will be lost!
from PySide2 import QtCore
qt_resource_data = b"\
\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;\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"\
\x00\x08\
\x06\xc5\x8e\xa5\
\x00o\
\x00p\x00e\x00n\x00p\x00y\x00p\x00e\
\x00\x06\
\x07\x03}\xc3\
\x00i\
\x00m\x00a\x00g\x00e\x00s\
\x00\x12\
\x01.\x03'\
\x00c\
\x00o\x00m\x00b\x00o\x00b\x00o\x00x\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\
"
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\x02\x00\x00\x00\x03\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00(\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01vA\x9d\xa25\
\x00\x00\x00R\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa9\
\x00\x00\x01vA\x9d\xa25\
"
def qInitResources():
QtCore.qRegisterResourceData(
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
)
def qCleanupResources():
QtCore.qUnregisterResourceData(
0x03, qt_resource_struct, qt_resource_name, qt_resource_data
)

View file

@ -0,0 +1,32 @@
import Qt
initialized = False
resources = None
if Qt.__binding__ == "PySide2":
from . import pyside2_resources as resources
elif Qt.__binding__ == "PyQt5":
from . import pyqt5_resources as resources
def qInitResources():
global resources
global initialized
if resources is not None and not initialized:
initialized = True
resources.qInitResources()
def qCleanupResources():
global resources
global initialized
if resources is not None:
initialized = False
resources.qCleanupResources()
__all__ = (
"resources",
"qInitResources",
"qCleanupResources"
)

View file

@ -0,0 +1,6 @@
<RCC>
<qresource prefix="/openpype">
<file>images/combobox_arrow.png</file>
<file>images/combobox_arrow_disabled.png</file>
</qresource>
</RCC>

View file

@ -0,0 +1,21 @@
QTreeView::item {
padding-top: 3px;
padding-bottom: 3px;
padding-right: 3px;
}
QTreeView::item:selected, QTreeView::item:selected:!active {
background: rgba(0, 122, 204, 127);
color: black;
}
#RefreshBtn {
padding: 2px;
}
#TypeEditor, #ToolEditor, #NameEditor, #NumberEditor {
background: transparent;
border: 1px solid #005c99;
border-radius: 0.3em;
}

View file

@ -0,0 +1,643 @@
import collections
from queue import Queue
from Qt import QtWidgets, QtCore, QtGui
from .delegates import (
NumberDelegate,
NameDelegate,
TypeDelegate,
ToolsDelegate
)
from openpype.lib import ApplicationManager
from .constants import (
REMOVED_ROLE,
IDENTIFIER_ROLE,
ITEM_TYPE_ROLE,
HIERARCHY_CHANGE_ABLE_ROLE,
EDITOR_OPENED_ROLE
)
class NameDef:
pass
class NumberDef:
def __init__(self, minimum=None, maximum=None, decimals=None):
self.minimum = 0 if minimum is None else minimum
self.maximum = 999999 if maximum is None else maximum
self.decimals = 0 if decimals is None else decimals
class TypeDef:
pass
class ToolsDef:
pass
class ProjectDocCache:
def __init__(self, dbcon):
self.dbcon = dbcon
self.project_doc = None
def set_project(self, project_name):
self.project_doc = None
if not project_name:
return
self.project_doc = self.dbcon.database[project_name].find_one(
{"type": "project"}
)
class ToolsCache:
def __init__(self):
self.tools_data = []
def refresh(self):
app_manager = ApplicationManager()
tools_data = []
for tool_name, tool in app_manager.tools.items():
tools_data.append(
(tool_name, tool.label)
)
self.tools_data = tools_data
class HierarchyView(QtWidgets.QTreeView):
"""A tree view that deselects on clicking on an empty area in the view"""
column_delegate_defs = {
"name": NameDef(),
"type": TypeDef(),
"frameStart": NumberDef(1),
"frameEnd": NumberDef(1),
"fps": NumberDef(1, decimals=2),
"resolutionWidth": NumberDef(0),
"resolutionHeight": NumberDef(0),
"handleStart": NumberDef(0),
"handleEnd": NumberDef(0),
"clipIn": NumberDef(1),
"clipOut": NumberDef(1),
"pixelAspect": NumberDef(0, decimals=2),
"tools_env": ToolsDef()
}
columns_sizes = {
"default": {
"stretch": QtWidgets.QHeaderView.ResizeToContents
},
"name": {
"stretch": QtWidgets.QHeaderView.Stretch
},
"type": {
"stretch": QtWidgets.QHeaderView.Interactive,
"width": 100
},
"tools_env": {
"stretch": QtWidgets.QHeaderView.Interactive,
"width": 140
},
"pixelAspect": {
"stretch": QtWidgets.QHeaderView.Interactive,
"width": 80
}
}
persistent_columns = {
"type",
"frameStart",
"frameEnd",
"fps",
"resolutionWidth",
"resolutionHeight",
"handleStart",
"handleEnd",
"clipIn",
"clipOut",
"pixelAspect",
"tools_env"
}
def __init__(self, dbcon, source_model, parent):
super(HierarchyView, self).__init__(parent)
# Direct access to model
self._source_model = source_model
self._editors_mapping = {}
self._persisten_editors = set()
# Access to parent because of `show_message` method
self._parent = parent
project_doc_cache = ProjectDocCache(dbcon)
tools_cache = ToolsCache()
main_delegate = QtWidgets.QStyledItemDelegate()
self.setItemDelegate(main_delegate)
self.setAlternatingRowColors(True)
self.setSelectionMode(HierarchyView.ExtendedSelection)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
column_delegates = {}
column_key_to_index = {}
for key, item_type in self.column_delegate_defs.items():
if isinstance(item_type, NameDef):
delegate = NameDelegate()
elif isinstance(item_type, NumberDef):
delegate = NumberDelegate(
item_type.minimum,
item_type.maximum,
item_type.decimals
)
elif isinstance(item_type, TypeDef):
delegate = TypeDelegate(project_doc_cache)
elif isinstance(item_type, ToolsDef):
delegate = ToolsDelegate(tools_cache)
column = self._source_model.columns.index(key)
self.setItemDelegateForColumn(column, delegate)
column_delegates[key] = delegate
column_key_to_index[key] = column
source_model.index_moved.connect(self._on_rows_moved)
self.customContextMenuRequested.connect(self._on_context_menu)
self._source_model.project_changed.connect(self._on_project_reset)
self._project_doc_cache = project_doc_cache
self._tools_cache = tools_cache
self._delegate = main_delegate
self._column_delegates = column_delegates
self._column_key_to_index = column_key_to_index
def header_init(self):
header = self.header()
header.setStretchLastSection(False)
default_behavior = self.columns_sizes["default"]
widths_by_idx = {}
for idx in range(header.count()):
key = self._source_model.columns[idx]
behavior = self.columns_sizes.get(key, default_behavior)
logical_index = header.logicalIndex(idx)
stretch = behavior["stretch"]
header.setSectionResizeMode(logical_index, stretch)
width = behavior.get("width")
if width is not None:
widths_by_idx[idx] = width
for idx, width in widths_by_idx.items():
self.setColumnWidth(idx, width)
def set_project(self, project_name):
# Trigger helpers first
self._project_doc_cache.set_project(project_name)
self._tools_cache.refresh()
# Trigger update of model after all data for delegates are filled
self._source_model.set_project(project_name)
def _on_project_reset(self):
self.header_init()
self.collapseAll()
project_item = self._source_model.project_item
if project_item:
index = self._source_model.index_for_item(project_item)
self.expand(index)
def _on_rows_moved(self, index):
parent_index = index.parent()
if not self.isExpanded(parent_index):
self.expand(parent_index)
def commitData(self, editor):
super(HierarchyView, self).commitData(editor)
current_index = self.currentIndex()
column = current_index.column()
row = current_index.row()
skipped_index = None
# Change column from "type" to "name"
if column == 1:
new_index = self._source_model.index(
current_index.row(),
0,
current_index.parent()
)
self.setCurrentIndex(new_index)
elif column > 0:
indexes = []
for index in self.selectedIndexes():
if index.column() == column:
if index.row() == row:
skipped_index = index
else:
indexes.append(index)
if skipped_index is not None:
value = current_index.data(QtCore.Qt.EditRole)
for index in indexes:
index.model().setData(index, value, QtCore.Qt.EditRole)
# Update children data
self.updateEditorData()
def _deselect_editor(self, editor):
if editor:
if isinstance(
editor, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox)
):
line_edit = editor.findChild(QtWidgets.QLineEdit)
line_edit.deselect()
elif isinstance(editor, QtWidgets.QLineEdit):
editor.deselect()
def edit(self, index, *args, **kwargs):
result = super(HierarchyView, self).edit(index, *args, **kwargs)
if result:
# Mark index to not return text for DisplayRole
editor = self.indexWidget(index)
if (
editor not in self._persisten_editors
and editor not in self._editors_mapping
):
self._editors_mapping[editor] = index
self._source_model.setData(index, True, EDITOR_OPENED_ROLE)
# Deselect content of editor
# QUESTION not sure if we want do this all the time
self._deselect_editor(editor)
return result
def closeEditor(self, editor, hint):
if (
editor not in self._persisten_editors
and editor in self._editors_mapping
):
index = self._editors_mapping.pop(editor)
self._source_model.setData(index, False, EDITOR_OPENED_ROLE)
super(HierarchyView, self).closeEditor(editor, hint)
def openPersistentEditor(self, index):
self._source_model.setData(index, True, EDITOR_OPENED_ROLE)
super(HierarchyView, self).openPersistentEditor(index)
editor = self.indexWidget(index)
self._persisten_editors.add(editor)
self._deselect_editor(editor)
def closePersistentEditor(self, index):
self._source_model.setData(index, False, EDITOR_OPENED_ROLE)
editor = self.indexWidget(index)
self._persisten_editors.remove(editor)
super(HierarchyView, self).closePersistentEditor(index)
def rowsInserted(self, parent_index, start, end):
super(HierarchyView, self).rowsInserted(parent_index, start, end)
for row in range(start, end + 1):
for key, column in self._column_key_to_index.items():
if key not in self.persistent_columns:
continue
col_index = self._source_model.index(row, column, parent_index)
if bool(
self._source_model.flags(col_index)
& QtCore.Qt.ItemIsEditable
):
self.openPersistentEditor(col_index)
# Expand parent on insert
if not self.isExpanded(parent_index):
self.expand(parent_index)
def mousePressEvent(self, event):
index = self.indexAt(event.pos())
if not index.isValid():
# clear the selection
self.clearSelection()
# clear the current index
self.setCurrentIndex(QtCore.QModelIndex())
super(HierarchyView, self).mousePressEvent(event)
def keyPressEvent(self, event):
call_super = False
if event.key() == QtCore.Qt.Key_Delete:
self._delete_items()
elif event.matches(QtGui.QKeySequence.Copy):
self._copy_items()
elif event.matches(QtGui.QKeySequence.Paste):
self._paste_items()
elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
mdfs = event.modifiers()
if mdfs == (QtCore.Qt.ShiftModifier | QtCore.Qt.ControlModifier):
self._on_ctrl_shift_enter_pressed()
elif mdfs == QtCore.Qt.ShiftModifier:
self._on_shift_enter_pressed()
else:
if self.state() == HierarchyView.NoState:
self._on_enter_pressed()
elif event.modifiers() == QtCore.Qt.ControlModifier:
if event.key() == QtCore.Qt.Key_Left:
self._on_left_ctrl_pressed()
elif event.key() == QtCore.Qt.Key_Right:
self._on_right_ctrl_pressed()
elif event.key() == QtCore.Qt.Key_Up:
self._on_up_ctrl_pressed()
elif event.key() == QtCore.Qt.Key_Down:
self._on_down_ctrl_pressed()
else:
call_super = True
if call_super:
super(HierarchyView, self).keyPressEvent(event)
else:
event.accept()
def _copy_items(self, indexes=None):
try:
if indexes is None:
indexes = self.selectedIndexes()
mime_data = self._source_model.copy_mime_data(indexes)
QtWidgets.QApplication.clipboard().setMimeData(mime_data)
self._show_message("Tasks copied")
except ValueError as exc:
self._show_message(str(exc))
def _paste_items(self):
index = self.currentIndex()
mime_data = QtWidgets.QApplication.clipboard().mimeData()
self._source_model.paste_mime_data(index, mime_data)
def _delete_items(self, indexes=None):
if indexes is None:
indexes = self.selectedIndexes()
self._source_model.delete_indexes(indexes)
def _on_ctrl_shift_enter_pressed(self):
self._add_task_and_edit()
def add_asset(self, parent_index=None):
if parent_index is None:
parent_index = self.currentIndex()
if not parent_index.isValid():
return
# Stop editing
self.setState(HierarchyView.NoState)
QtWidgets.QApplication.processEvents()
return self._source_model.add_new_asset(parent_index)
def add_task(self, parent_index=None):
if parent_index is None:
parent_index = self.currentIndex()
if not parent_index.isValid():
return
return self._source_model.add_new_task(parent_index)
def _add_asset_and_edit(self, parent_index=None):
new_index = self.add_asset(parent_index)
if new_index is None:
return
# Change current index
self.selectionModel().setCurrentIndex(
new_index,
QtCore.QItemSelectionModel.Clear
| QtCore.QItemSelectionModel.Select
)
# Start editing
self.edit(new_index)
def _add_task_and_edit(self):
new_index = self.add_task()
if new_index is None:
return
# Stop editing
self.setState(HierarchyView.NoState)
QtWidgets.QApplication.processEvents()
# TODO change hardcoded column index to coded
task_type_index = self._source_model.index(
new_index.row(), 1, new_index.parent()
)
# Change current index
self.selectionModel().setCurrentIndex(
task_type_index,
QtCore.QItemSelectionModel.Clear
| QtCore.QItemSelectionModel.Select
)
# Start editing
self.edit(task_type_index)
def _on_shift_enter_pressed(self):
parent_index = self.currentIndex()
if not parent_index.isValid():
return
if parent_index.data(ITEM_TYPE_ROLE) == "asset":
parent_index = parent_index.parent()
self._add_asset_and_edit(parent_index)
def _on_up_ctrl_pressed(self):
indexes = self.selectedIndexes()
self._source_model.move_vertical(indexes, -1)
def _on_down_ctrl_pressed(self):
indexes = self.selectedIndexes()
self._source_model.move_vertical(indexes, 1)
def _on_left_ctrl_pressed(self):
indexes = self.selectedIndexes()
self._source_model.move_horizontal(indexes, -1)
def _on_right_ctrl_pressed(self):
indexes = self.selectedIndexes()
self._source_model.move_horizontal(indexes, 1)
def _on_enter_pressed(self):
index = self.currentIndex()
if (
index.isValid()
and index.flags() & QtCore.Qt.ItemIsEditable
):
self.edit(index)
def _remove_delete_flag(self, item_ids):
"""Remove deletion flag on items marked for deletion."""
self._source_model.remove_delete_flag(item_ids)
def _expand_items(self, indexes):
"""Expand multiple items with all it's children.
Args:
indexes (list): List of QModelIndex that should be expanded.
"""
process_queue = Queue()
for index in indexes:
if index.column() == 0:
process_queue.put(index)
item_ids = set()
# Use deque as expanding not visible items as first is faster
indexes_deque = collections.deque()
while not process_queue.empty():
index = process_queue.get()
item_id = index.data(IDENTIFIER_ROLE)
if item_id in item_ids:
continue
item_ids.add(item_id)
indexes_deque.append(index)
for row in range(self._source_model.rowCount(index)):
process_queue.put(self._source_model.index(
row, 0, index
))
while indexes_deque:
self.expand(indexes_deque.pop())
def _collapse_items(self, indexes):
"""Collapse multiple items with all it's children.
Args:
indexes (list): List of QModelIndex that should be collapsed.
"""
item_ids = set()
process_queue = Queue()
for index in indexes:
if index.column() == 0:
process_queue.put(index)
while not process_queue.empty():
index = process_queue.get()
item_id = index.data(IDENTIFIER_ROLE)
if item_id in item_ids:
continue
item_ids.add(item_id)
self.collapse(index)
for row in range(self._source_model.rowCount(index)):
process_queue.put(self._source_model.index(
row, 0, index
))
def _show_message(self, message):
"""Show message to user."""
self._parent.show_message(message)
def _on_context_menu(self, point):
"""Context menu on right click.
Currently is menu shown only on "name" column.
"""
index = self.indexAt(point)
column = index.column()
if column != 0:
return
actions = []
context_menu = QtWidgets.QMenu(self)
indexes = self.selectedIndexes()
items_by_id = {}
for index in indexes:
if index.column() != column:
continue
item_id = index.data(IDENTIFIER_ROLE)
items_by_id[item_id] = self._source_model.items_by_id[item_id]
item_ids = tuple(items_by_id.keys())
if len(item_ids) == 1:
item = items_by_id[item_ids[0]]
item_type = item.data(ITEM_TYPE_ROLE)
if item_type in ("asset", "project"):
add_asset_action = QtWidgets.QAction("Add Asset", context_menu)
add_asset_action.triggered.connect(
self._add_asset_and_edit
)
actions.append(add_asset_action)
if item_type in ("asset", "task"):
add_task_action = QtWidgets.QAction("Add Task", context_menu)
add_task_action.triggered.connect(
self._add_task_and_edit
)
actions.append(add_task_action)
# Remove delete tag on items
removed_item_ids = []
show_delete_items = False
for item_id, item in items_by_id.items():
if item.data(REMOVED_ROLE):
removed_item_ids.append(item_id)
elif (
not show_delete_items
and item.data(ITEM_TYPE_ROLE) != "project"
and item.data(HIERARCHY_CHANGE_ABLE_ROLE)
):
show_delete_items = True
if show_delete_items:
action = QtWidgets.QAction("Delete items", context_menu)
action.triggered.connect(
lambda: self._delete_items()
)
actions.append(action)
if removed_item_ids:
action = QtWidgets.QAction("Keep items", context_menu)
action.triggered.connect(
lambda: self._remove_delete_flag(removed_item_ids)
)
actions.append(action)
# Collapse/Expand action
show_collapse_expand_action = False
for item_id in item_ids:
item = items_by_id[item_id]
item_type = item.data(ITEM_TYPE_ROLE)
if item_type != "task":
show_collapse_expand_action = True
break
if show_collapse_expand_action:
expand_action = QtWidgets.QAction("Expand all", context_menu)
collapse_action = QtWidgets.QAction("Collapse all", context_menu)
expand_action.triggered.connect(
lambda: self._expand_items(indexes)
)
collapse_action.triggered.connect(
lambda: self._collapse_items(indexes)
)
actions.append(expand_action)
actions.append(collapse_action)
if not actions:
return
for action in actions:
context_menu.addAction(action)
global_point = self.viewport().mapToGlobal(point)
context_menu.exec_(global_point)

View file

@ -0,0 +1,281 @@
import re
from .constants import (
NAME_ALLOWED_SYMBOLS,
NAME_REGEX
)
from openpype.lib import (
create_project,
PROJECT_NAME_ALLOWED_SYMBOLS,
PROJECT_NAME_REGEX
)
from avalon.api import AvalonMongoDB
from Qt import QtWidgets, QtCore
class NameTextEdit(QtWidgets.QLineEdit):
def __init__(self, *args, **kwargs):
super(NameTextEdit, self).__init__(*args, **kwargs)
self.textChanged.connect(self._on_text_change)
def _on_text_change(self, text):
if NAME_REGEX.match(text):
return
idx = self.cursorPosition()
before_text = text[0:idx]
after_text = text[idx:len(text)]
sub_regex = "[^{}]+".format(NAME_ALLOWED_SYMBOLS)
new_before_text = re.sub(sub_regex, "", before_text)
new_after_text = re.sub(sub_regex, "", after_text)
idx -= (len(before_text) - len(new_before_text))
self.setText(new_before_text + new_after_text)
self.setCursorPosition(idx)
class FilterComboBox(QtWidgets.QComboBox):
def __init__(self, parent=None):
super(FilterComboBox, self).__init__(parent)
self._last_value = None
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setEditable(True)
filter_proxy_model = QtCore.QSortFilterProxyModel(self)
filter_proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
filter_proxy_model.setSourceModel(self.model())
completer = QtWidgets.QCompleter(filter_proxy_model, self)
completer.setCompletionMode(
QtWidgets.QCompleter.UnfilteredPopupCompletion
)
self.setCompleter(completer)
self.lineEdit().textEdited.connect(
filter_proxy_model.setFilterFixedString
)
completer.activated.connect(self.on_completer_activated)
self._completer = completer
self._filter_proxy_model = filter_proxy_model
def focusInEvent(self, event):
super(FilterComboBox, self).focusInEvent(event)
self._last_value = self.lineEdit().text()
self.lineEdit().selectAll()
def value_cleanup(self):
text = self.lineEdit().text()
idx = self.findText(text)
if idx < 0:
count = self._completer.completionModel().rowCount()
if count > 0:
index = self._completer.completionModel().index(0, 0)
text = index.data(QtCore.Qt.DisplayRole)
idx = self.findText(text)
elif self._last_value is not None:
idx = self.findText(self._last_value)
if idx < 0:
idx = 0
self.setCurrentIndex(idx)
def on_completer_activated(self, text):
if text:
index = self.findText(text)
self.setCurrentIndex(index)
def keyPressEvent(self, event):
if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
self.value_cleanup()
super(FilterComboBox, self).keyPressEvent(event)
def setModel(self, model):
super(FilterComboBox, self).setModel(model)
self._filter_proxy_model.setSourceModel(model)
self._completer.setModel(self._filter_proxy_model)
def setModelColumn(self, column):
self._completer.setCompletionColumn(column)
self._filter_proxy_model.setFilterKeyColumn(column)
super(FilterComboBox, self).setModelColumn(column)
class CreateProjectDialog(QtWidgets.QDialog):
def __init__(self, parent=None, dbcon=None):
super(CreateProjectDialog, self).__init__(parent)
self.setWindowTitle("Create Project")
self.allowed_regex = "[^{}]+".format(PROJECT_NAME_ALLOWED_SYMBOLS)
if dbcon is None:
dbcon = AvalonMongoDB()
self.dbcon = dbcon
self._ignore_code_change = False
self._project_name_is_valid = False
self._project_code_is_valid = False
self._project_code_value = None
project_names, project_codes = self._get_existing_projects()
inputs_widget = QtWidgets.QWidget(self)
project_name_input = QtWidgets.QLineEdit(inputs_widget)
project_code_input = QtWidgets.QLineEdit(inputs_widget)
library_project_input = QtWidgets.QCheckBox(inputs_widget)
inputs_layout = QtWidgets.QFormLayout(inputs_widget)
inputs_layout.setContentsMargins(0, 0, 0, 0)
inputs_layout.addRow("Project name:", project_name_input)
inputs_layout.addRow("Project code:", project_code_input)
inputs_layout.addRow("Library project:", library_project_input)
project_name_label = QtWidgets.QLabel(self)
project_code_label = QtWidgets.QLabel(self)
btns_widget = QtWidgets.QWidget(self)
ok_btn = QtWidgets.QPushButton("Ok", btns_widget)
ok_btn.setEnabled(False)
cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget)
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
btns_layout.setContentsMargins(0, 0, 0, 0)
btns_layout.addStretch(1)
btns_layout.addWidget(ok_btn)
btns_layout.addWidget(cancel_btn)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(inputs_widget, 0)
main_layout.addWidget(project_name_label, 1)
main_layout.addWidget(project_code_label, 1)
main_layout.addStretch(1)
main_layout.addWidget(btns_widget, 0)
project_name_input.textChanged.connect(self._on_project_name_change)
project_code_input.textChanged.connect(self._on_project_code_change)
ok_btn.clicked.connect(self._on_ok_clicked)
cancel_btn.clicked.connect(self._on_cancel_clicked)
self.invalid_project_names = project_names
self.invalid_project_codes = project_codes
self.project_name_label = project_name_label
self.project_code_label = project_code_label
self.project_name_input = project_name_input
self.project_code_input = project_code_input
self.library_project_input = library_project_input
self.ok_btn = ok_btn
@property
def project_name(self):
return self.project_name_input.text()
def _on_project_name_change(self, value):
if self._project_code_value is None:
self._ignore_code_change = True
self.project_code_input.setText(value.lower())
self._ignore_code_change = False
self._update_valid_project_name(value)
def _on_project_code_change(self, value):
if not value:
value = None
self._update_valid_project_code(value)
if not self._ignore_code_change:
self._project_code_value = value
def _update_valid_project_name(self, value):
message = ""
is_valid = True
if not value:
message = "Project name is empty"
is_valid = False
elif value in self.invalid_project_names:
message = "Project name \"{}\" already exist".format(value)
is_valid = False
elif not PROJECT_NAME_REGEX.match(value):
message = (
"Project name \"{}\" contain not supported symbols"
).format(value)
is_valid = False
self._project_name_is_valid = is_valid
self.project_name_label.setText(message)
self.project_name_label.setVisible(bool(message))
self._enable_button()
def _update_valid_project_code(self, value):
message = ""
is_valid = True
if not value:
message = "Project code is empty"
is_valid = False
elif value in self.invalid_project_names:
message = "Project code \"{}\" already exist".format(value)
is_valid = False
elif not PROJECT_NAME_REGEX.match(value):
message = (
"Project code \"{}\" contain not supported symbols"
).format(value)
is_valid = False
self._project_code_is_valid = is_valid
self.project_code_label.setText(message)
self._enable_button()
def _enable_button(self):
self.ok_btn.setEnabled(
self._project_name_is_valid and self._project_code_is_valid
)
def _on_cancel_clicked(self):
self.done(0)
def _on_ok_clicked(self):
if not self._project_name_is_valid or not self._project_code_is_valid:
return
project_name = self.project_name_input.text()
project_code = self.project_code_input.text()
library_project = self.library_project_input.isChecked()
create_project(project_name, project_code, library_project, self.dbcon)
self.done(1)
def _get_existing_projects(self):
project_names = set()
project_codes = set()
for project_name in self.dbcon.database.collection_names():
# Each collection will have exactly one project document
project_doc = self.dbcon.database[project_name].find_one(
{"type": "project"},
{"name": 1, "data.code": 1}
)
if not project_doc:
continue
project_name = project_doc.get("name")
if not project_name:
continue
project_names.add(project_name)
project_code = project_doc.get("data", {}).get("code")
if not project_code:
project_code = project_name
project_codes.add(project_code)
return project_names, project_codes

View file

@ -0,0 +1,176 @@
from Qt import QtWidgets, QtCore, QtGui
from . import (
ProjectModel,
HierarchyModel,
HierarchySelectionModel,
HierarchyView,
CreateProjectDialog
)
from .style import load_stylesheet, ResourceCache
from openpype import resources
from avalon.api import AvalonMongoDB
class ProjectManagerWindow(QtWidgets.QWidget):
def __init__(self, parent=None):
super(ProjectManagerWindow, self).__init__(parent)
self.setWindowTitle("OpenPype Project Manager")
self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath()))
# Top part of window
top_part_widget = QtWidgets.QWidget(self)
# Project selection
project_widget = QtWidgets.QWidget(top_part_widget)
dbcon = AvalonMongoDB()
project_model = ProjectModel(dbcon)
project_combobox = QtWidgets.QComboBox(project_widget)
project_combobox.setModel(project_model)
project_combobox.setRootModelIndex(QtCore.QModelIndex())
refresh_projects_btn = QtWidgets.QPushButton(project_widget)
refresh_projects_btn.setIcon(ResourceCache.get_icon("refresh"))
refresh_projects_btn.setToolTip("Refresh projects")
refresh_projects_btn.setObjectName("RefreshBtn")
create_project_btn = QtWidgets.QPushButton(
"Create project...", project_widget
)
project_layout = QtWidgets.QHBoxLayout(project_widget)
project_layout.setContentsMargins(0, 0, 0, 0)
project_layout.addWidget(project_combobox, 0)
project_layout.addWidget(refresh_projects_btn, 0)
project_layout.addWidget(create_project_btn, 0)
project_layout.addStretch(1)
# Helper buttons
helper_btns_widget = QtWidgets.QWidget(top_part_widget)
helper_label = QtWidgets.QLabel("Add:", helper_btns_widget)
add_asset_btn = QtWidgets.QPushButton(
ResourceCache.get_icon("asset", "default"),
"Asset",
helper_btns_widget
)
add_task_btn = QtWidgets.QPushButton(
ResourceCache.get_icon("task", "default"),
"Task",
helper_btns_widget
)
helper_btns_layout = QtWidgets.QHBoxLayout(helper_btns_widget)
helper_btns_layout.setContentsMargins(0, 0, 0, 0)
helper_btns_layout.addWidget(helper_label)
helper_btns_layout.addWidget(add_asset_btn)
helper_btns_layout.addWidget(add_task_btn)
helper_btns_layout.addStretch(1)
# Add widgets to top widget layout
top_part_layout = QtWidgets.QVBoxLayout(top_part_widget)
top_part_layout.setContentsMargins(0, 0, 0, 0)
top_part_layout.addWidget(project_widget)
top_part_layout.addWidget(helper_btns_widget)
hierarchy_model = HierarchyModel(dbcon)
hierarchy_view = HierarchyView(dbcon, hierarchy_model, self)
hierarchy_view.setModel(hierarchy_model)
_selection_model = HierarchySelectionModel(
hierarchy_model.multiselection_column_indexes
)
_selection_model.setModel(hierarchy_view.model())
hierarchy_view.setSelectionModel(_selection_model)
buttons_widget = QtWidgets.QWidget(self)
message_label = QtWidgets.QLabel(buttons_widget)
save_btn = QtWidgets.QPushButton("Save", buttons_widget)
buttons_layout = QtWidgets.QHBoxLayout(buttons_widget)
buttons_layout.setContentsMargins(0, 0, 0, 0)
buttons_layout.addWidget(message_label)
buttons_layout.addStretch(1)
buttons_layout.addWidget(save_btn)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(top_part_widget)
main_layout.addWidget(hierarchy_view)
main_layout.addWidget(buttons_widget)
refresh_projects_btn.clicked.connect(self._on_project_refresh)
create_project_btn.clicked.connect(self._on_project_create)
project_combobox.currentIndexChanged.connect(self._on_project_change)
save_btn.clicked.connect(self._on_save_click)
add_asset_btn.clicked.connect(self._on_add_asset)
add_task_btn.clicked.connect(self._on_add_task)
self.project_model = project_model
self.project_combobox = project_combobox
self.hierarchy_view = hierarchy_view
self.hierarchy_model = hierarchy_model
self.message_label = message_label
self.resize(1200, 600)
self.setStyleSheet(load_stylesheet())
self.refresh_projects()
def _set_project(self, project_name=None):
self.hierarchy_view.set_project(project_name)
def refresh_projects(self, project_name=None):
if project_name is None:
if self.project_combobox.count() > 0:
project_name = self.project_combobox.currentText()
self.project_model.refresh()
if self.project_combobox.count() == 0:
return self._set_project()
if project_name:
row = self.project_combobox.findText(project_name)
if row >= 0:
self.project_combobox.setCurrentIndex(row)
self._set_project(self.project_combobox.currentText())
def _on_project_change(self):
self._set_project(self.project_combobox.currentText())
def _on_project_refresh(self):
self.refresh_projects()
def _on_save_click(self):
self.hierarchy_model.save()
def _on_add_asset(self):
self.hierarchy_view.add_asset()
def _on_add_task(self):
self.hierarchy_view.add_task()
def show_message(self, message):
# TODO add nicer message pop
self.message_label.setText(message)
def _on_project_create(self):
dialog = CreateProjectDialog(self)
dialog.exec_()
if dialog.result() != 1:
return
project_name = dialog.project_name
self.show_message("Created project \"{}\"".format(project_name))
self.refresh_projects(project_name)

View file

@ -21,6 +21,7 @@ from openpype.settings.entities import (
TextEntity,
PathInput,
RawJsonEntity,
ColorEntity,
DefaultsNotDefined,
StudioDefaultsNotDefined,
@ -44,7 +45,7 @@ from .item_widgets import (
PathWidget,
PathInputWidget
)
from .color_widget import ColorWidget
from avalon.vendor import qtawesome
@ -113,6 +114,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
elif isinstance(entity, RawJsonEntity):
return RawJsonWidget(*args)
elif isinstance(entity, ColorEntity):
return ColorWidget(*args)
elif isinstance(entity, BaseEnumEntity):
return EnumeratorWidget(*args)

View file

@ -0,0 +1,171 @@
from Qt import QtWidgets, QtCore, QtGui
from .item_widgets import InputWidget
from openpype.widgets.color_widgets import (
ColorPickerWidget,
draw_checkerboard_tile
)
class ColorWidget(InputWidget):
def _add_inputs_to_layout(self):
self.input_field = ColorViewer(self.content_widget)
self.setFocusProxy(self.input_field)
self.content_layout.addWidget(self.input_field, 1)
self.input_field.clicked.connect(self._on_click)
self._dialog = None
def _on_click(self):
if self._dialog:
self._dialog.open()
return
dialog = ColorDialog(self.input_field.color(), self)
self._dialog = dialog
dialog.open()
dialog.finished.connect(self._on_dialog_finish)
def _on_dialog_finish(self, *_args):
if not self._dialog:
return
color = self._dialog.result()
if color is not None:
self.input_field.set_color(color)
self._on_value_change()
self._dialog.deleteLater()
self._dialog = None
def _on_entity_change(self):
if self.entity.value != self.input_value():
self.set_entity_value()
def set_entity_value(self):
self.input_field.set_color(*self.entity.value)
def input_value(self):
color = self.input_field.color()
return [color.red(), color.green(), color.blue(), color.alpha()]
def _on_value_change(self):
if self.ignore_input_changes:
return
self.entity.set(self.input_value())
class ColorViewer(QtWidgets.QWidget):
clicked = QtCore.Signal()
def __init__(self, parent=None):
super(ColorViewer, self).__init__(parent)
self.setMinimumSize(10, 10)
self.actual_pen = QtGui.QPen()
self.actual_color = QtGui.QColor()
self._checkerboard = None
def mouseReleaseEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.clicked.emit()
super(ColorViewer, self).mouseReleaseEvent(event)
def checkerboard(self):
if not self._checkerboard:
self._checkerboard = draw_checkerboard_tile(self.height() / 4)
return self._checkerboard
def color(self):
return self.actual_color
def set_color(self, *args):
# Create copy of entered color
self.actual_color = QtGui.QColor(*args)
# Repaint
self.update()
def set_alpha(self, alpha):
# Change alpha of current color
self.actual_color.setAlpha(alpha)
# Repaint
self.update()
def paintEvent(self, event):
rect = event.rect()
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
radius = rect.height() / 2
rounded_rect = QtGui.QPainterPath()
rounded_rect.addRoundedRect(QtCore.QRectF(rect), radius, radius)
painter.setClipPath(rounded_rect)
pen = QtGui.QPen(QtGui.QColor(255, 255, 255, 67))
pen.setWidth(1)
painter.setPen(pen)
painter.drawTiledPixmap(rect, self.checkerboard())
painter.fillRect(rect, self.actual_color)
painter.drawPath(rounded_rect)
painter.end()
class ColorDialog(QtWidgets.QDialog):
def __init__(self, color=None, parent=None):
super(ColorDialog, self).__init__(parent)
self.setWindowTitle("Color picker dialog")
picker_widget = ColorPickerWidget(color, self)
footer_widget = QtWidgets.QWidget(self)
ok_btn = QtWidgets.QPushButton("Ok", footer_widget)
cancel_btn = QtWidgets.QPushButton("Cancel", footer_widget)
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
footer_layout.setContentsMargins(0, 0, 0, 0)
footer_layout.addStretch(1)
footer_layout.addWidget(ok_btn)
footer_layout.addWidget(cancel_btn)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(picker_widget, 1)
layout.addWidget(footer_widget, 0)
ok_btn.clicked.connect(self.on_ok_clicked)
cancel_btn.clicked.connect(self.on_cancel_clicked)
self.picker_widget = picker_widget
self.ok_btn = ok_btn
self.cancel_btn = cancel_btn
self._result = None
def showEvent(self, event):
super(ColorDialog, self).showEvent(event)
btns_width = max(self.ok_btn.width(), self.cancel_btn.width())
self.ok_btn.setFixedWidth(btns_width)
self.cancel_btn.setFixedWidth(btns_width)
def on_ok_clicked(self):
self._result = self.picker_widget.color()
self.close()
def on_cancel_clicked(self):
self._result = None
self.close()
def result(self):
return self._result

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.0.0-rc4"
__version__ = "3.0.0-rc.5"

View file

@ -0,0 +1,14 @@
from .color_picker_widget import (
ColorPickerWidget
)
from .color_view import (
draw_checkerboard_tile
)
__all__ = (
"ColorPickerWidget",
"draw_checkerboard_tile"
)

View file

@ -0,0 +1,639 @@
import re
from Qt import QtWidgets, QtCore, QtGui
from .color_view import draw_checkerboard_tile
slide_style = """
QSlider::groove:horizontal {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #000, stop: 1 #fff
);
height: 8px;
border-radius: 4px;
}
QSlider::handle:horizontal {
background: qlineargradient(
x1:0, y1:0, x2:1, y2:1, stop:0 #ddd, stop:1 #bbb
);
border: 1px solid #777;
width: 8px;
margin-top: -1px;
margin-bottom: -1px;
border-radius: 4px;
}
QSlider::handle:horizontal:hover {
background: qlineargradient(
x1:0, y1:0, x2:1, y2:1, stop:0 #eee, stop:1 #ddd
);
border: 1px solid #444;ff
border-radius: 4px;
}"""
class AlphaSlider(QtWidgets.QSlider):
def __init__(self, *args, **kwargs):
super(AlphaSlider, self).__init__(*args, **kwargs)
self._mouse_clicked = False
self.setSingleStep(1)
self.setMinimum(0)
self.setMaximum(255)
self.setValue(255)
self._checkerboard = None
def checkerboard(self):
if self._checkerboard is None:
self._checkerboard = draw_checkerboard_tile(
3, QtGui.QColor(173, 173, 173), QtGui.QColor(27, 27, 27)
)
return self._checkerboard
def mousePressEvent(self, event):
self._mouse_clicked = True
if event.button() == QtCore.Qt.LeftButton:
self._set_value_to_pos(event.pos().x())
return event.accept()
return super(AlphaSlider, self).mousePressEvent(event)
def _set_value_to_pos(self, pos_x):
value = (
self.maximum() - self.minimum()
) * pos_x / self.width() + self.minimum()
self.setValue(value)
def mouseMoveEvent(self, event):
if self._mouse_clicked:
self._set_value_to_pos(event.pos().x())
super(AlphaSlider, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
self._mouse_clicked = True
super(AlphaSlider, self).mouseReleaseEvent(event)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
painter.fillRect(event.rect(), QtCore.Qt.transparent)
painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform)
rect = self.style().subControlRect(
QtWidgets.QStyle.CC_Slider,
opt,
QtWidgets.QStyle.SC_SliderGroove,
self
)
final_height = 9
offset_top = 0
if rect.height() > final_height:
offset_top = int((rect.height() - final_height) / 2)
rect = QtCore.QRect(
rect.x(),
offset_top,
rect.width(),
final_height
)
pix_rect = QtCore.QRect(event.rect())
pix_rect.setX(rect.x())
pix_rect.setWidth(rect.width() - (2 * rect.x()))
pix = QtGui.QPixmap(pix_rect.width(), pix_rect.height())
pix_painter = QtGui.QPainter(pix)
pix_painter.drawTiledPixmap(pix_rect, self.checkerboard())
gradient = QtGui.QLinearGradient(rect.topLeft(), rect.bottomRight())
gradient.setColorAt(0, QtCore.Qt.transparent)
gradient.setColorAt(1, QtCore.Qt.white)
pix_painter.fillRect(pix_rect, gradient)
pix_painter.end()
brush = QtGui.QBrush(pix)
painter.save()
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(brush)
ratio = rect.height() / 2
painter.drawRoundedRect(rect, ratio, ratio)
painter.restore()
_handle_rect = self.style().subControlRect(
QtWidgets.QStyle.CC_Slider,
opt,
QtWidgets.QStyle.SC_SliderHandle,
self
)
handle_rect = QtCore.QRect(rect)
if offset_top > 1:
height = handle_rect.height()
handle_rect.setY(handle_rect.y() - 1)
handle_rect.setHeight(height + 2)
handle_rect.setX(_handle_rect.x())
handle_rect.setWidth(handle_rect.height())
painter.save()
gradient = QtGui.QRadialGradient()
radius = handle_rect.height() / 2
center_x = handle_rect.width() / 2 + handle_rect.x()
center_y = handle_rect.height()
gradient.setCenter(center_x, center_y)
gradient.setCenterRadius(radius)
gradient.setFocalPoint(center_x, center_y)
gradient.setColorAt(0.9, QtGui.QColor(127, 127, 127))
gradient.setColorAt(1, QtCore.Qt.transparent)
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(gradient)
painter.drawEllipse(handle_rect)
painter.restore()
class AlphaInputs(QtWidgets.QWidget):
alpha_changed = QtCore.Signal(int)
def __init__(self, parent=None):
super(AlphaInputs, self).__init__(parent)
self._block_changes = False
self.alpha_value = None
percent_input = QtWidgets.QDoubleSpinBox(self)
percent_input.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
percent_input.setMinimum(0)
percent_input.setMaximum(100)
percent_input.setDecimals(2)
int_input = QtWidgets.QSpinBox(self)
int_input.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
int_input.setMinimum(0)
int_input.setMaximum(255)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(int_input)
layout.addWidget(QtWidgets.QLabel("0-255"))
layout.addWidget(percent_input)
layout.addWidget(QtWidgets.QLabel("%"))
percent_input.valueChanged.connect(self._on_percent_change)
int_input.valueChanged.connect(self._on_int_change)
self.percent_input = percent_input
self.int_input = int_input
self.set_alpha(255)
def set_alpha(self, alpha):
if alpha == self.alpha_value:
return
self.alpha_value = alpha
self.update_alpha()
def _on_percent_change(self):
if self._block_changes:
return
self.alpha_value = int(self.percent_input.value() * 255 / 100)
self.alpha_changed.emit(self.alpha_value)
self.update_alpha()
def _on_int_change(self):
if self._block_changes:
return
self.alpha_value = self.int_input.value()
self.alpha_changed.emit(self.alpha_value)
self.update_alpha()
def update_alpha(self):
self._block_changes = True
if self.int_input.value() != self.alpha_value:
self.int_input.setValue(self.alpha_value)
percent = round(100 * self.alpha_value / 255, 2)
if self.percent_input.value() != percent:
self.percent_input.setValue(percent)
self._block_changes = False
class RGBInputs(QtWidgets.QWidget):
value_changed = QtCore.Signal()
def __init__(self, color, parent=None):
super(RGBInputs, self).__init__(parent)
self._block_changes = False
self.color = color
input_red = QtWidgets.QSpinBox(self)
input_green = QtWidgets.QSpinBox(self)
input_blue = QtWidgets.QSpinBox(self)
input_red.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
input_green.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
input_blue.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
input_red.setMinimum(0)
input_green.setMinimum(0)
input_blue.setMinimum(0)
input_red.setMaximum(255)
input_green.setMaximum(255)
input_blue.setMaximum(255)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(input_red, 1)
layout.addWidget(input_green, 1)
layout.addWidget(input_blue, 1)
input_red.valueChanged.connect(self._on_red_change)
input_green.valueChanged.connect(self._on_green_change)
input_blue.valueChanged.connect(self._on_blue_change)
self.input_red = input_red
self.input_green = input_green
self.input_blue = input_blue
def _on_red_change(self, value):
if self._block_changes:
return
self.color.setRed(value)
self._on_change()
def _on_green_change(self, value):
if self._block_changes:
return
self.color.setGreen(value)
self._on_change()
def _on_blue_change(self, value):
if self._block_changes:
return
self.color.setBlue(value)
self._on_change()
def _on_change(self):
self.value_changed.emit()
def color_changed(self):
if (
self.input_red.value() == self.color.red()
and self.input_green.value() == self.color.green()
and self.input_blue.value() == self.color.blue()
):
return
self._block_changes = True
self.input_red.setValue(self.color.red())
self.input_green.setValue(self.color.green())
self.input_blue.setValue(self.color.blue())
self._block_changes = False
class CMYKInputs(QtWidgets.QWidget):
value_changed = QtCore.Signal()
def __init__(self, color, parent=None):
super(CMYKInputs, self).__init__(parent)
self.color = color
self._block_changes = False
input_cyan = QtWidgets.QSpinBox(self)
input_magenta = QtWidgets.QSpinBox(self)
input_yellow = QtWidgets.QSpinBox(self)
input_black = QtWidgets.QSpinBox(self)
input_cyan.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
input_magenta.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
input_yellow.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
input_black.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
input_cyan.setMinimum(0)
input_magenta.setMinimum(0)
input_yellow.setMinimum(0)
input_black.setMinimum(0)
input_cyan.setMaximum(255)
input_magenta.setMaximum(255)
input_yellow.setMaximum(255)
input_black.setMaximum(255)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(input_cyan, 1)
layout.addWidget(input_magenta, 1)
layout.addWidget(input_yellow, 1)
layout.addWidget(input_black, 1)
input_cyan.valueChanged.connect(self._on_change)
input_magenta.valueChanged.connect(self._on_change)
input_yellow.valueChanged.connect(self._on_change)
input_black.valueChanged.connect(self._on_change)
self.input_cyan = input_cyan
self.input_magenta = input_magenta
self.input_yellow = input_yellow
self.input_black = input_black
def _on_change(self):
if self._block_changes:
return
self.color.setCmyk(
self.input_cyan.value(),
self.input_magenta.value(),
self.input_yellow.value(),
self.input_black.value()
)
self.value_changed.emit()
def color_changed(self):
if self._block_changes:
return
_cur_color = QtGui.QColor()
_cur_color.setCmyk(
self.input_cyan.value(),
self.input_magenta.value(),
self.input_yellow.value(),
self.input_black.value()
)
if (
_cur_color.red() == self.color.red()
and _cur_color.green() == self.color.green()
and _cur_color.blue() == self.color.blue()
):
return
c, m, y, k, _ = self.color.getCmyk()
self._block_changes = True
self.input_cyan.setValue(c)
self.input_magenta.setValue(m)
self.input_yellow.setValue(y)
self.input_black.setValue(k)
self._block_changes = False
class HEXInputs(QtWidgets.QWidget):
hex_regex = re.compile("^#(([0-9a-fA-F]{2}){3}|([0-9a-fA-F]){3})$")
value_changed = QtCore.Signal()
def __init__(self, color, parent=None):
super(HEXInputs, self).__init__(parent)
self.color = color
input_field = QtWidgets.QLineEdit(self)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(input_field, 1)
input_field.textChanged.connect(self._on_change)
self.input_field = input_field
def _on_change(self):
if self._block_changes:
return
input_value = self.input_field.text()
# TODO what if does not match?
if self.hex_regex.match(input_value):
self.color.setNamedColor(input_value)
self.value_changed.emit()
def color_changed(self):
input_value = self.input_field.text()
if self.hex_regex.match(input_value):
_cur_color = QtGui.QColor()
_cur_color.setNamedColor(input_value)
if (
_cur_color.red() == self.color.red()
and _cur_color.green() == self.color.green()
and _cur_color.blue() == self.color.blue()
):
return
self._block_changes = True
self.input_field.setText(self.color.name())
self._block_changes = False
class HSVInputs(QtWidgets.QWidget):
value_changed = QtCore.Signal()
def __init__(self, color, parent=None):
super(HSVInputs, self).__init__(parent)
self._block_changes = False
self.color = color
input_hue = QtWidgets.QSpinBox(self)
input_sat = QtWidgets.QSpinBox(self)
input_val = QtWidgets.QSpinBox(self)
input_hue.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
input_sat.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
input_val.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
input_hue.setMinimum(0)
input_sat.setMinimum(0)
input_val.setMinimum(0)
input_hue.setMaximum(359)
input_sat.setMaximum(255)
input_val.setMaximum(255)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(input_hue, 1)
layout.addWidget(input_sat, 1)
layout.addWidget(input_val, 1)
input_hue.valueChanged.connect(self._on_change)
input_sat.valueChanged.connect(self._on_change)
input_val.valueChanged.connect(self._on_change)
self.input_hue = input_hue
self.input_sat = input_sat
self.input_val = input_val
def _on_change(self):
if self._block_changes:
return
self.color.setHsv(
self.input_hue.value(),
self.input_sat.value(),
self.input_val.value()
)
self.value_changed.emit()
def color_changed(self):
_cur_color = QtGui.QColor()
_cur_color.setHsv(
self.input_hue.value(),
self.input_sat.value(),
self.input_val.value()
)
if (
_cur_color.red() == self.color.red()
and _cur_color.green() == self.color.green()
and _cur_color.blue() == self.color.blue()
):
return
self._block_changes = True
h, s, v, _ = self.color.getHsv()
self.input_hue.setValue(h)
self.input_sat.setValue(s)
self.input_val.setValue(v)
self._block_changes = False
class HSLInputs(QtWidgets.QWidget):
value_changed = QtCore.Signal()
def __init__(self, color, parent=None):
super(HSLInputs, self).__init__(parent)
self._block_changes = False
self.color = color
input_hue = QtWidgets.QSpinBox(self)
input_sat = QtWidgets.QSpinBox(self)
input_light = QtWidgets.QSpinBox(self)
input_hue.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
input_sat.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
input_light.setButtonSymbols(QtWidgets.QSpinBox.NoButtons)
input_hue.setMinimum(0)
input_sat.setMinimum(0)
input_light.setMinimum(0)
input_hue.setMaximum(359)
input_sat.setMaximum(255)
input_light.setMaximum(255)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(input_hue, 1)
layout.addWidget(input_sat, 1)
layout.addWidget(input_light, 1)
input_hue.valueChanged.connect(self._on_change)
input_sat.valueChanged.connect(self._on_change)
input_light.valueChanged.connect(self._on_change)
self.input_hue = input_hue
self.input_sat = input_sat
self.input_light = input_light
def _on_change(self):
if self._block_changes:
return
self.color.setHsl(
self.input_hue.value(),
self.input_sat.value(),
self.input_light.value()
)
self.value_changed.emit()
def color_changed(self):
_cur_color = QtGui.QColor()
_cur_color.setHsl(
self.input_hue.value(),
self.input_sat.value(),
self.input_light.value()
)
if (
_cur_color.red() == self.color.red()
and _cur_color.green() == self.color.green()
and _cur_color.blue() == self.color.blue()
):
return
self._block_changes = True
h, s, l, _ = self.color.getHsl()
self.input_hue.setValue(h)
self.input_sat.setValue(s)
self.input_light.setValue(l)
self._block_changes = False
class ColorInputsWidget(QtWidgets.QWidget):
color_changed = QtCore.Signal(QtGui.QColor)
def __init__(self, parent=None, **kwargs):
super(ColorInputsWidget, self).__init__(parent)
color = QtGui.QColor()
input_fields = []
if kwargs.get("use_hex", True):
input_fields.append(HEXInputs(color, self))
if kwargs.get("use_rgb", True):
input_fields.append(RGBInputs(color, self))
if kwargs.get("use_hsl", True):
input_fields.append(HSLInputs(color, self))
if kwargs.get("use_hsv", True):
input_fields.append(HSVInputs(color, self))
if kwargs.get("use_cmyk", True):
input_fields.append(CMYKInputs(color, self))
inputs_widget = QtWidgets.QWidget(self)
inputs_layout = QtWidgets.QVBoxLayout(inputs_widget)
for input_field in input_fields:
inputs_layout.addWidget(input_field)
input_field.value_changed.connect(self._on_value_change)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(inputs_widget, 0)
spacer = QtWidgets.QWidget(self)
layout.addWidget(spacer, 1)
self.input_fields = input_fields
self.color = color
def set_color(self, color):
if (
color.red() == self.color.red()
and color.green() == self.color.green()
and color.blue() == self.color.blue()
):
return
self.color.setRed(color.red())
self.color.setGreen(color.green())
self.color.setBlue(color.blue())
self._on_value_change()
def _on_value_change(self):
for input_field in self.input_fields:
input_field.color_changed()
self.color_changed.emit(self.color)

View file

@ -0,0 +1,176 @@
import os
from Qt import QtWidgets, QtCore, QtGui
from .color_triangle import QtColorTriangle
from .color_view import ColorViewer
from .color_screen_pick import PickScreenColorWidget
from .color_inputs import (
AlphaSlider,
AlphaInputs,
HEXInputs,
RGBInputs,
HSLInputs,
HSVInputs
)
class ColorPickerWidget(QtWidgets.QWidget):
color_changed = QtCore.Signal(QtGui.QColor)
def __init__(self, color=None, parent=None):
super(ColorPickerWidget, self).__init__(parent)
# Color triangle
color_triangle = QtColorTriangle(self)
alpha_slider_proxy = QtWidgets.QWidget(self)
alpha_slider = AlphaSlider(QtCore.Qt.Horizontal, alpha_slider_proxy)
alpha_slider_layout = QtWidgets.QHBoxLayout(alpha_slider_proxy)
alpha_slider_layout.setContentsMargins(5, 5, 5, 5)
alpha_slider_layout.addWidget(alpha_slider, 1)
# Eye picked widget
pick_widget = PickScreenColorWidget()
pick_widget.setMaximumHeight(50)
# Color pick button
btn_pick_color = QtWidgets.QPushButton(self)
icon_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"eyedropper.png"
)
btn_pick_color.setIcon(QtGui.QIcon(icon_path))
btn_pick_color.setToolTip("Pick a color")
# Color preview
color_view = ColorViewer(self)
color_view.setMaximumHeight(50)
alpha_inputs = AlphaInputs(self)
color_inputs_color = QtGui.QColor()
col_inputs_by_label = [
("HEX", HEXInputs(color_inputs_color, self)),
("RGB", RGBInputs(color_inputs_color, self)),
("HSL", HSLInputs(color_inputs_color, self)),
("HSV", HSVInputs(color_inputs_color, self))
]
layout = QtWidgets.QGridLayout(self)
empty_col = 1
label_col = empty_col + 1
input_col = label_col + 1
empty_widget = QtWidgets.QWidget(self)
empty_widget.setFixedWidth(10)
layout.addWidget(empty_widget, 0, empty_col)
row = 0
layout.addWidget(btn_pick_color, row, label_col)
layout.addWidget(color_view, row, input_col)
row += 1
color_input_fields = []
for label, input_field in col_inputs_by_label:
layout.addWidget(QtWidgets.QLabel(label, self), row, label_col)
layout.addWidget(input_field, row, input_col)
input_field.value_changed.connect(
self._on_color_input_value_change
)
color_input_fields.append(input_field)
row += 1
layout.addWidget(color_triangle, 0, 0, row + 1, 1)
layout.setRowStretch(row, 1)
row += 1
layout.addWidget(alpha_slider_proxy, row, 0)
layout.addWidget(QtWidgets.QLabel("Alpha", self), row, label_col)
layout.addWidget(alpha_inputs, row, input_col)
row += 1
layout.setRowStretch(row, 1)
color_view.set_color(color_triangle.cur_color)
color_triangle.color_changed.connect(self.triangle_color_changed)
alpha_slider.valueChanged.connect(self._on_alpha_slider_change)
pick_widget.color_selected.connect(self.on_color_change)
alpha_inputs.alpha_changed.connect(self._on_alpha_inputs_changed)
btn_pick_color.released.connect(self.pick_color)
self.color_input_fields = color_input_fields
self.color_inputs_color = color_inputs_color
self.pick_widget = pick_widget
self.color_triangle = color_triangle
self.alpha_slider = alpha_slider
self.color_view = color_view
self.alpha_inputs = alpha_inputs
self.btn_pick_color = btn_pick_color
self._minimum_size_set = False
if color:
self.set_color(color)
self.alpha_changed(color.alpha())
def showEvent(self, event):
super(ColorPickerWidget, self).showEvent(event)
if self._minimum_size_set:
return
triangle_size = max(int(self.width() / 5 * 3), 180)
self.color_triangle.setMinimumWidth(triangle_size)
self.color_triangle.setMinimumHeight(triangle_size)
self._minimum_size_set = True
def color(self):
return self.color_view.color()
def set_color(self, color):
self.alpha_inputs.set_alpha(color.alpha())
self.on_color_change(color)
def pick_color(self):
self.pick_widget.pick_color()
def triangle_color_changed(self, color):
self.color_view.set_color(color)
if self.color_inputs_color != color:
self.color_inputs_color.setRgb(
color.red(), color.green(), color.blue()
)
for color_input in self.color_input_fields:
color_input.color_changed()
def on_color_change(self, color):
self.color_view.set_color(color)
self.color_triangle.set_color(color)
if self.color_inputs_color != color:
self.color_inputs_color.setRgb(
color.red(), color.green(), color.blue()
)
for color_input in self.color_input_fields:
color_input.color_changed()
def _on_color_input_value_change(self):
for input_field in self.color_input_fields:
input_field.color_changed()
self.on_color_change(QtGui.QColor(self.color_inputs_color))
def alpha_changed(self, value):
self.color_view.set_alpha(value)
if self.alpha_slider.value() != value:
self.alpha_slider.setValue(value)
if self.alpha_inputs.alpha_value != value:
self.alpha_inputs.set_alpha(value)
def _on_alpha_inputs_changed(self, value):
self.alpha_changed(value)
def _on_alpha_slider_change(self, value):
self.alpha_changed(value)

View file

@ -0,0 +1,248 @@
import Qt
from Qt import QtWidgets, QtCore, QtGui
class PickScreenColorWidget(QtWidgets.QWidget):
color_selected = QtCore.Signal(QtGui.QColor)
def __init__(self, parent=None):
super(PickScreenColorWidget, self).__init__(parent)
self.labels = []
self.magnification = 2
self._min_magnification = 1
self._max_magnification = 10
def add_magnification_delta(self, delta):
_delta = abs(delta / 1000)
if delta > 0:
self.magnification += _delta
else:
self.magnification -= _delta
if self.magnification > self._max_magnification:
self.magnification = self._max_magnification
elif self.magnification < self._min_magnification:
self.magnification = self._min_magnification
def pick_color(self):
if self.labels:
if self.labels[0].isVisible():
return
self.labels = []
for screen in QtWidgets.QApplication.screens():
label = PickLabel(self)
label.pick_color(screen)
label.color_selected.connect(self.on_color_select)
label.close_session.connect(self.end_pick_session)
self.labels.append(label)
def end_pick_session(self):
for label in self.labels:
label.close()
self.labels = []
def on_color_select(self, color):
self.color_selected.emit(color)
self.end_pick_session()
class PickLabel(QtWidgets.QLabel):
color_selected = QtCore.Signal(QtGui.QColor)
close_session = QtCore.Signal()
def __init__(self, pick_widget):
super(PickLabel, self).__init__()
self.setMouseTracking(True)
self.pick_widget = pick_widget
self.radius_pen = QtGui.QPen(QtGui.QColor(27, 27, 27), 2)
self.text_pen = QtGui.QPen(QtGui.QColor(127, 127, 127), 4)
self.text_bg = QtGui.QBrush(QtGui.QColor(27, 27, 27))
self._mouse_over = False
self.radius = 100
self.radius_ratio = 11
@property
def magnification(self):
return self.pick_widget.magnification
def pick_color(self, screen_obj):
self.show()
self.windowHandle().setScreen(screen_obj)
geo = screen_obj.geometry()
args = (
QtWidgets.QApplication.desktop().winId(),
geo.x(), geo.y(), geo.width(), geo.height()
)
if Qt.__binding__ in ("PyQt4", "PySide"):
pix = QtGui.QPixmap.grabWindow(*args)
else:
pix = screen_obj.grabWindow(*args)
if pix.width() > pix.height():
size = pix.height()
else:
size = pix.width()
self.radius = int(size / self.radius_ratio)
self.setPixmap(pix)
self.showFullScreen()
def wheelEvent(self, event):
y_delta = event.angleDelta().y()
self.pick_widget.add_magnification_delta(y_delta)
self.update()
def enterEvent(self, event):
self._mouse_over = True
super().enterEvent(event)
def leaveEvent(self, event):
self._mouse_over = False
super().leaveEvent(event)
self.update()
def mouseMoveEvent(self, event):
self.update()
def paintEvent(self, event):
super().paintEvent(event)
if not self._mouse_over:
return
mouse_pos_to_widet = self.mapFromGlobal(QtGui.QCursor.pos())
magnified_half_size = self.radius / self.magnification
magnified_size = magnified_half_size * 2
zoom_x_1 = mouse_pos_to_widet.x() - magnified_half_size
zoom_x_2 = mouse_pos_to_widet.x() + magnified_half_size
zoom_y_1 = mouse_pos_to_widet.y() - magnified_half_size
zoom_y_2 = mouse_pos_to_widet.y() + magnified_half_size
pix_width = magnified_size
pix_height = magnified_size
draw_pos_x = 0
draw_pos_y = 0
if zoom_x_1 < 0:
draw_pos_x = abs(zoom_x_1)
pix_width -= draw_pos_x
zoom_x_1 = 1
elif zoom_x_2 > self.pixmap().width():
pix_width -= zoom_x_2 - self.pixmap().width()
if zoom_y_1 < 0:
draw_pos_y = abs(zoom_y_1)
pix_height -= draw_pos_y
zoom_y_1 = 1
elif zoom_y_2 > self.pixmap().height():
pix_height -= zoom_y_2 - self.pixmap().height()
new_pix = QtGui.QPixmap(magnified_size, magnified_size)
new_pix.fill(QtCore.Qt.transparent)
new_pix_painter = QtGui.QPainter(new_pix)
new_pix_painter.drawPixmap(
QtCore.QRect(draw_pos_x, draw_pos_y, pix_width, pix_height),
self.pixmap().copy(zoom_x_1, zoom_y_1, pix_width, pix_height)
)
new_pix_painter.end()
painter = QtGui.QPainter(self)
ellipse_rect = QtCore.QRect(
mouse_pos_to_widet.x() - self.radius,
mouse_pos_to_widet.y() - self.radius,
self.radius * 2,
self.radius * 2
)
ellipse_rect_f = QtCore.QRectF(ellipse_rect)
path = QtGui.QPainterPath()
path.addEllipse(ellipse_rect_f)
painter.setClipPath(path)
new_pix_rect = QtCore.QRect(
mouse_pos_to_widet.x() - self.radius + 1,
mouse_pos_to_widet.y() - self.radius + 1,
new_pix.width() * self.magnification,
new_pix.height() * self.magnification
)
painter.drawPixmap(new_pix_rect, new_pix)
painter.setClipping(False)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(self.radius_pen)
painter.drawEllipse(ellipse_rect_f)
image = self.pixmap().toImage()
if image.valid(mouse_pos_to_widet):
color = QtGui.QColor(image.pixel(mouse_pos_to_widet))
else:
color = QtGui.QColor()
color_text = "Red: {} - Green: {} - Blue: {}".format(
color.red(), color.green(), color.blue()
)
font = painter.font()
font.setPointSize(self.radius / 10)
painter.setFont(font)
text_rect_height = int(painter.fontMetrics().height() + 10)
text_rect = QtCore.QRect(
ellipse_rect.x(),
ellipse_rect.bottom(),
ellipse_rect.width(),
text_rect_height
)
if text_rect.bottom() > self.pixmap().height():
text_rect.moveBottomLeft(ellipse_rect.topLeft())
rect_radius = text_rect_height / 2
path = QtGui.QPainterPath()
path.addRoundedRect(
QtCore.QRectF(text_rect),
rect_radius,
rect_radius
)
painter.fillPath(path, self.text_bg)
painter.setPen(self.text_pen)
painter.drawText(
text_rect,
QtCore.Qt.AlignLeft | QtCore.Qt.AlignCenter,
color_text
)
color_rect_x = ellipse_rect.x() - text_rect_height
if color_rect_x < 0:
color_rect_x += (text_rect_height + ellipse_rect.width())
color_rect = QtCore.QRect(
color_rect_x,
ellipse_rect.y(),
text_rect_height,
ellipse_rect.height()
)
path = QtGui.QPainterPath()
path.addRoundedRect(
QtCore.QRectF(color_rect),
rect_radius,
rect_radius
)
painter.fillPath(path, color)
painter.drawRoundedRect(color_rect, rect_radius, rect_radius)
painter.end()
def mouseReleaseEvent(self, event):
color = QtGui.QColor(self.pixmap().toImage().pixel(event.pos()))
self.color_selected.emit(color)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Escape:
self.close_session.emit()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,83 @@
from Qt import QtWidgets, QtCore, QtGui
def draw_checkerboard_tile(piece_size=None, color_1=None, color_2=None):
if piece_size is None:
piece_size = 7
if color_1 is None:
color_1 = QtGui.QColor(188, 188, 188)
if color_2 is None:
color_2 = QtGui.QColor(90, 90, 90)
pix = QtGui.QPixmap(piece_size * 2, piece_size * 2)
pix_painter = QtGui.QPainter(pix)
rect = QtCore.QRect(
0, 0, piece_size, piece_size
)
pix_painter.fillRect(rect, color_1)
rect.moveTo(piece_size, piece_size)
pix_painter.fillRect(rect, color_1)
rect.moveTo(piece_size, 0)
pix_painter.fillRect(rect, color_2)
rect.moveTo(0, piece_size)
pix_painter.fillRect(rect, color_2)
pix_painter.end()
return pix
class ColorViewer(QtWidgets.QWidget):
def __init__(self, parent=None):
super(ColorViewer, self).__init__(parent)
self.setMinimumSize(10, 10)
self.alpha = 255
self.actual_pen = QtGui.QPen()
self.actual_color = QtGui.QColor()
self._checkerboard = None
def checkerboard(self):
if not self._checkerboard:
self._checkerboard = draw_checkerboard_tile(4)
return self._checkerboard
def color(self):
return self.actual_color
def set_color(self, color):
if color == self.actual_color:
return
# Create copy of entered color
self.actual_color = QtGui.QColor(color)
# Set alpha by current alpha value
self.actual_color.setAlpha(self.alpha)
# Repaint
self.update()
def set_alpha(self, alpha):
if alpha == self.alpha:
return
# Change alpha of current color
self.actual_color.setAlpha(alpha)
# Store the value
self.alpha = alpha
# Repaint
self.update()
def paintEvent(self, event):
clip_rect = event.rect()
rect = clip_rect.adjusted(0, 0, -1, -1)
painter = QtGui.QPainter(self)
painter.setClipRect(clip_rect)
painter.drawTiledPixmap(rect, self.checkerboard())
painter.setBrush(self.actual_color)
pen = QtGui.QPen(QtGui.QColor(255, 255, 255, 67))
painter.setPen(pen)
painter.drawRect(rect)
painter.end()

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
version = "3.0.0-rc4"
version = "3.0.0-rc.5"
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team <info@openpype.io>"]
license = "MIT License"

View file

@ -98,7 +98,7 @@ executables = [
setup(
name="OpenPype",
version=__version__,
description="Ultimate pipeline",
description="OpenPype",
cmdclass={"build_sphinx": BuildDoc},
options={
"build_exe": build_exe_options,

View file

@ -12,8 +12,22 @@
PS> .\build.ps1
.EXAMPLE
To build without automatical submodule update:
PS> .\build.ps1 --no-submodule-update
.LINK
https://openpype.io/docs
#>
$arguments=$ARGS
$disable_submodule_update=""
if($arguments -eq "--no-submodule-update") {
$disable_submodule_update=$true
}
function Start-Progress {
param([ScriptBlock]$code)
$scroll = "/-\|/-\|"
@ -76,17 +90,17 @@ $art = @"
. . .. . ..
_oOOP3OPP3Op_. .
.PPpo~· ·· ~2p. ·· ···· · ·
·Ppo · .pPO3Op.· · O:· · · ·
.3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · ·
·~OP 3PO· .Op3 : · ·· _____ _____ _____
·P3O · oP3oP3O3P' · · · · / /·/ /·/ /
O3:· O3p~ · ·:· · ·/____/·/____/ /____/
'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · ·
· ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · ·
· '_ .. · . _OP3·· · ·https://openpype.io·· ·
~P3·OPPPO3OP~ · ·· ·
· ' '· · ·· · · · ·· ·
.PPpo~. .. ~2p. .. .... . .
.Ppo . .pPO3Op.. . O:. . . .
.3Pp . oP3'. 'P33. . 4 .. . . . .. . . .
.~OP 3PO. .Op3 : . .. _____ _____ _____
.P3O . oP3oP3O3P' . . . . / /./ /./ /
O3:. O3p~ . .:. . ./____/./____/ /____/
'P . 3p3. oP3~. ..P:. . . .. . . .. . . .
. ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . .
. '_ .. . . _OP3.. . .https://openpype.io.. .
~P3.OPPPO3OP~ . .. .
. ' '. . .. . . . .. .
"@
@ -134,10 +148,14 @@ catch {
Write-Host $_.Exception.Message
Exit-WithCode 1
}
Write-Host ">>> " -NoNewLine -ForegroundColor green
Write-Host "Making sure submodules are up-to-date ..."
git submodule update --init --recursive
if (-not $disable_submodule_update) {
Write-Host ">>> " -NoNewLine -ForegroundColor green
Write-Host "Making sure submodules are up-to-date ..."
git submodule update --init --recursive
} else {
Write-Host "*** " -NoNewLine -ForegroundColor yellow
Write-Host "Not updating submodules ..."
}
Write-Host ">>> " -NoNewline -ForegroundColor green
Write-Host "OpenPype [ " -NoNewline -ForegroundColor white
@ -164,7 +182,7 @@ Write-Host "OK" -ForegroundColor green
Write-Host ">>> " -NoNewline -ForegroundColor green
Write-Host "Building OpenPype ..."
$startTime = (Get-Date).Millisecond
$startTime = [int][double]::Parse((Get-Date -UFormat %s))
$out = & poetry run python setup.py build 2>&1
if ($LASTEXITCODE -ne 0)
@ -183,7 +201,7 @@ Write-Host ">>> " -NoNewline -ForegroundColor green
Write-Host "restoring current directory"
Set-Location -Path $current_dir
$endTime = (Get-Date).Millisecond
$endTime = [int][double]::Parse((Get-Date -UFormat %s))
Write-Host "*** " -NoNewline -ForegroundColor Cyan
Write-Host "All done in $($endTime - $startTime) secs. You will find OpenPype and build log in " -NoNewLine
Write-Host "'.\build'" -NoNewline -ForegroundColor Green

View file

@ -57,6 +57,26 @@ BIPurple='\033[1;95m' # Purple
BICyan='\033[1;96m' # Cyan
BIWhite='\033[1;97m' # White
args=$@
disable_submodule_update = 0
while :; do
case $1 in
--no-submodule-update)
disable_submodule_update=1
;;
--)
shift
break
;;
*)
break
esac
shift
done
##############################################################################
# Detect required version of python
@ -172,9 +192,12 @@ main () {
. "$openpype_root/tools/create_env.sh" || { echo -e "${BIRed}!!!${RST} Poetry installation failed"; return; }
fi
if [ "$disable_submodule_update" == 1 ]; then
echo -e "${BIGreen}>>>${RST} Making sure submodules are up-to-date ..."
git submodule update --init --recursive
else
echo -e "${BIYellow}***${RST} Not updating submodules ..."
fi
echo -e "${BIGreen}>>>${RST} Building ..."
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
poetry run python "$openpype_root/setup.py" build > "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return; }

View file

@ -26,10 +26,12 @@ import platform
from pathlib import Path
import shutil
import blessed
import enlighten
import time
term = blessed.Terminal()
manager = enlighten.get_manager()
def _print(msg: str, type: int = 0) -> None:
@ -52,6 +54,24 @@ def _print(msg: str, type: int = 0) -> None:
print("{}{}".format(header, msg))
def count_folders(path: Path) -> int:
"""Recursively count items inside given Path.
Args:
path (Path): Path to count.
Returns:
int: number of items.
"""
cnt = 0
for child in path.iterdir():
if child.is_dir():
cnt += 1
cnt += count_folders(child)
return cnt
_print("Starting dependency cleanup ...")
start_time = time.time_ns()
@ -96,30 +116,55 @@ deps_dir = build_dir / "dependencies"
# copy all files
_print("Copying dependencies ...")
shutil.copytree(site_pkg.as_posix(), deps_dir.as_posix())
total_files = count_folders(site_pkg)
progress_bar = enlighten.Counter(
total=total_files, desc="Processing Dependencies",
units="%", color="green")
def _progress(_base, _names):
progress_bar.update()
return []
shutil.copytree(site_pkg.as_posix(),
deps_dir.as_posix(),
ignore=_progress)
progress_bar.close()
# iterate over frozen libs and create list to delete
libs_dir = build_dir / "lib"
to_delete = []
_print("Finding duplicates ...")
# _print("Finding duplicates ...")
deps_items = list(deps_dir.iterdir())
item_count = len(list(libs_dir.iterdir()))
find_progress_bar = enlighten.Counter(
total=item_count, desc="Finding duplicates", units="%", color="yellow")
for d in libs_dir.iterdir():
if (deps_dir / d.name) in deps_items:
to_delete.append(d)
_print(f"found {d}", 3)
# _print(f"found {d}", 3)
find_progress_bar.update()
find_progress_bar.close()
# add openpype and igniter in libs too
to_delete.append(libs_dir / "openpype")
to_delete.append(libs_dir / "igniter")
# delete duplicates
_print(f"Deleting {len(to_delete)} duplicates ...")
# _print(f"Deleting {len(to_delete)} duplicates ...")
delete_progress_bar = enlighten.Counter(
total=len(to_delete), desc="Deleting duplicates", units="%", color="red")
for d in to_delete:
if d.is_dir():
shutil.rmtree(d)
else:
d.unlink()
delete_progress_bar.update()
delete_progress_bar.close()
end_time = time.time_ns()
total_time = (end_time - start_time) / 1000000000

View file

@ -1,16 +1,14 @@
<#
.SYNOPSIS
Helper script to build OpenPype.
Helper script to build OpenPype Installer.
.DESCRIPTION
This script will detect Python installation, and build OpenPype to `build`
directory using existing virtual environment created by Poetry (or
by running `/tools/create_venv.ps1`). It will then shuffle dependencies in
build folder to optimize for different Python versions (2/3) in Python host.
This script will use already built OpenPype (in `build` directory) and
create Windows installer from it using Inno Setup (https://jrsoftware.org/)
.EXAMPLE
PS> .\build.ps1
PS> .\build_win_installer.ps1
#>
@ -76,11 +74,19 @@ function Install-Poetry() {
$art = @"
.---= [ by Pype Club ] =---.
https://openpype.io
. . .. . ..
_oOOP3OPP3Op_. .
.PPpo~. .. ~2p. .. .... . .
.Ppo . .pPO3Op.. . O:. . . .
.3Pp . oP3'. 'P33. . 4 .. . . . .. . . .
.~OP 3PO. .Op3 : . .. _____ _____ _____
.P3O . oP3oP3O3P' . . . . / /./ /./ /
O3:. O3p~ . .:. . ./____/./____/ /____/
'P . 3p3. oP3~. ..P:. . . .. . . .. . . .
. ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . .
. '_ .. . . _OP3.. . .https://openpype.io.. .
~P3.OPPPO3OP~ . .. .
. ' '. . .. . . . .. .
"@

View file

@ -11,6 +11,11 @@
PS> .\create_env.ps1
.EXAMPLE
Print verbose information from Poetry:
PS> .\create_env.ps1 --verbose
#>
$arguments=$ARGS
@ -98,17 +103,17 @@ $art = @"
. . .. . ..
_oOOP3OPP3Op_. .
.PPpo~· ·· ~2p. ·· ···· · ·
·Ppo · .pPO3Op.· · O:· · · ·
.3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · ·
·~OP 3PO· .Op3 : · ·· _____ _____ _____
·P3O · oP3oP3O3P' · · · · / /·/ /·/ /
O3:· O3p~ · ·:· · ·/____/·/____/ /____/
'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · ·
· ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · ·
· '_ .. · . _OP3·· · ·https://openpype.io·· ·
~P3·OPPPO3OP~ · ·· ·
· ' '· · ·· · · · ·· ·
.PPpo~. .. ~2p. .. .... . .
.Ppo . .pPO3Op.. . O:. . . .
.3Pp . oP3'. 'P33. . 4 .. . . . .. . . .
.~OP 3PO. .Op3 : . .. _____ _____ _____
.P3O . oP3oP3O3P' . . . . / /./ /./ /
O3:. O3p~ . .:. . ./____/./____/ /____/
'P . 3p3. oP3~. ..P:. . . .. . . .. . . .
. ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . .
. '_ .. . . _OP3.. . .https://openpype.io.. .
~P3.OPPPO3OP~ . .. .
. ' '. . .. . . . .. .
"@

View file

@ -4,14 +4,19 @@
.DESCRIPTION
This script will detect Python installation and run OpenPype to create
zip. It needs mongodb running. I will create zip from current source code
version and copy it top `%LOCALAPPDATA%/pypeclub/pype` if `--path` or `-p`
zip. It will create zip from current source code
version and copy it top `%LOCALAPPDATA%/pypeclub/openpype` if `--path` or `-p`
argument is not used.
.EXAMPLE
PS> .\create_zip.ps1
.EXAMPLE
To put generated zip to C:\OpenPype directory:
PS> .\create_zip.ps1 --path C:\OpenPype
#>
function Exit-WithCode($exitcode) {
@ -52,17 +57,17 @@ $art = @"
. . .. . ..
_oOOP3OPP3Op_. .
.PPpo~· ·· ~2p. ·· ···· · ·
·Ppo · .pPO3Op.· · O:· · · ·
.3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · ·
·~OP 3PO· .Op3 : · ·· _____ _____ _____
·P3O · oP3oP3O3P' · · · · / /·/ /·/ /
O3:· O3p~ · ·:· · ·/____/·/____/ /____/
'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · ·
· ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · ·
· '_ .. · . _OP3·· · ·https://openpype.io·· ·
~P3·OPPPO3OP~ · ·· ·
· ' '· · ·· · · · ·· ·
.PPpo~. .. ~2p. .. .... . .
.Ppo . .pPO3Op.. . O:. . . .
.3Pp . oP3'. 'P33. . 4 .. . . . .. . . .
.~OP 3PO. .Op3 : . .. _____ _____ _____
.P3O . oP3oP3O3P' . . . . / /./ /./ /
O3:. O3p~ . .:. . ./____/./____/ /____/
'P . 3p3. oP3~. ..P:. . . .. . . .. . . .
. ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . .
. '_ .. . . _OP3.. . .https://openpype.io.. .
~P3.OPPPO3OP~ . .. .
. ' '. . .. . . . .. .
"@

View file

@ -32,17 +32,17 @@ $art = @"
. . .. . ..
_oOOP3OPP3Op_. .
.PPpo~· ·· ~2p. ·· ···· · ·
·Ppo · .pPO3Op.· · O:· · · ·
.3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · ·
·~OP 3PO· .Op3 : · ·· _____ _____ _____
·P3O · oP3oP3O3P' · · · · / /·/ /·/ /
O3:· O3p~ · ·:· · ·/____/·/____/ /____/
'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · ·
· ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · ·
· '_ .. · . _OP3·· · ·https://openpype.io·· ·
~P3·OPPPO3OP~ · ·· ·
· ' '· · ·· · · · ·· ·
.PPpo~. .. ~2p. .. .... . .
.Ppo . .pPO3Op.. . O:. . . .
.3Pp . oP3'. 'P33. . 4 .. . . . .. . . .
.~OP 3PO. .Op3 : . .. _____ _____ _____
.P3O . oP3oP3O3P' . . . . / /./ /./ /
O3:. O3p~ . .:. . ./____/./____/ /____/
'P . 3p3. oP3~. ..P:. . . .. . . .. . . .
. ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . .
. '_ .. . . _OP3.. . .https://openpype.io.. .
~P3.OPPPO3OP~ . .. .
. ' '. . .. . . . .. .
"@

View file

@ -1,23 +1,38 @@
<#
.SYNOPSIS
Helper script to run mongodb.
Helper script to run Docusaurus for easy editing of OpenPype documentation.
.DESCRIPTION
This script will detect mongodb, add it to the PATH and launch it on specified port and db location.
This script is using `yarn` package manager to run Docusaurus. If you don't
have `yarn`, install Node.js (https://nodejs.org/) and then run:
npm install -g yarn
It take some time to run this script. If all is successful you should see
new browser window with OpenPype documentation. All changes is markdown files
under .\website should be immediately seen in browser.
.EXAMPLE
PS> .\run_mongo.ps1
PS> .\run_documentation.ps1
#>
$art = @"
.---= [ by Pype Club ] =---.
https://openpype.io
. . .. . ..
_oOOP3OPP3Op_. .
.PPpo~. .. ~2p. .. .... . .
.Ppo . .pPO3Op.. . O:. . . .
.3Pp . oP3'. 'P33. . 4 .. . . . .. . . .
.~OP 3PO. .Op3 : . .. _____ _____ _____
.P3O . oP3oP3O3P' . . . . / /./ /./ /
O3:. O3p~ . .:. . ./____/./____/ /____/
'P . 3p3. oP3~. ..P:. . . .. . . .. . . .
. ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . .
. '_ .. . . _OP3.. . .https://openpype.io.. .
~P3.OPPPO3OP~ . .. .
. ' '. . .. . . . .. .
"@
@ -26,7 +41,6 @@ Write-Host $art -ForegroundColor DarkGreen
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$openpype_root = (Get-Item $script_dir).parent.FullName
cd $openpype_root/website
yarn run start
Set-Location $openpype_root/website
& yarn run start

View file

@ -15,17 +15,17 @@ $art = @"
. . .. . ..
_oOOP3OPP3Op_. .
.PPpo~· ·· ~2p. ·· ···· · ·
·Ppo · .pPO3Op.· · O:· · · ·
.3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · ·
·~OP 3PO· .Op3 : · ·· _____ _____ _____
·P3O · oP3oP3O3P' · · · · / /·/ /·/ /
O3:· O3p~ · ·:· · ·/____/·/____/ /____/
'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · ·
· ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · ·
· '_ .. · . _OP3·· · ·https://openpype.io·· ·
~P3·OPPPO3OP~ · ·· ·
· ' '· · ·· · · · ·· ·
.PPpo~. .. ~2p. .. .... . .
.Ppo . .pPO3Op.. . O:. . . .
.3Pp . oP3'. 'P33. . 4 .. . . . .. . . .
.~OP 3PO. .Op3 : . .. _____ _____ _____
.P3O . oP3oP3O3P' . . . . / /./ /./ /
O3:. O3p~ . .:. . ./____/./____/ /____/
'P . 3p3. oP3~. ..P:. . . .. . . .. . . .
. ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . .
. '_ .. . . _OP3.. . .https://openpype.io.. .
~P3.OPPPO3OP~ . .. .
. ' '. . .. . . . .. .
"@

View file

@ -0,0 +1,18 @@
<#
.SYNOPSIS
Helper script to run Project Manager.
.DESCRIPTION
.EXAMPLE
PS> .\run_project_manager.ps1
#>
$current_dir = Get-Location
$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$openpype_root = (Get-Item $script_dir).parent.FullName
Set-Location -Path $openpype_root
& poetry run python "$($openpype_root)\start.py" projectmanager
Set-Location -Path $current_dir

View file

@ -34,17 +34,17 @@ $art = @"
. . .. . ..
_oOOP3OPP3Op_. .
.PPpo~· ·· ~2p. ·· ···· · ·
·Ppo · .pPO3Op.· · O:· · · ·
.3Pp · oP3'· 'P33· · 4 ·· · · · ·· · · ·
·~OP 3PO· .Op3 : · ·· _____ _____ _____
·P3O · oP3oP3O3P' · · · · / /·/ /·/ /
O3:· O3p~ · ·:· · ·/____/·/____/ /____/
'P · 3p3· oP3~· ·.P:· · · ·· · · ·· · · ·
· ': · Po' ·Opo'· .3O· . o[ by Pype Club ]]]==- - - · ·
· '_ .. · . _OP3·· · ·https://openpype.io·· ·
~P3·OPPPO3OP~ · ·· ·
· ' '· · ·· · · · ·· ·
.PPpo~. .. ~2p. .. .... . .
.Ppo . .pPO3Op.. . O:. . . .
.3Pp . oP3'. 'P33. . 4 .. . . . .. . . .
.~OP 3PO. .Op3 : . .. _____ _____ _____
.P3O . oP3oP3O3P' . . . . / /./ /./ /
O3:. O3p~ . .:. . ./____/./____/ /____/
'P . 3p3. oP3~. ..P:. . . .. . . .. . . .
. ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . .
. '_ .. . . _OP3.. . .https://openpype.io.. .
~P3.OPPPO3OP~ . .. .
. ' '. . .. . . . .. .
"@

View file

@ -0,0 +1,82 @@
---
id: artist_install
title: Installation
sidebar_label: Installation
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
## Installation
OpenPype comes in packages for Windows (10 or Server), Mac OS X (Mojave or higher), and Linux distribution (Centos, Ubuntu), and you can install them on your machine the same way as you are used to.
:::important
To install OpenPype you will need administrator permissions.
:::
:::note pick your platform
<Tabs
defaultValue='win'
values={[
{label: 'Windows', value: 'win'},
{label: 'Linux', value: 'linux'},
{label: 'Mac OS X', value: 'mac'},
]}>
<TabItem value='win'>
For installation on Windows, download and run the executable file `OpenPype-3.0.0.exe`.
During the installation process, you can change the destination location path of the application,
![Windows installation](assets/install_01.png)
and create an icon on the desktop.
![Windows create icon](assets/install_02.png)
</TabItem>
<TabItem value='linux'>
For installation on your Linux distribution, download and unzip `OpenPype-3.0.0.zip`. A new folder `OpenPype-3.0.0` will be created.
Inside this folder find and run `openpype_gui`,
![Linux launch](assets/install_03.png)
</TabItem>
<TabItem value='mac'>
For installation on Mac OS X, download and run dmg image file `OpenPype-3.0.0.dmg`.
Drag the OpenPype icon into the Application folder.
![Mac installation](assets/install_04.png)
After the installation, you can find OpenPype among the other Applications.
</TabItem>
</Tabs>
:::
## Run OpenPype
To run OpenPype click on the icon or find executable file (e.g. `C:\Program Files (x86)\OpenPype\openpype_gui.exe`) in the application location.
On the very first run of OpenPype the user will be asked for OpenPype Mongo URL.
This piece of information will be provided by the administrator or project manager who set up the studio.
![Mongo example](assets/install_05.png)
Once the Mongo URL address is entered, press `Start`, and OpenPype will be initiated.
OpenPype will also remember the connection for the next launch, so it is a one-time process.
:::note
If the launch was successful, the artist should see a turquoise OpenPype logo in their
tray menu. Keep in mind that on Windows this icon might be hidden by default, in which case, the artist can simply drag the icon down to the tray.
![Systray](assets/artist_systray.png)
:::

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

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