Merge branch 'develop' into feature/1377-hiero-publish-with-retiming

This commit is contained in:
Jakub Jezek 2021-05-27 16:47:30 +02:00
commit 0eeeb19ec8
No known key found for this signature in database
GPG key ID: D8548FBF690B100A
68 changed files with 5601 additions and 3153 deletions

View file

@ -0,0 +1,34 @@
import os
from openpype.lib import PreLaunchHook
class LaunchWithTerminal(PreLaunchHook):
"""Mac specific pre arguments for application.
Mac applications should be launched using "open" argument which is internal
callbacks to open executable. We also add argument "-a" to tell it's
application open. This is used only for executables ending with ".app". It
is expected that these executables lead to app packages.
"""
order = 1000
platforms = ["darwin"]
def execute(self):
executable = str(self.launch_context.executable)
# Skip executables not ending with ".app" or that are not folder
if not executable.endswith(".app") or not os.path.isdir(executable):
return
# Check if first argument match executable path
# - Few applications are not executed directly but through OpenPype
# process (Photoshop, AfterEffects, Harmony, ...). These should not
# use `open`.
if self.launch_context.launch_args[0] != executable:
return
# Tell `open` to pass arguments if there are any
if len(self.launch_context.launch_args) > 1:
self.launch_context.launch_args.insert(1, "--args")
# Prepend open arguments
self.launch_context.launch_args.insert(0, ["open", "-a"])

View file

@ -23,18 +23,32 @@ def add_implementation_envs(env, _app):
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 ""
previous_user_scripts = []
for path in blender_user_scripts.split(os.pathsep):
if path and os.path.exists(path):
path = os.path.normpath(path)
if path != implementation_user_script_path:
previous_user_scripts.append(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
)
env["BLENDER_USER_SCRIPTS"] = implementation_user_script_path
# Define Qt binding if not defined
if not env.get("QT_PREFERRED_BINDING"):

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

@ -1,25 +0,0 @@
import bpy
from avalon import api, blender
import openpype.hosts.blender.api.plugin
class CreateSetDress(openpype.hosts.blender.api.plugin.Creator):
"""A grouped package of loaded content"""
name = "setdressMain"
label = "Set Dress"
family = "setdress"
icon = "cubes"
defaults = ["Main", "Anim"]
def process(self):
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)
self.data['task'] = api.Session.get('AVALON_TASK')
blender.lib.imprint(collection, self.data)
return collection

View file

@ -25,9 +25,6 @@ class BlendLayoutLoader(plugin.AssetLoader):
icon = "code-fork"
color = "orange"
animation_creator_name = "CreateAnimation"
setdress_creator_name = "CreateSetDress"
def _remove(self, objects, obj_container):
for obj in list(objects):
if obj.type == 'ARMATURE':
@ -293,7 +290,6 @@ class UnrealLayoutLoader(plugin.AssetLoader):
color = "orange"
animation_creator_name = "CreateAnimation"
setdress_creator_name = "CreateSetDress"
def _remove_objects(self, objects):
for obj in list(objects):
@ -383,7 +379,7 @@ class UnrealLayoutLoader(plugin.AssetLoader):
def _process(
self, libpath, layout_container, container_name, representation,
actions, parent
actions, parent_collection
):
with open(libpath, "r") as fp:
data = json.load(fp)
@ -392,6 +388,11 @@ class UnrealLayoutLoader(plugin.AssetLoader):
layout_collection = bpy.data.collections.new(container_name)
scene.collection.children.link(layout_collection)
parent = parent_collection
if parent is None:
parent = scene.collection
all_loaders = api.discover(api.Loader)
avalon_container = bpy.data.collections.get(
@ -516,23 +517,9 @@ class UnrealLayoutLoader(plugin.AssetLoader):
container_metadata["libpath"] = libpath
container_metadata["lib_container"] = lib_container
# Create a setdress subset to contain all the animation for all
# the rigs in the layout
creator_plugin = get_creator_by_name(self.setdress_creator_name)
if not creator_plugin:
raise ValueError("Creator plugin \"{}\" was not found.".format(
self.setdress_creator_name
))
parent = api.create(
creator_plugin,
name="animation",
asset=api.Session["AVALON_ASSET"],
options={"useSelection": True},
data={"dependencies": str(context["representation"]["_id"])})
layout_collection = self._process(
libpath, layout_container, container_name,
str(context["representation"]["_id"]), None, parent)
str(context["representation"]["_id"]), None, None)
container_metadata["obj_container"] = layout_collection

View file

@ -107,6 +107,9 @@ class BlendRigLoader(plugin.AssetLoader):
if action is not None:
local_obj.animation_data.action = action
elif local_obj.animation_data.action is not None:
plugin.prepare_data(
local_obj.animation_data.action, collection_name)
# Set link the drivers to the local object
if local_obj.data.animation_data:

View file

@ -1,61 +0,0 @@
import os
import json
import openpype.api
import pyblish.api
import bpy
class ExtractSetDress(openpype.api.Extractor):
"""Extract setdress."""
label = "Extract SetDress"
hosts = ["blender"]
families = ["setdress"]
optional = True
order = pyblish.api.ExtractorOrder + 0.1
def process(self, instance):
stagingdir = self.staging_dir(instance)
json_data = []
for i in instance.context:
collection = i.data.get("name")
container = None
for obj in bpy.data.collections[collection].objects:
if obj.type == "ARMATURE":
container_name = obj.get("avalon").get("container_name")
container = bpy.data.collections[container_name]
if container:
json_dict = {
"subset": i.data.get("subset"),
"container": container.name,
}
json_dict["instance_name"] = container.get("avalon").get(
"instance_name"
)
json_data.append(json_dict)
if "representations" not in instance.data:
instance.data["representations"] = []
json_filename = f"{instance.name}.json"
json_path = os.path.join(stagingdir, json_filename)
with open(json_path, "w+") as file:
json.dump(json_data, fp=file, indent=2)
json_representation = {
"name": "json",
"ext": "json",
"files": json_filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(json_representation)
self.log.info(
"Extracted instance '{}' to: {}".format(instance.name,
json_representation)
)

View file

@ -1,4 +1,5 @@
import os
import json
import openpype.api
@ -121,6 +122,25 @@ class ExtractAnimationFBX(openpype.api.Extractor):
pair[1].user_clear()
bpy.data.actions.remove(pair[1])
json_filename = f"{instance.name}.json"
json_path = os.path.join(stagingdir, json_filename)
json_dict = {}
collection = instance.data.get("name")
container = None
for obj in bpy.data.collections[collection].objects:
if obj.type == "ARMATURE":
container_name = obj.get("avalon").get("container_name")
container = bpy.data.collections[container_name]
if container:
json_dict = {
"instance_name": container.get("avalon").get("instance_name")
}
with open(json_path, "w+") as file:
json.dump(json_dict, fp=file, indent=2)
if "representations" not in instance.data:
instance.data["representations"] = []
@ -130,7 +150,15 @@ class ExtractAnimationFBX(openpype.api.Extractor):
'files': fbx_filename,
"stagingDir": stagingdir,
}
json_representation = {
'name': 'json',
'ext': 'json',
'files': json_filename,
"stagingDir": stagingdir,
}
instance.data["representations"].append(fbx_representation)
instance.data["representations"].append(json_representation)
self.log.info("Extracted instance '{}' to: {}".format(
instance.name, fbx_representation))

View file

@ -1059,7 +1059,7 @@ class WorkfileSettings(object):
# replace reset resolution from avalon core to pype's
self.reset_frame_range_handles()
# add colorspace menu item
# self.set_colorspace()
self.set_colorspace()
def set_favorites(self):
work_dir = os.getenv("AVALON_WORKDIR")

View file

@ -1,6 +1,7 @@
from openpype.hosts.nuke.api.lib import (
on_script_load,
check_inventory_versions
check_inventory_versions,
WorkfileSettings
)
import nuke
@ -9,8 +10,14 @@ from openpype.api import Logger
log = Logger().get_logger(__name__)
nuke.addOnScriptSave(on_script_load)
# fix ffmpeg settings on script
nuke.addOnScriptLoad(on_script_load)
# set checker for last versions on loaded containers
nuke.addOnScriptLoad(check_inventory_versions)
nuke.addOnScriptSave(check_inventory_versions)
# # set apply all workfile settings on script load and save
nuke.addOnScriptLoad(WorkfileSettings().set_context_settings)
log.info('Automatic syncing of write file knob to script version')

View file

@ -19,6 +19,10 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
def on_instance_toggle(instance, old_value, new_value):
# Review may not have real instance in wokrfile metadata
if not instance.data.get("uuid"):
return
instance_id = instance.data["uuid"]
found_idx = None
current_instances = pipeline.list_instances()

View file

@ -1,19 +0,0 @@
from avalon.tvpaint import pipeline
from openpype.hosts.tvpaint.api import plugin
class CreateReview(plugin.Creator):
"""Review for global review of all layers."""
name = "review"
label = "Review"
family = "review"
icon = "cube"
defaults = ["Main"]
def process(self):
instances = pipeline.list_instances()
for instance in instances:
if instance["family"] == self.family:
self.log.info("Review family is already Created.")
return
super(CreateReview, self).process()

View file

@ -17,6 +17,20 @@ class CollectInstances(pyblish.api.ContextPlugin):
json.dumps(workfile_instances, indent=4)
))
# Backwards compatibility for workfiles that already have review
# instance in metadata.
review_instance_exist = False
for instance_data in workfile_instances:
if instance_data["family"] == "review":
review_instance_exist = True
break
# Fake review instance if review was not found in metadata families
if not review_instance_exist:
workfile_instances.append(
self._create_review_instance_data(context)
)
for instance_data in workfile_instances:
instance_data["fps"] = context.data["sceneFps"]
@ -90,6 +104,16 @@ class CollectInstances(pyblish.api.ContextPlugin):
instance, json.dumps(instance.data, indent=4)
))
def _create_review_instance_data(self, context):
"""Fake review instance data."""
return {
"family": "review",
"asset": context.data["workfile_context"]["asset"],
# Dummy subset name
"subset": "reviewMain"
}
def create_render_layer_instance(self, context, instance_data):
name = instance_data["name"]
# Change label

View file

@ -1,4 +1,5 @@
import os
import json
from avalon import api, pipeline
from avalon.unreal import lib
@ -61,10 +62,16 @@ class AnimationFBXLoader(api.Loader):
task = unreal.AssetImportTask()
task.options = unreal.FbxImportUI()
# If there are no options, the process cannot be automated
if options:
libpath = self.fname.replace("fbx", "json")
with open(libpath, "r") as fp:
data = json.load(fp)
instance_name = data.get("instance_name")
if instance_name:
automated = True
actor_name = 'PersistentLevel.' + options.get('instance_name')
actor_name = 'PersistentLevel.' + instance_name
actor = unreal.EditorLevelLibrary.get_actor_reference(actor_name)
skeleton = actor.skeletal_mesh_component.skeletal_mesh.skeleton
task.options.set_editor_property('skeleton', skeleton)
@ -81,16 +88,31 @@ class AnimationFBXLoader(api.Loader):
# set import options here
task.options.set_editor_property(
'automated_import_should_detect_type', True)
'automated_import_should_detect_type', False)
task.options.set_editor_property(
'original_import_type', unreal.FBXImportType.FBXIT_ANIMATION)
'original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH)
task.options.set_editor_property(
'mesh_type_to_import', unreal.FBXImportType.FBXIT_ANIMATION)
task.options.set_editor_property('import_mesh', False)
task.options.set_editor_property('import_animations', True)
task.options.set_editor_property('override_full_name', True)
task.options.skeletal_mesh_import_data.set_editor_property(
'import_content_type',
unreal.FBXImportContentType.FBXICT_SKINNING_WEIGHTS
task.options.anim_sequence_import_data.set_editor_property(
'animation_length',
unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME
)
task.options.anim_sequence_import_data.set_editor_property(
'import_meshes_in_bone_hierarchy', False)
task.options.anim_sequence_import_data.set_editor_property(
'use_default_sample_rate', True)
task.options.anim_sequence_import_data.set_editor_property(
'import_custom_attribute', True)
task.options.anim_sequence_import_data.set_editor_property(
'import_bone_tracks', True)
task.options.anim_sequence_import_data.set_editor_property(
'remove_redundant_keys', True)
task.options.anim_sequence_import_data.set_editor_property(
'convert_scene', True)
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])

View file

@ -1,127 +0,0 @@
import json
from avalon import api
import unreal
class AnimationCollectionLoader(api.Loader):
"""Load Unreal SkeletalMesh from FBX"""
families = ["setdress"]
representations = ["json"]
label = "Load Animation Collection"
icon = "cube"
color = "orange"
def load(self, context, name, namespace, options):
from avalon import api, pipeline
from avalon.unreal import lib
from avalon.unreal import pipeline as unreal_pipeline
import unreal
# Create directory for asset and avalon container
root = "/Game/Avalon/Assets"
asset = context.get('asset').get('name')
suffix = "_CON"
tools = unreal.AssetToolsHelpers().get_asset_tools()
asset_dir, container_name = tools.create_unique_asset_name(
"{}/{}".format(root, asset), suffix="")
container_name += suffix
unreal.EditorAssetLibrary.make_directory(asset_dir)
libpath = self.fname
with open(libpath, "r") as fp:
data = json.load(fp)
all_loaders = api.discover(api.Loader)
for element in data:
reference = element.get('_id')
loaders = api.loaders_from_representation(all_loaders, reference)
loader = None
for l in loaders:
if l.__name__ == "AnimationFBXLoader":
loader = l
break
if not loader:
continue
instance_name = element.get('instance_name')
api.load(
loader,
reference,
namespace=instance_name,
options=element
)
# Create Asset Container
lib.create_avalon_container(
container=container_name, path=asset_dir)
data = {
"schema": "openpype:container-2.0",
"id": pipeline.AVALON_CONTAINER_ID,
"asset": asset,
"namespace": asset_dir,
"container_name": container_name,
"loader": str(self.__class__.__name__),
"representation": context["representation"]["_id"],
"parent": context["representation"]["parent"],
"family": context["representation"]["context"]["family"]
}
unreal_pipeline.imprint(
"{}/{}".format(asset_dir, container_name), data)
asset_content = unreal.EditorAssetLibrary.list_assets(
asset_dir, recursive=True, include_folder=True
)
return asset_content
def update(self, container, representation):
from avalon import api, io
from avalon.unreal import pipeline
source_path = api.get_representation_path(representation)
with open(source_path, "r") as fp:
data = json.load(fp)
animation_containers = [
i for i in pipeline.ls() if
i.get('asset') == container.get('asset') and
i.get('family') == 'animation']
for element in data:
new_version = io.find_one({"_id": io.ObjectId(element.get('_id'))})
new_version_number = new_version.get('context').get('version')
anim_container = None
for i in animation_containers:
if i.get('container_name') == (element.get('subset') + "_CON"):
anim_container = i
break
if not anim_container:
continue
api.update(anim_container, new_version_number)
container_path = "{}/{}".format(container["namespace"],
container["objectName"])
# update metadata
pipeline.imprint(
container_path,
{
"representation": str(representation["_id"]),
"parent": str(representation["parent"])
})
def remove(self, container):
unreal.EditorAssetLibrary.delete_directory(container["namespace"])

View file

@ -440,7 +440,20 @@ class EnvironmentTool:
class ApplicationExecutable:
"""Representation of executable loaded from settings."""
def __init__(self, executable):
# On MacOS check if exists path to executable when ends with `.app`
# - it is common that path will lead to "/Applications/Blender" but
# real path is "/Applications/Blender.app"
if (
platform.system().lower() == "darwin"
and not os.path.exists(executable)
):
_executable = executable + ".app"
if os.path.exists(_executable):
executable = _executable
self.executable_path = executable
def __str__(self):
@ -1177,17 +1190,23 @@ def prepare_context_environments(data):
try:
workdir = get_workdir_with_workdir_data(workdir_data, anatomy)
if not os.path.exists(workdir):
log.debug(
"Creating workdir folder: \"{}\"".format(workdir)
)
os.makedirs(workdir)
except Exception as exc:
raise ApplicationLaunchFailed(
"Error in anatomy.format: {}".format(str(exc))
)
if not os.path.exists(workdir):
log.debug(
"Creating workdir folder: \"{}\"".format(workdir)
)
try:
os.makedirs(workdir)
except Exception as exc:
raise ApplicationLaunchFailed(
"Couldn't create workdir because: {}".format(str(exc))
)
context_env = {
"AVALON_PROJECT": project_doc["name"],
"AVALON_ASSET": asset_doc["name"],

303
openpype/lib/delivery.py Normal file
View file

@ -0,0 +1,303 @@
"""Functions useful for delivery action or loader"""
import os
import shutil
import clique
import collections
def collect_frames(files):
"""
Returns dict of source path and its frame, if from sequence
Uses clique as most precise solution
Args:
files(list): list of source paths
Returns:
(dict): {'/asset/subset_v001.0001.png': '0001', ....}
"""
collections, remainder = clique.assemble(files)
sources_and_frames = {}
if collections:
for collection in collections:
src_head = collection.head
src_tail = collection.tail
for index in collection.indexes:
src_frame = collection.format("{padding}") % index
src_file_name = "{}{}{}".format(src_head, src_frame,
src_tail)
sources_and_frames[src_file_name] = src_frame
else:
sources_and_frames[remainder.pop()] = None
return sources_and_frames
def sizeof_fmt(num, suffix='B'):
"""Returns formatted string with size in appropriate unit"""
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, 'Yi', suffix)
def path_from_represenation(representation, anatomy):
from avalon import pipeline # safer importing
try:
template = representation["data"]["template"]
except KeyError:
return None
try:
context = representation["context"]
context["root"] = anatomy.roots
path = pipeline.format_template_with_optional_keys(
context, template
)
except KeyError:
# Template references unavailable data
return None
return os.path.normpath(path)
def copy_file(src_path, dst_path):
"""Hardlink file if possible(to save space), copy if not"""
from avalon.vendor import filelink # safer importing
if os.path.exists(dst_path):
return
try:
filelink.create(
src_path,
dst_path,
filelink.HARDLINK
)
except OSError:
shutil.copyfile(src_path, dst_path)
def get_format_dict(anatomy, location_path):
"""Returns replaced root values from user provider value.
Args:
anatomy (Anatomy)
location_path (str): user provided value
Returns:
(dict): prepared for formatting of a template
"""
format_dict = {}
if location_path:
location_path = location_path.replace("\\", "/")
root_names = anatomy.root_names_from_templates(
anatomy.templates["delivery"]
)
if root_names is None:
format_dict["root"] = location_path
else:
format_dict["root"] = {}
for name in root_names:
format_dict["root"][name] = location_path
return format_dict
def check_destination_path(repre_id,
anatomy, anatomy_data,
datetime_data, template_name):
""" Try to create destination path based on 'template_name'.
In the case that path cannot be filled, template contains unmatched
keys, provide error message to filter out repre later.
Args:
anatomy (Anatomy)
anatomy_data (dict): context to fill anatomy
datetime_data (dict): values with actual date
template_name (str): to pick correct delivery template
Returns:
(collections.defauldict): {"TYPE_OF_ERROR":"ERROR_DETAIL"}
"""
anatomy_data.update(datetime_data)
anatomy_filled = anatomy.format_all(anatomy_data)
dest_path = anatomy_filled["delivery"][template_name]
report_items = collections.defaultdict(list)
sub_msg = None
if not dest_path.solved:
msg = (
"Missing keys in Representation's context"
" for anatomy template \"{}\"."
).format(template_name)
if dest_path.missing_keys:
keys = ", ".join(dest_path.missing_keys)
sub_msg = (
"Representation: {}<br>- Missing keys: \"{}\"<br>"
).format(repre_id, keys)
if dest_path.invalid_types:
items = []
for key, value in dest_path.invalid_types.items():
items.append("\"{}\" {}".format(key, str(value)))
keys = ", ".join(items)
sub_msg = (
"Representation: {}<br>"
"- Invalid value DataType: \"{}\"<br>"
).format(repre_id, keys)
report_items[msg].append(sub_msg)
return report_items
def process_single_file(
src_path, repre, anatomy, template_name, anatomy_data, format_dict,
report_items, log
):
"""Copy single file to calculated path based on template
Args:
src_path(str): path of source representation file
_repre (dict): full repre, used only in process_sequence, here only
as to share same signature
anatomy (Anatomy)
template_name (string): user selected delivery template name
anatomy_data (dict): data from repre to fill anatomy with
format_dict (dict): root dictionary with names and values
report_items (collections.defaultdict): to return error messages
log (Logger): for log printing
Returns:
(collections.defaultdict , int)
"""
if not os.path.exists(src_path):
msg = "{} doesn't exist for {}".format(src_path,
repre["_id"])
report_items["Source file was not found"].append(msg)
return report_items, 0
anatomy_filled = anatomy.format(anatomy_data)
if format_dict:
template_result = anatomy_filled["delivery"][template_name]
delivery_path = template_result.rootless.format(**format_dict)
else:
delivery_path = anatomy_filled["delivery"][template_name]
# context.representation could be .psd
delivery_path = delivery_path.replace("..", ".")
delivery_folder = os.path.dirname(delivery_path)
if not os.path.exists(delivery_folder):
os.makedirs(delivery_folder)
log.debug("Copying single: {} -> {}".format(src_path, delivery_path))
copy_file(src_path, delivery_path)
return report_items, 1
def process_sequence(
src_path, repre, anatomy, template_name, anatomy_data, format_dict,
report_items, log
):
""" For Pype2(mainly - works in 3 too) where representation might not
contain files.
Uses listing physical files (not 'files' on repre as a)might not be
present, b)might not be reliable for representation and copying them.
TODO Should be refactored when files are sufficient to drive all
representations.
Args:
src_path(str): path of source representation file
repre (dict): full representation
anatomy (Anatomy)
template_name (string): user selected delivery template name
anatomy_data (dict): data from repre to fill anatomy with
format_dict (dict): root dictionary with names and values
report_items (collections.defaultdict): to return error messages
log (Logger): for log printing
Returns:
(collections.defaultdict , int)
"""
if not os.path.exists(src_path):
msg = "{} doesn't exist for {}".format(src_path,
repre["_id"])
report_items["Source file was not found"].append(msg)
return report_items, 0
dir_path, file_name = os.path.split(str(src_path))
context = repre["context"]
ext = context.get("ext", context.get("representation"))
if not ext:
msg = "Source extension not found, cannot find collection"
report_items[msg].append(src_path)
log.warning("{} <{}>".format(msg, context))
return report_items, 0
ext = "." + ext
# context.representation could be .psd
ext = ext.replace("..", ".")
src_collections, remainder = clique.assemble(os.listdir(dir_path))
src_collection = None
for col in src_collections:
if col.tail != ext:
continue
src_collection = col
break
if src_collection is None:
msg = "Source collection of files was not found"
report_items[msg].append(src_path)
log.warning("{} <{}>".format(msg, src_path))
return report_items, 0
frame_indicator = "@####@"
anatomy_data["frame"] = frame_indicator
anatomy_filled = anatomy.format(anatomy_data)
if format_dict:
template_result = anatomy_filled["delivery"][template_name]
delivery_path = template_result.rootless.format(**format_dict)
else:
delivery_path = anatomy_filled["delivery"][template_name]
delivery_folder = os.path.dirname(delivery_path)
dst_head, dst_tail = delivery_path.split(frame_indicator)
dst_padding = src_collection.padding
dst_collection = clique.Collection(
head=dst_head,
tail=dst_tail,
padding=dst_padding
)
if not os.path.exists(delivery_folder):
os.makedirs(delivery_folder)
src_head = src_collection.head
src_tail = src_collection.tail
uploaded = 0
for index in src_collection.indexes:
src_padding = src_collection.format("{padding}") % index
src_file_name = "{}{}{}".format(src_head, src_padding, src_tail)
src = os.path.normpath(
os.path.join(dir_path, src_file_name)
)
dst_padding = dst_collection.format("{padding}") % index
dst = "{}{}{}".format(dst_head, dst_padding, dst_tail)
log.debug("Copying single: {} -> {}".format(src, dst))
copy_file(src, dst)
uploaded += 1
return report_items, uploaded

View file

@ -1101,9 +1101,6 @@ class SyncToAvalonEvent(BaseEvent):
# Parents, Hierarchy
ent_path_items = [ent["name"] for ent in ftrack_ent["link"]]
parents = ent_path_items[1:len(ent_path_items)-1:]
hierarchy = ""
if len(parents) > 0:
hierarchy = os.path.sep.join(parents)
# TODO logging
self.log.debug(
@ -1132,7 +1129,6 @@ class SyncToAvalonEvent(BaseEvent):
"ftrackId": ftrack_ent["id"],
"entityType": ftrack_ent.entity_type,
"parents": parents,
"hierarchy": hierarchy,
"tasks": {},
"visualParent": vis_par
}
@ -1975,14 +1971,9 @@ class SyncToAvalonEvent(BaseEvent):
if cur_par == parents:
continue
hierarchy = ""
if len(parents) > 0:
hierarchy = os.path.sep.join(parents)
if "data" not in self.updates[mongo_id]:
self.updates[mongo_id]["data"] = {}
self.updates[mongo_id]["data"]["parents"] = parents
self.updates[mongo_id]["data"]["hierarchy"] = hierarchy
# Skip custom attributes if didn't change
if not hier_cust_attrs_ids:

View file

@ -11,23 +11,28 @@ from avalon.api import AvalonMongoDB
class DeleteAssetSubset(BaseAction):
'''Edit meta data action.'''
#: Action identifier.
# Action identifier.
identifier = "delete.asset.subset"
#: Action label.
# Action label.
label = "Delete Asset/Subsets"
#: Action description.
# Action description.
description = "Removes from Avalon with all childs and asset from Ftrack"
icon = statics_icon("ftrack", "action_icons", "DeleteAsset.svg")
settings_key = "delete_asset_subset"
#: Db connection
dbcon = AvalonMongoDB()
# Db connection
dbcon = None
splitter = {"type": "label", "value": "---"}
action_data_by_id = {}
asset_prefix = "asset:"
subset_prefix = "subset:"
def __init__(self, *args, **kwargs):
self.dbcon = AvalonMongoDB()
super(DeleteAssetSubset, self).__init__(*args, **kwargs)
def discover(self, session, entities, event):
""" Validation """
task_ids = []
@ -446,7 +451,14 @@ class DeleteAssetSubset(BaseAction):
if len(assets_to_delete) > 0:
map_av_ftrack_id = spec_data["without_ftrack_id"]
# Prepare data when deleting whole avalon asset
avalon_assets = self.dbcon.find({"type": "asset"})
avalon_assets = self.dbcon.find(
{"type": "asset"},
{
"_id": 1,
"data.visualParent": 1,
"data.ftrackId": 1
}
)
avalon_assets_by_parent = collections.defaultdict(list)
for asset in avalon_assets:
asset_id = asset["_id"]
@ -537,11 +549,13 @@ class DeleteAssetSubset(BaseAction):
ftrack_proc_txt, ", ".join(ftrack_ids_to_delete)
))
ftrack_ents_to_delete = (
entities_by_link_len = (
self._filter_entities_to_delete(ftrack_ids_to_delete, session)
)
for entity in ftrack_ents_to_delete:
session.delete(entity)
for link_len in sorted(entities_by_link_len.keys(), reverse=True):
for entity in entities_by_link_len[link_len]:
session.delete(entity)
try:
session.commit()
except Exception:
@ -600,29 +614,11 @@ class DeleteAssetSubset(BaseAction):
joined_ids_to_delete
)
).all()
filtered = to_delete_entities[:]
while True:
changed = False
_filtered = filtered[:]
for entity in filtered:
entity_id = entity["id"]
entities_by_link_len = collections.defaultdict(list)
for entity in to_delete_entities:
entities_by_link_len[len(entity["link"])].append(entity)
for _entity in tuple(_filtered):
if entity_id == _entity["id"]:
continue
for _link in _entity["link"]:
if entity_id == _link["id"] and _entity in _filtered:
_filtered.remove(_entity)
changed = True
break
filtered = _filtered
if not changed:
break
return filtered
return entities_by_link_len
def report_handle(self, report_messages, project_name, event):
if not report_messages:

View file

@ -1,18 +1,20 @@
import os
import copy
import json
import shutil
import collections
import clique
from bson.objectid import ObjectId
from avalon import pipeline
from avalon.vendor import filelink
from openpype.api import Anatomy, config
from openpype.modules.ftrack.lib import BaseAction, statics_icon
from openpype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY
from openpype.lib.delivery import (
path_from_represenation,
get_format_dict,
check_destination_path,
process_single_file,
process_sequence
)
from avalon.api import AvalonMongoDB
@ -450,18 +452,7 @@ class Delivery(BaseAction):
anatomy = Anatomy(project_name)
format_dict = {}
if location_path:
location_path = location_path.replace("\\", "/")
root_names = anatomy.root_names_from_templates(
anatomy.templates["delivery"]
)
if root_names is None:
format_dict["root"] = location_path
else:
format_dict["root"] = {}
for name in root_names:
format_dict["root"][name] = location_path
format_dict = get_format_dict(anatomy, location_path)
datetime_data = config.get_datetime_data()
for repre in repres_to_deliver:
@ -471,41 +462,15 @@ class Delivery(BaseAction):
debug_msg += " with published path {}.".format(source_path)
self.log.debug(debug_msg)
# Get destination repre path
anatomy_data = copy.deepcopy(repre["context"])
anatomy_data.update(datetime_data)
anatomy_filled = anatomy.format_all(anatomy_data)
test_path = anatomy_filled["delivery"][anatomy_name]
repre_report_items = check_destination_path(repre["_id"],
anatomy,
anatomy_data,
datetime_data,
anatomy_name)
if not test_path.solved:
msg = (
"Missing keys in Representation's context"
" for anatomy template \"{}\"."
).format(anatomy_name)
if test_path.missing_keys:
keys = ", ".join(test_path.missing_keys)
sub_msg = (
"Representation: {}<br>- Missing keys: \"{}\"<br>"
).format(str(repre["_id"]), keys)
if test_path.invalid_types:
items = []
for key, value in test_path.invalid_types.items():
items.append("\"{}\" {}".format(key, str(value)))
keys = ", ".join(items)
sub_msg = (
"Representation: {}<br>"
"- Invalid value DataType: \"{}\"<br>"
).format(str(repre["_id"]), keys)
report_items[msg].append(sub_msg)
self.log.warning(
"{} Representation: \"{}\" Filled: <{}>".format(
msg, str(repre["_id"]), str(test_path)
)
)
if repre_report_items:
report_items.update(repre_report_items)
continue
# Get source repre path
@ -514,151 +479,28 @@ class Delivery(BaseAction):
if frame:
repre["context"]["frame"] = len(str(frame)) * "#"
repre_path = self.path_from_represenation(repre, anatomy)
repre_path = path_from_represenation(repre, anatomy)
# TODO add backup solution where root of path from component
# is repalced with root
# is replaced with root
args = (
repre_path,
repre,
anatomy,
anatomy_name,
anatomy_data,
format_dict,
report_items
report_items,
self.log
)
if not frame:
self.process_single_file(*args)
process_single_file(*args)
else:
self.process_sequence(*args)
process_sequence(*args)
return self.report(report_items)
def process_single_file(
self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict,
report_items
):
anatomy_filled = anatomy.format(anatomy_data)
if format_dict:
template_result = anatomy_filled["delivery"][anatomy_name]
delivery_path = template_result.rootless.format(**format_dict)
else:
delivery_path = anatomy_filled["delivery"][anatomy_name]
delivery_folder = os.path.dirname(delivery_path)
if not os.path.exists(delivery_folder):
os.makedirs(delivery_folder)
self.copy_file(repre_path, delivery_path)
def process_sequence(
self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict,
report_items
):
dir_path, file_name = os.path.split(str(repre_path))
base_name, ext = os.path.splitext(file_name)
file_name_items = None
if "#" in base_name:
file_name_items = [part for part in base_name.split("#") if part]
elif "%" in base_name:
file_name_items = base_name.split("%")
if not file_name_items:
msg = "Source file was not found"
report_items[msg].append(repre_path)
self.log.warning("{} <{}>".format(msg, repre_path))
return
src_collections, remainder = clique.assemble(os.listdir(dir_path))
src_collection = None
for col in src_collections:
if col.tail != ext:
continue
# skip if collection don't have same basename
if not col.head.startswith(file_name_items[0]):
continue
src_collection = col
break
if src_collection is None:
# TODO log error!
msg = "Source collection of files was not found"
report_items[msg].append(repre_path)
self.log.warning("{} <{}>".format(msg, repre_path))
return
frame_indicator = "@####@"
anatomy_data["frame"] = frame_indicator
anatomy_filled = anatomy.format(anatomy_data)
if format_dict:
template_result = anatomy_filled["delivery"][anatomy_name]
delivery_path = template_result.rootless.format(**format_dict)
else:
delivery_path = anatomy_filled["delivery"][anatomy_name]
delivery_folder = os.path.dirname(delivery_path)
dst_head, dst_tail = delivery_path.split(frame_indicator)
dst_padding = src_collection.padding
dst_collection = clique.Collection(
head=dst_head,
tail=dst_tail,
padding=dst_padding
)
if not os.path.exists(delivery_folder):
os.makedirs(delivery_folder)
src_head = src_collection.head
src_tail = src_collection.tail
for index in src_collection.indexes:
src_padding = src_collection.format("{padding}") % index
src_file_name = "{}{}{}".format(src_head, src_padding, src_tail)
src = os.path.normpath(
os.path.join(dir_path, src_file_name)
)
dst_padding = dst_collection.format("{padding}") % index
dst = "{}{}{}".format(dst_head, dst_padding, dst_tail)
self.copy_file(src, dst)
def path_from_represenation(self, representation, anatomy):
try:
template = representation["data"]["template"]
except KeyError:
return None
try:
context = representation["context"]
context["root"] = anatomy.roots
path = pipeline.format_template_with_optional_keys(
context, template
)
except KeyError:
# Template references unavailable data
return None
return os.path.normpath(path)
def copy_file(self, src_path, dst_path):
if os.path.exists(dst_path):
return
try:
filelink.create(
src_path,
dst_path,
filelink.HARDLINK
)
except OSError:
shutil.copyfile(src_path, dst_path)
def report(self, report_items):
"""Returns dict with final status of delivery (succes, fail etc.)."""
items = []
title = "Delivery report"
for msg, _items in report_items.items():

View file

@ -1237,12 +1237,8 @@ class SyncEntitiesFactory:
ent_path_items = [ent["name"] for ent in entity["link"]]
parents = ent_path_items[1:len(ent_path_items) - 1:]
hierarchy = ""
if len(parents) > 0:
hierarchy = os.path.sep.join(parents)
data["parents"] = parents
data["hierarchy"] = hierarchy
data["tasks"] = self.entities_dict[id].pop("tasks", {})
self.entities_dict[id]["final_entity"]["data"] = data
self.entities_dict[id]["final_entity"]["type"] = "asset"
@ -2169,8 +2165,6 @@ class SyncEntitiesFactory:
hierarchy = "/".join(parents)
self.entities_dict[ftrack_id][
"final_entity"]["data"]["parents"] = parents
self.entities_dict[ftrack_id][
"final_entity"]["data"]["hierarchy"] = hierarchy
_parents.append(self.entities_dict[ftrack_id]["name"])
for child_id in self.entities_dict[ftrack_id]["children"]:
@ -2181,7 +2175,6 @@ class SyncEntitiesFactory:
if "data" not in self.updates[mongo_id]:
self.updates[mongo_id]["data"] = {}
self.updates[mongo_id]["data"]["parents"] = parents
self.updates[mongo_id]["data"]["hierarchy"] = hierarchy
def prepare_project_changes(self):
ftrack_ent_dict = self.entities_dict[self.ft_project_id]

View file

@ -16,6 +16,18 @@ class LoginServerHandler(BaseHTTPRequestHandler):
self.login_callback = login_callback
BaseHTTPRequestHandler.__init__(self, *args, **kw)
def log_message(self, format_str, *args):
"""Override method of BaseHTTPRequestHandler.
Goal is to use `print` instead of `sys.stderr.write`
"""
# Change
print("%s - - [%s] %s\n" % (
self.client_address[0],
self.log_date_time_string(),
format_str % args
))
def do_GET(self):
'''Override to handle requests ourselves.'''
parsed_path = parse.urlparse(self.path)

View file

@ -0,0 +1,318 @@
from collections import defaultdict
import copy
from Qt import QtWidgets, QtCore, QtGui
from avalon import api, style
from avalon.api import AvalonMongoDB
from openpype.api import Anatomy, config
from openpype import resources
from openpype.lib.delivery import (
sizeof_fmt,
path_from_represenation,
get_format_dict,
check_destination_path,
process_single_file,
process_sequence,
collect_frames
)
class Delivery(api.SubsetLoader):
"""Export selected versions to folder structure from Template"""
is_multiple_contexts_compatible = True
sequence_splitter = "__sequence_splitter__"
representations = ["*"]
families = ["*"]
tool_names = ["library_loader"]
label = "Deliver Versions"
order = 35
icon = "upload"
color = "#d8d8d8"
def message(self, text):
msgBox = QtWidgets.QMessageBox()
msgBox.setText(text)
msgBox.setStyleSheet(style.load_stylesheet())
msgBox.setWindowFlags(
msgBox.windowFlags() | QtCore.Qt.FramelessWindowHint
)
msgBox.exec_()
def load(self, contexts, name=None, namespace=None, options=None):
try:
dialog = DeliveryOptionsDialog(contexts, self.log)
dialog.exec_()
except Exception:
self.log.error("Failed to deliver versions.", exc_info=True)
class DeliveryOptionsDialog(QtWidgets.QDialog):
"""Dialog to select template where to deliver selected representations."""
def __init__(self, contexts, log=None, parent=None):
super(DeliveryOptionsDialog, self).__init__(parent=parent)
project = contexts[0]["project"]["name"]
self.anatomy = Anatomy(project)
self._representations = None
self.log = log
self.currently_uploaded = 0
self.dbcon = AvalonMongoDB()
self.dbcon.Session["AVALON_PROJECT"] = project
self.dbcon.install()
self._set_representations(contexts)
self.setWindowTitle("OpenPype - Deliver versions")
icon = QtGui.QIcon(resources.pype_icon_filepath())
self.setWindowIcon(icon)
self.setWindowFlags(
QtCore.Qt.WindowCloseButtonHint |
QtCore.Qt.WindowMinimizeButtonHint
)
self.setStyleSheet(style.load_stylesheet())
dropdown = QtWidgets.QComboBox()
self.templates = self._get_templates(self.anatomy)
for name, _ in self.templates.items():
dropdown.addItem(name)
template_label = QtWidgets.QLabel()
template_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
template_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
root_line_edit = QtWidgets.QLineEdit()
repre_checkboxes_layout = QtWidgets.QFormLayout()
repre_checkboxes_layout.setContentsMargins(10, 5, 5, 10)
self._representation_checkboxes = {}
for repre in self._get_representation_names():
checkbox = QtWidgets.QCheckBox()
checkbox.setChecked(False)
self._representation_checkboxes[repre] = checkbox
checkbox.stateChanged.connect(self._update_selected_label)
repre_checkboxes_layout.addRow(repre, checkbox)
selected_label = QtWidgets.QLabel()
input_widget = QtWidgets.QWidget(self)
input_layout = QtWidgets.QFormLayout(input_widget)
input_layout.setContentsMargins(10, 15, 5, 5)
input_layout.addRow("Selected representations", selected_label)
input_layout.addRow("Delivery template", dropdown)
input_layout.addRow("Template value", template_label)
input_layout.addRow("Root", root_line_edit)
input_layout.addRow("Representations", repre_checkboxes_layout)
btn_delivery = QtWidgets.QPushButton("Deliver")
btn_delivery.setEnabled(bool(dropdown.currentText()))
progress_bar = QtWidgets.QProgressBar(self)
progress_bar.setMinimum = 0
progress_bar.setMaximum = 100
progress_bar.setVisible(False)
text_area = QtWidgets.QTextEdit()
text_area.setReadOnly(True)
text_area.setVisible(False)
text_area.setMinimumHeight(100)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(input_widget)
layout.addStretch(1)
layout.addWidget(btn_delivery)
layout.addWidget(progress_bar)
layout.addWidget(text_area)
self.selected_label = selected_label
self.template_label = template_label
self.dropdown = dropdown
self.root_line_edit = root_line_edit
self.progress_bar = progress_bar
self.text_area = text_area
self.btn_delivery = btn_delivery
self.files_selected, self.size_selected = \
self._get_counts(self._get_selected_repres())
self._update_selected_label()
self._update_template_value()
btn_delivery.clicked.connect(self.deliver)
dropdown.currentIndexChanged.connect(self._update_template_value)
def deliver(self):
"""Main method to loop through all selected representations"""
self.progress_bar.setVisible(True)
self.btn_delivery.setEnabled(False)
QtWidgets.QApplication.processEvents()
report_items = defaultdict(list)
selected_repres = self._get_selected_repres()
datetime_data = config.get_datetime_data()
template_name = self.dropdown.currentText()
format_dict = get_format_dict(self.anatomy, self.root_line_edit.text())
for repre in self._representations:
if repre["name"] not in selected_repres:
continue
repre_path = path_from_represenation(repre, self.anatomy)
anatomy_data = copy.deepcopy(repre["context"])
new_report_items = check_destination_path(str(repre["_id"]),
self.anatomy,
anatomy_data,
datetime_data,
template_name)
report_items.update(new_report_items)
if new_report_items:
continue
args = [
repre_path,
repre,
self.anatomy,
template_name,
anatomy_data,
format_dict,
report_items,
self.log
]
if repre.get("files"):
src_paths = []
for repre_file in repre["files"]:
src_path = self.anatomy.fill_root(repre_file["path"])
src_paths.append(src_path)
sources_and_frames = collect_frames(src_paths)
for src_path, frame in sources_and_frames.items():
args[0] = src_path
if frame:
anatomy_data["frame"] = frame
new_report_items, uploaded = process_single_file(*args)
report_items.update(new_report_items)
self._update_progress(uploaded)
else: # fallback for Pype2 and representations without files
frame = repre['context'].get('frame')
if frame:
repre["context"]["frame"] = len(str(frame)) * "#"
if not frame:
new_report_items, uploaded = process_single_file(*args)
else:
new_report_items, uploaded = process_sequence(*args)
report_items.update(new_report_items)
self._update_progress(uploaded)
self.text_area.setText(self._format_report(report_items))
self.text_area.setVisible(True)
def _get_representation_names(self):
"""Get set of representation names for checkbox filtering."""
return set([repre["name"] for repre in self._representations])
def _get_templates(self, anatomy):
"""Adds list of delivery templates from Anatomy to dropdown."""
templates = {}
for template_name, value in anatomy.templates["delivery"].items():
if not isinstance(value, str) or not value.startswith('{root'):
continue
templates[template_name] = value
return templates
def _set_representations(self, contexts):
version_ids = [context["version"]["_id"] for context in contexts]
repres = list(self.dbcon.find({
"type": "representation",
"parent": {"$in": version_ids}
}))
self._representations = repres
def _get_counts(self, selected_repres=None):
"""Returns tuple of number of selected files and their size."""
files_selected = 0
size_selected = 0
for repre in self._representations:
if repre["name"] in selected_repres:
files = repre.get("files", [])
if not files: # for repre without files, cannot divide by 0
files_selected += 1
size_selected += 0
else:
for repre_file in files:
files_selected += 1
size_selected += repre_file["size"]
return files_selected, size_selected
def _prepare_label(self):
"""Provides text with no of selected files and their size."""
label = "{} files, size {}".format(self.files_selected,
sizeof_fmt(self.size_selected))
return label
def _get_selected_repres(self):
"""Returns list of representation names filtered from checkboxes."""
selected_repres = []
for repre_name, chckbox in self._representation_checkboxes.items():
if chckbox.isChecked():
selected_repres.append(repre_name)
return selected_repres
def _update_selected_label(self):
"""Updates label with list of number of selected files."""
selected_repres = self._get_selected_repres()
self.files_selected, self.size_selected = \
self._get_counts(selected_repres)
self.selected_label.setText(self._prepare_label())
def _update_template_value(self, _index=None):
"""Sets template value to label after selection in dropdown."""
name = self.dropdown.currentText()
template_value = self.templates.get(name)
if template_value:
self.btn_delivery.setEnabled(True)
self.template_label.setText(template_value)
def _update_progress(self, uploaded):
"""Update progress bar after each repre copied."""
self.currently_uploaded += uploaded
ratio = self.currently_uploaded / self.files_selected
self.progress_bar.setValue(ratio * self.progress_bar.maximum())
def _format_report(self, report_items):
"""Format final result and error details as html."""
msg = "Delivery finished"
if not report_items:
msg += " successfully"
else:
msg += " with errors"
txt = "<h2>{}</h2>".format(msg)
for header, data in report_items.items():
txt += "<h3>{}</h3>".format(header)
for item in data:
txt += "{}<br>".format(item)
return txt

View file

@ -56,14 +56,14 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
audio_inputs.insert(0, empty)
# create cmd
cmd = self.ffmpeg_path + " "
cmd = '"{}"'.format(self.ffmpeg_path) + " "
cmd += self.create_cmd(audio_inputs)
cmd += audio_temp_fpath
cmd += "\"{}\"".format(audio_temp_fpath)
# run subprocess
self.log.debug("Executing: {}".format(cmd))
openpype.api.run_subprocess(
cmd, shell=True, logger=self.log
cmd, logger=self.log
)
# remove empty
@ -100,17 +100,17 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
audio_fpath = self.create_temp_file(name)
cmd = " ".join([
self.ffmpeg_path,
'"{}"'.format(self.ffmpeg_path),
"-ss {}".format(start_sec),
"-t {}".format(duration_sec),
"-i {}".format(audio_file),
"-i \"{}\"".format(audio_file),
audio_fpath
])
# run subprocess
self.log.debug("Executing: {}".format(cmd))
openpype.api.run_subprocess(
cmd, shell=True, logger=self.log
cmd, logger=self.log
)
else:
audio_fpath = recycling_file.pop()
@ -221,11 +221,11 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
# create empty cmd
cmd = " ".join([
self.ffmpeg_path,
'"{}"'.format(self.ffmpeg_path),
"-f lavfi",
"-i anullsrc=channel_layout=stereo:sample_rate=48000",
"-t {}".format(max_duration_sec),
empty_fpath
"\"{}\"".format(empty_fpath)
])
# generate empty with ffmpeg
@ -233,7 +233,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
self.log.debug("Executing: {}".format(cmd))
openpype.api.run_subprocess(
cmd, shell=True, logger=self.log
cmd, logger=self.log
)
# return dict with output

View file

@ -209,7 +209,7 @@ class ExtractOTIOReview(openpype.api.Extractor):
"frameStart": start,
"frameEnd": end,
"stagingDir": self.staging_dir,
"tags": ["review", "ftrackreview", "delete"]
"tags": ["review", "delete"]
}
collection = clique.Collection(
@ -313,7 +313,7 @@ class ExtractOTIOReview(openpype.api.Extractor):
out_frame_start += end_offset
# start command list
command = [ffmpeg_path]
command = ['"{}"'.format(ffmpeg_path)]
if sequence:
input_dir, collection = sequence
@ -326,7 +326,7 @@ class ExtractOTIOReview(openpype.api.Extractor):
# form command for rendering gap files
command.extend([
"-start_number {}".format(in_frame_start),
"-i {}".format(input_path)
"-i \"{}\"".format(input_path)
])
elif video:
@ -341,7 +341,7 @@ class ExtractOTIOReview(openpype.api.Extractor):
command.extend([
"-ss {}".format(sec_start),
"-t {}".format(sec_duration),
"-i {}".format(video_path)
"-i \"{}\"".format(video_path)
])
elif gap:
@ -360,11 +360,13 @@ class ExtractOTIOReview(openpype.api.Extractor):
# add output attributes
command.extend([
"-start_number {}".format(out_frame_start),
output_path
"\"{}\"".format(output_path)
])
# execute
self.log.debug("Executing: {}".format(" ".join(command)))
output = openpype.api.run_subprocess(" ".join(command), shell=True)
output = openpype.api.run_subprocess(
" ".join(command), logger=self.log
)
self.log.debug("Output: {}".format(output))
def _generate_used_frames(self, duration, end_offset=None):

View file

@ -48,6 +48,8 @@ class ExtractReview(pyblish.api.InstancePlugin):
video_exts = ["mov", "mp4"]
supported_exts = image_exts + video_exts
alpha_exts = ["exr", "png", "dpx"]
# FFmpeg tools paths
ffmpeg_path = get_ffmpeg_tool_path("ffmpeg")
@ -296,6 +298,13 @@ class ExtractReview(pyblish.api.InstancePlugin):
):
with_audio = False
input_is_sequence = self.input_is_sequence(repre)
input_allow_bg = False
if input_is_sequence and repre["files"]:
ext = os.path.splitext(repre["files"][0])[1].replace(".", "")
if ext in self.alpha_exts:
input_allow_bg = True
return {
"fps": float(instance.data["fps"]),
"frame_start": frame_start,
@ -310,7 +319,8 @@ class ExtractReview(pyblish.api.InstancePlugin):
"resolution_width": instance.data.get("resolutionWidth"),
"resolution_height": instance.data.get("resolutionHeight"),
"origin_repre": repre,
"input_is_sequence": self.input_is_sequence(repre),
"input_is_sequence": input_is_sequence,
"input_allow_bg": input_allow_bg,
"with_audio": with_audio,
"without_handles": without_handles,
"handles_are_set": handles_are_set
@ -470,6 +480,39 @@ class ExtractReview(pyblish.api.InstancePlugin):
lut_filters = self.lut_filters(new_repre, instance, ffmpeg_input_args)
ffmpeg_video_filters.extend(lut_filters)
bg_alpha = 0
bg_color = output_def.get("bg_color")
if bg_color:
bg_red, bg_green, bg_blue, bg_alpha = bg_color
if bg_alpha > 0:
if not temp_data["input_allow_bg"]:
self.log.info((
"Output definition has defined BG color input was"
" resolved as does not support adding BG."
))
else:
bg_color_hex = "#{0:0>2X}{1:0>2X}{2:0>2X}".format(
bg_red, bg_green, bg_blue
)
bg_color_alpha = float(bg_alpha) / 255
bg_color_str = "{}@{}".format(bg_color_hex, bg_color_alpha)
self.log.info("Applying BG color {}".format(bg_color_str))
color_args = [
"split=2[bg][fg]",
"[bg]drawbox=c={}:replace=1:t=fill[bg]".format(
bg_color_str
),
"[bg][fg]overlay=format=auto"
]
# Prepend bg color change before all video filters
# NOTE at the time of creation it is required as video filters
# from settings may affect color of BG
# e.g. `eq` can remove alpha from input
for arg in reversed(color_args):
ffmpeg_video_filters.insert(0, arg)
# Add argument to override output file
ffmpeg_output_args.append("-y")
@ -547,10 +590,12 @@ class ExtractReview(pyblish.api.InstancePlugin):
all_args.append("\"{}\"".format(self.ffmpeg_path))
all_args.extend(input_args)
if video_filters:
all_args.append("-filter:v {}".format(",".join(video_filters)))
all_args.append("-filter:v")
all_args.append("\"{}\"".format(",".join(video_filters)))
if audio_filters:
all_args.append("-filter:a {}".format(",".join(audio_filters)))
all_args.append("-filter:a")
all_args.append("\"{}\"".format(",".join(audio_filters)))
all_args.extend(output_args)

View file

@ -37,11 +37,11 @@
"ftrackreview"
],
"ffmpeg_args": {
"video_filters": [
"eq=gamma=2.2"
],
"video_filters": [],
"audio_filters": [],
"input": [],
"input": [
"-apply_trc gamma22"
],
"output": [
"-pix_fmt yuv420p",
"-crf 18",
@ -57,6 +57,12 @@
},
"width": 0,
"height": 0,
"bg_color": [
0,
0,
0,
0
],
"letter_box": {
"enabled": false,
"ratio": 0.0,

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:

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

@ -193,6 +193,15 @@
"minimum": 0,
"maximum": 100000
},
{
"type": "label",
"label": "Background color is used only when input have transparency and Alpha is higher than 0."
},
{
"type": "color",
"label": "Background color",
"key": "bg_color"
},
{
"key": "letter_box",
"label": "Letter box",
@ -228,14 +237,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 +249,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"
}
]
}
@ -290,24 +289,14 @@
"minimum": 0
},
{
"type": "schema_template",
"name": "template_rgba_color",
"template_data": [
{
"label": "Font Color",
"name": "font_color"
}
]
"type": "color",
"key": "font_color",
"label": "Font Color"
},
{
"type": "schema_template",
"name": "template_rgba_color",
"template_data": [
{
"label": "Background Color",
"name": "bg_color"
}
]
"type": "color",
"key": "bg_color",
"label": "Background Color"
},
{
"type": "number",

View file

@ -1,33 +0,0 @@
[
{
"type": "list-strict",
"key": "{name}",
"label": "{label}",
"object_types": [
{
"label": "R",
"type": "number",
"minimum": 0,
"maximum": 255
},
{
"label": "G",
"type": "number",
"minimum": 0,
"maximum": 255
},
{
"label": "B",
"type": "number",
"minimum": 0,
"maximum": 255
},
{
"label": "A",
"type": "number",
"minimum": 0,
"maximum": 255
}
]
}
]

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

@ -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-rc.5"
__version__ = "3.0.0-rc.6"

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