mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into release/3.15.x
This commit is contained in:
commit
26ee6ade64
68 changed files with 1384 additions and 301 deletions
|
|
@ -1,5 +1,4 @@
|
|||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IHostAddon
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
|
||||
|
||||
class AfterEffectsAddon(OpenPypeModule, IHostAddon):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IHostAddon
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
|
||||
BLENDER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IHostAddon
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
|
||||
HOST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IHostAddon
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
|
||||
FUSION_HOST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IHostAddon
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
|
||||
HARMONY_HOST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import os
|
||||
import platform
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IHostAddon
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
|
||||
HIERO_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IHostAddon
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
|
||||
HOUDINI_HOST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IHostAddon
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
|
||||
MAYA_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import os
|
||||
import platform
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IHostAddon
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
|
||||
NUKE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IHostAddon
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
|
||||
PHOTOSHOP_HOST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import os
|
||||
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IHostAddon
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
|
||||
from .utils import RESOLVE_ROOT_DIR
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import click
|
|||
|
||||
from openpype.lib import get_openpype_execute_args
|
||||
from openpype.lib.execute import run_detached_process
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import ITrayAction, IHostAddon
|
||||
from openpype.modules import OpenPypeModule, ITrayAction, IHostAddon
|
||||
|
||||
STANDALONEPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import click
|
|||
|
||||
from openpype.lib import get_openpype_execute_args
|
||||
from openpype.lib.execute import run_detached_process
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import ITrayAction, IHostAddon
|
||||
from openpype.modules import OpenPypeModule, ITrayAction, IHostAddon
|
||||
|
||||
TRAYPUBLISH_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from openpype.pipeline.create import (
|
|||
HiddenCreator,
|
||||
CreatedInstance,
|
||||
cache_and_get_instances,
|
||||
PRE_CREATE_THUMBNAIL_KEY,
|
||||
)
|
||||
from .pipeline import (
|
||||
list_instances,
|
||||
|
|
@ -92,11 +93,14 @@ class TrayPublishCreator(Creator):
|
|||
|
||||
class SettingsCreator(TrayPublishCreator):
|
||||
create_allow_context_change = True
|
||||
create_allow_thumbnail = True
|
||||
|
||||
extensions = []
|
||||
|
||||
def create(self, subset_name, data, pre_create_data):
|
||||
# Pass precreate data to creator attributes
|
||||
thumbnail_path = pre_create_data.pop(PRE_CREATE_THUMBNAIL_KEY, None)
|
||||
|
||||
data["creator_attributes"] = pre_create_data
|
||||
data["settings_creator"] = True
|
||||
# Create new instance
|
||||
|
|
@ -104,6 +108,9 @@ class SettingsCreator(TrayPublishCreator):
|
|||
|
||||
self._store_new_instance(new_instance)
|
||||
|
||||
if thumbnail_path:
|
||||
self.set_instance_thumbnail_path(new_instance.id, thumbnail_path)
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
return [
|
||||
FileDef(
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ class CollectMovieBatch(
|
|||
if creator_attributes["add_review_family"]:
|
||||
repre["tags"].append("review")
|
||||
instance.data["families"].append("review")
|
||||
instance.data["thumbnailSource"] = file_url
|
||||
if not instance.data.get("thumbnailSource"):
|
||||
instance.data["thumbnailSource"] = file_url
|
||||
|
||||
instance.data["source"] = file_url
|
||||
|
||||
|
|
|
|||
|
|
@ -188,7 +188,8 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin):
|
|||
if "review" not in instance.data["families"]:
|
||||
instance.data["families"].append("review")
|
||||
|
||||
instance.data["thumbnailSource"] = first_filepath
|
||||
if not instance.data.get("thumbnailSource"):
|
||||
instance.data["thumbnailSource"] = first_filepath
|
||||
|
||||
review_representation["tags"].append("review")
|
||||
self.log.debug("Representation {} was marked for review. {}".format(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IHostAddon
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
|
||||
TVPAINT_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IHostAddon
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
|
||||
UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import os
|
|||
|
||||
import click
|
||||
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IHostAddon
|
||||
from openpype.modules import OpenPypeModule, IHostAddon
|
||||
|
||||
WEBPUBLISHER_ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ XML_CHAR_REF_REGEX_HEX = re.compile(r"&#x?[0-9a-fA-F]+;")
|
|||
# Regex to parse array attributes
|
||||
ARRAY_TYPE_REGEX = re.compile(r"^(int|float|string)\[\d+\]$")
|
||||
|
||||
IMAGE_EXTENSIONS = [
|
||||
IMAGE_EXTENSIONS = {
|
||||
".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", ".cal",
|
||||
".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", ".fits",
|
||||
".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", ".icer",
|
||||
|
|
@ -54,15 +54,15 @@ IMAGE_EXTENSIONS = [
|
|||
".rgbe", ".logluv", ".tiff", ".sgi", ".tga", ".tiff", ".tiff/ep",
|
||||
".tiff/it", ".ufo", ".ufp", ".wbmp", ".webp", ".xbm", ".xcf",
|
||||
".xpm", ".xwd"
|
||||
]
|
||||
}
|
||||
|
||||
VIDEO_EXTENSIONS = [
|
||||
VIDEO_EXTENSIONS = {
|
||||
".3g2", ".3gp", ".amv", ".asf", ".avi", ".drc", ".f4a", ".f4b",
|
||||
".f4p", ".f4v", ".flv", ".gif", ".gifv", ".m2v", ".m4p", ".m4v",
|
||||
".mkv", ".mng", ".mov", ".mp2", ".mp4", ".mpe", ".mpeg", ".mpg",
|
||||
".mpv", ".mxf", ".nsv", ".ogg", ".ogv", ".qt", ".rm", ".rmvb",
|
||||
".roq", ".svi", ".vob", ".webm", ".wmv", ".yuv"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def get_transcode_temp_directory():
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from .interfaces import (
|
||||
ILaunchHookPaths,
|
||||
IPluginPaths,
|
||||
ITrayModule,
|
||||
ITrayAction,
|
||||
ITrayService,
|
||||
ISettingsChangeListener,
|
||||
IHostAddon,
|
||||
)
|
||||
|
||||
from .base import (
|
||||
OpenPypeModule,
|
||||
OpenPypeAddOn,
|
||||
|
|
@ -17,6 +27,14 @@ from .base import (
|
|||
|
||||
|
||||
__all__ = (
|
||||
"ILaunchHookPaths",
|
||||
"IPluginPaths",
|
||||
"ITrayModule",
|
||||
"ITrayAction",
|
||||
"ITrayService",
|
||||
"ISettingsChangeListener",
|
||||
"IHostAddon",
|
||||
|
||||
"OpenPypeModule",
|
||||
"OpenPypeAddOn",
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import os
|
||||
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayModule
|
||||
from openpype.modules import OpenPypeModule, ITrayModule
|
||||
|
||||
|
||||
class AvalonModule(OpenPypeModule, ITrayModule):
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import logging
|
|||
import platform
|
||||
import threading
|
||||
import collections
|
||||
import traceback
|
||||
from uuid import uuid4
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import six
|
||||
|
|
@ -139,6 +140,15 @@ class _InterfacesClass(_ModuleClass):
|
|||
"cannot import name '{}' from 'openpype_interfaces'"
|
||||
).format(attr_name))
|
||||
|
||||
if _LoadCache.interfaces_loaded and attr_name != "log":
|
||||
stack = list(traceback.extract_stack())
|
||||
stack.pop(-1)
|
||||
self.log.warning((
|
||||
"Using deprecated import of \"{}\" from 'openpype_interfaces'."
|
||||
" Please switch to use import"
|
||||
" from 'openpype.modules.interfaces'"
|
||||
" (will be removed after 3.16.x).{}"
|
||||
).format(attr_name, "".join(traceback.format_list(stack))))
|
||||
return self.__attributes__[attr_name]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,17 @@ import os
|
|||
import threading
|
||||
import time
|
||||
|
||||
from openpype.modules import (
|
||||
OpenPypeModule,
|
||||
ITrayModule,
|
||||
IPluginPaths
|
||||
)
|
||||
|
||||
from .clockify_api import ClockifyAPI
|
||||
from .constants import (
|
||||
CLOCKIFY_FTRACK_USER_PATH,
|
||||
CLOCKIFY_FTRACK_SERVER_PATH
|
||||
)
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import (
|
||||
ITrayModule,
|
||||
IPluginPaths
|
||||
)
|
||||
|
||||
|
||||
class ClockifyModule(
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import six
|
|||
import sys
|
||||
|
||||
from openpype.lib import requests_get, Logger
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import IPluginPaths
|
||||
from openpype.modules import OpenPypeModule, IPluginPaths
|
||||
|
||||
|
||||
class DeadlineWebserviceError(Exception):
|
||||
|
|
|
|||
|
|
@ -13,10 +13,7 @@ import click
|
|||
from openpype.modules import (
|
||||
JsonFilesSettingsDef,
|
||||
OpenPypeAddOn,
|
||||
ModulesManager
|
||||
)
|
||||
# Import interface defined by this addon to be able find other addons using it
|
||||
from openpype_interfaces import (
|
||||
ModulesManager,
|
||||
IPluginPaths,
|
||||
ITrayAction
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import platform
|
|||
|
||||
import click
|
||||
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import (
|
||||
from openpype.modules import (
|
||||
OpenPypeModule,
|
||||
ITrayModule,
|
||||
IPluginPaths,
|
||||
ISettingsChangeListener
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@
|
|||
import click
|
||||
import os
|
||||
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import IPluginPaths, ITrayAction
|
||||
from openpype.modules import (
|
||||
OpenPypeModule,
|
||||
IPluginPaths,
|
||||
ITrayAction,
|
||||
)
|
||||
|
||||
|
||||
class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayAction
|
||||
from openpype.modules import (
|
||||
OpenPypeModule,
|
||||
ITrayAction,
|
||||
)
|
||||
|
||||
|
||||
class LauncherAction(OpenPypeModule, ITrayAction):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayModule
|
||||
from openpype.modules import OpenPypeModule, ITrayModule
|
||||
|
||||
|
||||
class LogViewModule(OpenPypeModule, ITrayModule):
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import os
|
|||
import json
|
||||
import appdirs
|
||||
import requests
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayModule
|
||||
from openpype.modules import OpenPypeModule, ITrayModule
|
||||
|
||||
|
||||
class MusterModule(OpenPypeModule, ITrayModule):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayAction
|
||||
from openpype.modules import OpenPypeModule, ITrayAction
|
||||
|
||||
|
||||
class ProjectManagerAction(OpenPypeModule, ITrayAction):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayAction
|
||||
from openpype.modules import OpenPypeModule, ITrayAction
|
||||
|
||||
|
||||
class PythonInterpreterAction(OpenPypeModule, ITrayAction):
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
"""Module providing support for Royal Render."""
|
||||
import os
|
||||
import openpype.modules
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import IPluginPaths
|
||||
from openpype.modules import OpenPypeModule, IPluginPaths
|
||||
|
||||
|
||||
class RoyalRenderModule(OpenPypeModule, IPluginPaths):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayAction
|
||||
from openpype.modules import OpenPypeModule, ITrayAction
|
||||
|
||||
|
||||
class SettingsAction(OpenPypeModule, ITrayAction):
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import os
|
||||
|
||||
from openpype_interfaces import (
|
||||
from openpype.modules import (
|
||||
OpenPypeModule,
|
||||
ITrayModule,
|
||||
IPluginPaths,
|
||||
)
|
||||
|
||||
from openpype.modules import OpenPypeModule
|
||||
|
||||
SHOTGRID_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import os
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype.modules.interfaces import IPluginPaths
|
||||
from openpype.modules import OpenPypeModule, IPluginPaths
|
||||
|
||||
SLACK_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,12 @@ from collections import deque, defaultdict
|
|||
import click
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
from openpype.client import get_projects
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayModule
|
||||
from openpype.client import (
|
||||
get_projects,
|
||||
get_representations,
|
||||
get_representation_by_id,
|
||||
)
|
||||
from openpype.modules import OpenPypeModule, ITrayModule
|
||||
from openpype.settings import (
|
||||
get_project_settings,
|
||||
get_system_settings,
|
||||
|
|
@ -30,9 +33,6 @@ from .providers import lib
|
|||
|
||||
from .utils import time_function, SyncStatus, SiteAlreadyPresentError
|
||||
|
||||
from openpype.client import get_representations, get_representation_by_id
|
||||
|
||||
|
||||
log = Logger.get_logger("SyncServer")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import platform
|
|||
|
||||
|
||||
from openpype.client import get_asset_by_name
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import (
|
||||
from openpype.modules import (
|
||||
OpenPypeModule,
|
||||
ITrayService,
|
||||
IPluginPaths
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import logging
|
|||
from concurrent.futures import CancelledError
|
||||
from Qt import QtWidgets
|
||||
|
||||
from openpype_interfaces import ITrayService
|
||||
from openpype.modules import ITrayService
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,7 @@ import os
|
|||
import socket
|
||||
|
||||
from openpype import resources
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayService
|
||||
from openpype.modules import OpenPypeModule, ITrayService
|
||||
|
||||
|
||||
class WebServerModule(OpenPypeModule, ITrayService):
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ from .context_tools import (
|
|||
register_host,
|
||||
registered_host,
|
||||
deregister_host,
|
||||
get_process_id,
|
||||
)
|
||||
install = install_host
|
||||
uninstall = uninstall_host
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import json
|
|||
import types
|
||||
import logging
|
||||
import platform
|
||||
import uuid
|
||||
|
||||
import pyblish.api
|
||||
from pyblish.lib import MessageHandler
|
||||
|
|
@ -37,6 +38,7 @@ from . import (
|
|||
|
||||
|
||||
_is_installed = False
|
||||
_process_id = None
|
||||
_registered_root = {"_": ""}
|
||||
_registered_host = {"_": None}
|
||||
# Keep modules manager (and it's modules) in memory
|
||||
|
|
@ -546,3 +548,18 @@ def change_current_context(asset_doc, task_name, template_key=None):
|
|||
emit_event("taskChanged", data)
|
||||
|
||||
return changes
|
||||
|
||||
|
||||
def get_process_id():
|
||||
"""Fake process id created on demand using uuid.
|
||||
|
||||
Can be used to create process specific folders in temp directory.
|
||||
|
||||
Returns:
|
||||
str: Process id.
|
||||
"""
|
||||
|
||||
global _process_id
|
||||
if _process_id is None:
|
||||
_process_id = str(uuid.uuid4())
|
||||
return _process_id
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from .constants import (
|
||||
SUBSET_NAME_ALLOWED_SYMBOLS,
|
||||
DEFAULT_SUBSET_TEMPLATE,
|
||||
PRE_CREATE_THUMBNAIL_KEY,
|
||||
)
|
||||
|
||||
from .subset_name import (
|
||||
|
|
@ -42,6 +43,7 @@ from .legacy_create import (
|
|||
__all__ = (
|
||||
"SUBSET_NAME_ALLOWED_SYMBOLS",
|
||||
"DEFAULT_SUBSET_TEMPLATE",
|
||||
"PRE_CREATE_THUMBNAIL_KEY",
|
||||
|
||||
"TaskNotSetError",
|
||||
"get_subset_name",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
SUBSET_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_."
|
||||
DEFAULT_SUBSET_TEMPLATE = "{family}{Variant}"
|
||||
PRE_CREATE_THUMBNAIL_KEY = "thumbnail_source"
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SUBSET_NAME_ALLOWED_SYMBOLS",
|
||||
"DEFAULT_SUBSET_TEMPLATE",
|
||||
"PRE_CREATE_THUMBNAIL_KEY",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1077,6 +1077,8 @@ class CreateContext:
|
|||
# Shared data across creators during collection phase
|
||||
self._collection_shared_data = None
|
||||
|
||||
self.thumbnail_paths_by_instance_id = {}
|
||||
|
||||
# Trigger reset if was enabled
|
||||
if reset:
|
||||
self.reset(discover_publish_plugins)
|
||||
|
|
@ -1146,6 +1148,29 @@ class CreateContext:
|
|||
|
||||
self.reset_finalization()
|
||||
|
||||
def refresh_thumbnails(self):
|
||||
"""Cleanup thumbnail paths.
|
||||
|
||||
Remove all thumbnail filepaths that are empty or lead to files which
|
||||
does not exists or of instances that are not available anymore.
|
||||
"""
|
||||
|
||||
invalid = set()
|
||||
for instance_id, path in self.thumbnail_paths_by_instance_id.items():
|
||||
instance_available = True
|
||||
if instance_id is not None:
|
||||
instance_available = instance_id in self._instances_by_id
|
||||
|
||||
if (
|
||||
not instance_available
|
||||
or not path
|
||||
or not os.path.exists(path)
|
||||
):
|
||||
invalid.add(instance_id)
|
||||
|
||||
for instance_id in invalid:
|
||||
self.thumbnail_paths_by_instance_id.pop(instance_id)
|
||||
|
||||
def reset_preparation(self):
|
||||
"""Prepare attributes that must be prepared/cleaned before reset."""
|
||||
|
||||
|
|
@ -1157,6 +1182,7 @@ class CreateContext:
|
|||
|
||||
# Stop access to collection shared data
|
||||
self._collection_shared_data = None
|
||||
self.refresh_thumbnails()
|
||||
|
||||
def reset_avalon_context(self):
|
||||
"""Give ability to reset avalon context.
|
||||
|
|
|
|||
|
|
@ -443,6 +443,13 @@ class BaseCreator:
|
|||
|
||||
return self.create_context.collection_shared_data
|
||||
|
||||
def set_instance_thumbnail_path(self, instance_id, thumbnail_path=None):
|
||||
"""Set path to thumbnail for instance."""
|
||||
|
||||
self.create_context.thumbnail_paths_by_instance_id[instance_id] = (
|
||||
thumbnail_path
|
||||
)
|
||||
|
||||
|
||||
class Creator(BaseCreator):
|
||||
"""Creator that has more information for artist to show in UI.
|
||||
|
|
@ -469,6 +476,13 @@ class Creator(BaseCreator):
|
|||
# - in some cases it may confuse artists because it would not be used
|
||||
# e.g. for buld creators
|
||||
create_allow_context_change = True
|
||||
# A thumbnail can be passed in precreate attributes
|
||||
# - if is set to True is should expect that a thumbnail path under key
|
||||
# PRE_CREATE_THUMBNAIL_KEY can be sent in data with precreate data
|
||||
# - is disabled by default because the feature was added in later stages
|
||||
# and creators who would not expect PRE_CREATE_THUMBNAIL_KEY could
|
||||
# cause issues with instance data
|
||||
create_allow_thumbnail = False
|
||||
|
||||
# Precreate attribute definitions showed before creation
|
||||
# - similar to instance attribute definitions
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import os
|
||||
import json
|
||||
from uuid import uuid4
|
||||
from openpype.lib import Logger, filter_profiles
|
||||
from openpype.lib.pype_info import get_workstation_info
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.pipeline import get_process_id
|
||||
|
||||
|
||||
def _read_lock_file(lock_filepath):
|
||||
|
|
@ -37,7 +37,7 @@ def is_workfile_locked_for_current_process(filepath):
|
|||
|
||||
lock_filepath = _get_lock_file(filepath)
|
||||
data = _read_lock_file(lock_filepath)
|
||||
return data["process_id"] == _get_process_id()
|
||||
return data["process_id"] == get_process_id()
|
||||
|
||||
|
||||
def delete_workfile_lock(filepath):
|
||||
|
|
@ -49,7 +49,7 @@ def delete_workfile_lock(filepath):
|
|||
def create_workfile_lock(filepath):
|
||||
lock_filepath = _get_lock_file(filepath)
|
||||
info = get_workstation_info()
|
||||
info["process_id"] = _get_process_id()
|
||||
info["process_id"] = get_process_id()
|
||||
with open(lock_filepath, "w") as stream:
|
||||
json.dump(info, stream)
|
||||
|
||||
|
|
@ -59,14 +59,6 @@ def remove_workfile_lock(filepath):
|
|||
delete_workfile_lock(filepath)
|
||||
|
||||
|
||||
def _get_process_id():
|
||||
process_id = os.environ.get("OPENPYPE_PROCESS_ID")
|
||||
if not process_id:
|
||||
process_id = str(uuid4())
|
||||
os.environ["OPENPYPE_PROCESS_ID"] = process_id
|
||||
return process_id
|
||||
|
||||
|
||||
def is_workfile_lock_enabled(host_name, project_name, project_setting=None):
|
||||
if project_setting is None:
|
||||
project_setting = get_project_settings(project_name)
|
||||
|
|
|
|||
|
|
@ -19,14 +19,28 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
|
|||
if not create_context:
|
||||
return
|
||||
|
||||
thumbnail_paths_by_instance_id = (
|
||||
create_context.thumbnail_paths_by_instance_id
|
||||
)
|
||||
context.data["thumbnailSource"] = (
|
||||
thumbnail_paths_by_instance_id.get(None)
|
||||
)
|
||||
|
||||
project_name = create_context.project_name
|
||||
if project_name:
|
||||
context.data["projectName"] = project_name
|
||||
|
||||
for created_instance in create_context.instances:
|
||||
instance_data = created_instance.data_to_store()
|
||||
if instance_data["active"]:
|
||||
thumbnail_path = thumbnail_paths_by_instance_id.get(
|
||||
created_instance.id
|
||||
)
|
||||
self.create_instance(
|
||||
context, instance_data, created_instance.transient_data
|
||||
context,
|
||||
instance_data,
|
||||
created_instance.transient_data,
|
||||
thumbnail_path
|
||||
)
|
||||
|
||||
# Update global data to context
|
||||
|
|
@ -39,7 +53,13 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
|
|||
legacy_io.Session[key] = value
|
||||
os.environ[key] = value
|
||||
|
||||
def create_instance(self, context, in_data, transient_data):
|
||||
def create_instance(
|
||||
self,
|
||||
context,
|
||||
in_data,
|
||||
transient_data,
|
||||
thumbnail_path
|
||||
):
|
||||
subset = in_data["subset"]
|
||||
# If instance data already contain families then use it
|
||||
instance_families = in_data.get("families") or []
|
||||
|
|
@ -53,7 +73,8 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
|
|||
"name": subset,
|
||||
"family": in_data["family"],
|
||||
"families": instance_families,
|
||||
"representations": []
|
||||
"representations": [],
|
||||
"thumbnailSource": thumbnail_path
|
||||
})
|
||||
for key, value in in_data.items():
|
||||
if key not in instance.data:
|
||||
|
|
|
|||
|
|
@ -34,28 +34,55 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
label = "Extract Thumbnail (from source)"
|
||||
# Before 'ExtractThumbnail' in global plugins
|
||||
order = pyblish.api.ExtractorOrder - 0.00001
|
||||
hosts = ["traypublisher"]
|
||||
|
||||
def process(self, instance):
|
||||
self._create_context_thumbnail(instance.context)
|
||||
|
||||
subset_name = instance.data["subset"]
|
||||
self.log.info(
|
||||
"Processing instance with subset name {}".format(subset_name)
|
||||
)
|
||||
|
||||
thumbnail_source = instance.data.get("thumbnailSource")
|
||||
if not thumbnail_source:
|
||||
self.log.debug("Thumbnail source not filled. Skipping.")
|
||||
return
|
||||
|
||||
elif not os.path.exists(thumbnail_source):
|
||||
self.log.debug(
|
||||
"Thumbnail source file was not found {}. Skipping.".format(
|
||||
thumbnail_source))
|
||||
# Check if already has thumbnail created
|
||||
if self._instance_has_thumbnail(instance):
|
||||
self.log.info("Thumbnail representation already present.")
|
||||
return
|
||||
|
||||
# Check if already has thumbnail created
|
||||
if self._already_has_thumbnail(instance):
|
||||
self.log.info("Thumbnail representation already present.")
|
||||
dst_filepath = self._create_thumbnail(
|
||||
instance.context, thumbnail_source
|
||||
)
|
||||
if not dst_filepath:
|
||||
return
|
||||
|
||||
dst_staging, dst_filename = os.path.split(dst_filepath)
|
||||
new_repre = {
|
||||
"name": "thumbnail",
|
||||
"ext": "jpg",
|
||||
"files": dst_filename,
|
||||
"stagingDir": dst_staging,
|
||||
"thumbnail": True,
|
||||
"tags": ["thumbnail"]
|
||||
}
|
||||
|
||||
# adding representation
|
||||
self.log.debug(
|
||||
"Adding thumbnail representation: {}".format(new_repre)
|
||||
)
|
||||
instance.data["representations"].append(new_repre)
|
||||
|
||||
def _create_thumbnail(self, context, thumbnail_source):
|
||||
if not thumbnail_source:
|
||||
self.log.debug("Thumbnail source not filled. Skipping.")
|
||||
return
|
||||
|
||||
if not os.path.exists(thumbnail_source):
|
||||
self.log.debug((
|
||||
"Thumbnail source is set but file was not found {}. Skipping."
|
||||
).format(thumbnail_source))
|
||||
return
|
||||
|
||||
# Create temp directory for thumbnail
|
||||
|
|
@ -65,7 +92,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
"Create temp directory {} for thumbnail".format(dst_staging)
|
||||
)
|
||||
# Store new staging to cleanup paths
|
||||
instance.context.data["cleanupFullPaths"].append(dst_staging)
|
||||
context.data["cleanupFullPaths"].append(dst_staging)
|
||||
|
||||
thumbnail_created = False
|
||||
oiio_supported = is_oiio_supported()
|
||||
|
|
@ -97,26 +124,12 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
)
|
||||
|
||||
# Skip representation and try next one if wasn't created
|
||||
if not thumbnail_created:
|
||||
self.log.warning("Thumbanil has not been created.")
|
||||
return
|
||||
if thumbnail_created:
|
||||
return full_output_path
|
||||
|
||||
new_repre = {
|
||||
"name": "thumbnail",
|
||||
"ext": "jpg",
|
||||
"files": dst_filename,
|
||||
"stagingDir": dst_staging,
|
||||
"thumbnail": True,
|
||||
"tags": ["thumbnail"]
|
||||
}
|
||||
self.log.warning("Thumbanil has not been created.")
|
||||
|
||||
# adding representation
|
||||
self.log.debug(
|
||||
"Adding thumbnail representation: {}".format(new_repre)
|
||||
)
|
||||
instance.data["representations"].append(new_repre)
|
||||
|
||||
def _already_has_thumbnail(self, instance):
|
||||
def _instance_has_thumbnail(self, instance):
|
||||
if "representations" not in instance.data:
|
||||
self.log.warning(
|
||||
"Instance does not have 'representations' key filled"
|
||||
|
|
@ -171,3 +184,11 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin):
|
|||
exc_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
def _create_context_thumbnail(self, context):
|
||||
if "thumbnailPath" in context.data:
|
||||
return
|
||||
|
||||
thumbnail_source = context.data.get("thumbnailSource")
|
||||
thumbnail_path = self._create_thumbnail(context, thumbnail_source)
|
||||
context.data["thumbnailPath"] = thumbnail_path
|
||||
|
|
@ -13,166 +13,279 @@ import sys
|
|||
import errno
|
||||
import shutil
|
||||
import copy
|
||||
import collections
|
||||
|
||||
import six
|
||||
import pyblish.api
|
||||
|
||||
from openpype.client import get_version_by_id
|
||||
from openpype.client import get_versions
|
||||
from openpype.client.operations import OperationsSession, new_thumbnail_doc
|
||||
|
||||
InstanceFilterResult = collections.namedtuple(
|
||||
"InstanceFilterResult",
|
||||
["instance", "thumbnail_path", "version_id"]
|
||||
)
|
||||
|
||||
class IntegrateThumbnails(pyblish.api.InstancePlugin):
|
||||
|
||||
class IntegrateThumbnails(pyblish.api.ContextPlugin):
|
||||
"""Integrate Thumbnails for Openpype use in Loaders."""
|
||||
|
||||
label = "Integrate Thumbnails"
|
||||
order = pyblish.api.IntegratorOrder + 0.01
|
||||
families = ["review"]
|
||||
|
||||
required_context_keys = [
|
||||
"project", "asset", "task", "subset", "version"
|
||||
]
|
||||
|
||||
def process(self, instance):
|
||||
def process(self, context):
|
||||
# Filter instances which can be used for integration
|
||||
filtered_instance_items = self._prepare_instances(context)
|
||||
if not filtered_instance_items:
|
||||
self.log.info(
|
||||
"All instances were filtered. Thumbnail integration skipped."
|
||||
)
|
||||
return
|
||||
|
||||
# Initial validation of available templated and required keys
|
||||
env_key = "AVALON_THUMBNAIL_ROOT"
|
||||
thumbnail_root_format_key = "{thumbnail_root}"
|
||||
thumbnail_root = os.environ.get(env_key) or ""
|
||||
|
||||
published_repres = instance.data.get("published_representations")
|
||||
if not published_repres:
|
||||
self.log.debug(
|
||||
"There are no published representations on the instance."
|
||||
)
|
||||
return
|
||||
|
||||
anatomy = instance.context.data["anatomy"]
|
||||
anatomy = context.data["anatomy"]
|
||||
project_name = anatomy.project_name
|
||||
if "publish" not in anatomy.templates:
|
||||
self.log.warning("Anatomy is missing the \"publish\" key!")
|
||||
self.log.warning(
|
||||
"Anatomy is missing the \"publish\" key. Skipping."
|
||||
)
|
||||
return
|
||||
|
||||
if "thumbnail" not in anatomy.templates["publish"]:
|
||||
self.log.warning((
|
||||
"There is no \"thumbnail\" template set for the project \"{}\""
|
||||
"There is no \"thumbnail\" template set for the project"
|
||||
" \"{}\". Skipping."
|
||||
).format(project_name))
|
||||
return
|
||||
|
||||
thumbnail_template = anatomy.templates["publish"]["thumbnail"]
|
||||
if not thumbnail_template:
|
||||
self.log.info("Thumbnail template is not filled. Skipping.")
|
||||
return
|
||||
|
||||
if (
|
||||
not thumbnail_root
|
||||
and thumbnail_root_format_key in thumbnail_template
|
||||
):
|
||||
self.log.warning((
|
||||
"{} is not set. Skipping thumbnail integration."
|
||||
).format(env_key))
|
||||
self.log.warning(("{} is not set. Skipping.").format(env_key))
|
||||
return
|
||||
|
||||
thumb_repre = None
|
||||
thumb_repre_anatomy_data = None
|
||||
for repre_info in published_repres.values():
|
||||
repre = repre_info["representation"]
|
||||
if repre["name"].lower() == "thumbnail":
|
||||
thumb_repre = repre
|
||||
thumb_repre_anatomy_data = repre_info["anatomy_data"]
|
||||
# Collect verion ids from all filtered instance
|
||||
version_ids = {
|
||||
instance_items.version_id
|
||||
for instance_items in filtered_instance_items
|
||||
}
|
||||
# Query versions
|
||||
version_docs = get_versions(
|
||||
project_name,
|
||||
version_ids=version_ids,
|
||||
hero=True,
|
||||
fields=["_id", "type", "name"]
|
||||
)
|
||||
# Store version by their id (converted to string)
|
||||
version_docs_by_str_id = {
|
||||
str(version_doc["_id"]): version_doc
|
||||
for version_doc in version_docs
|
||||
}
|
||||
self._integrate_thumbnails(
|
||||
filtered_instance_items,
|
||||
version_docs_by_str_id,
|
||||
anatomy,
|
||||
thumbnail_root
|
||||
)
|
||||
|
||||
def _prepare_instances(self, context):
|
||||
context_thumbnail_path = context.get("thumbnailPath")
|
||||
valid_context_thumbnail = False
|
||||
if context_thumbnail_path and os.path.exists(context_thumbnail_path):
|
||||
valid_context_thumbnail = True
|
||||
|
||||
filtered_instances = []
|
||||
for instance in context:
|
||||
instance_label = self._get_instance_label(instance)
|
||||
# Skip instances without published representations
|
||||
# - there is no place where to put the thumbnail
|
||||
published_repres = instance.data.get("published_representations")
|
||||
if not published_repres:
|
||||
self.log.debug((
|
||||
"There are no published representations"
|
||||
" on the instance {}."
|
||||
).format(instance_label))
|
||||
continue
|
||||
|
||||
# Find thumbnail path on instance
|
||||
thumbnail_path = self._get_instance_thumbnail_path(
|
||||
published_repres)
|
||||
if thumbnail_path:
|
||||
self.log.debug((
|
||||
"Found thumbnail path for instance \"{}\"."
|
||||
" Thumbnail path: {}"
|
||||
).format(instance_label, thumbnail_path))
|
||||
|
||||
elif valid_context_thumbnail:
|
||||
# Use context thumbnail path if is available
|
||||
thumbnail_path = context_thumbnail_path
|
||||
self.log.debug((
|
||||
"Using context thumbnail path for instance \"{}\"."
|
||||
" Thumbnail path: {}"
|
||||
).format(instance_label, thumbnail_path))
|
||||
|
||||
# Skip instance if thumbnail path is not available for it
|
||||
if not thumbnail_path:
|
||||
self.log.info((
|
||||
"Skipping thumbnail integration for instance \"{}\"."
|
||||
" Instance and context"
|
||||
" thumbnail paths are not available."
|
||||
).format(instance_label))
|
||||
continue
|
||||
|
||||
version_id = str(self._get_version_id(published_repres))
|
||||
filtered_instances.append(
|
||||
InstanceFilterResult(instance, thumbnail_path, version_id)
|
||||
)
|
||||
return filtered_instances
|
||||
|
||||
def _get_version_id(self, published_representations):
|
||||
for repre_info in published_representations.values():
|
||||
return repre_info["representation"]["parent"]
|
||||
|
||||
def _get_instance_thumbnail_path(self, published_representations):
|
||||
thumb_repre_doc = None
|
||||
for repre_info in published_representations.values():
|
||||
repre_doc = repre_info["representation"]
|
||||
if repre_doc["name"].lower() == "thumbnail":
|
||||
thumb_repre_doc = repre_doc
|
||||
break
|
||||
|
||||
if not thumb_repre:
|
||||
if thumb_repre_doc is None:
|
||||
self.log.debug(
|
||||
"There is not representation with name \"thumbnail\""
|
||||
)
|
||||
return
|
||||
return None
|
||||
|
||||
version = get_version_by_id(project_name, thumb_repre["parent"])
|
||||
if not version:
|
||||
raise AssertionError(
|
||||
"There does not exist version with id {}".format(
|
||||
str(thumb_repre["parent"])
|
||||
)
|
||||
path = thumb_repre_doc["data"]["path"]
|
||||
if not os.path.exists(path):
|
||||
self.log.warning(
|
||||
"Thumbnail file cannot be found. Path: {}".format(path)
|
||||
)
|
||||
return None
|
||||
return os.path.normpath(path)
|
||||
|
||||
def _integrate_thumbnails(
|
||||
self,
|
||||
filtered_instance_items,
|
||||
version_docs_by_str_id,
|
||||
anatomy,
|
||||
thumbnail_root
|
||||
):
|
||||
op_session = OperationsSession()
|
||||
project_name = anatomy.project_name
|
||||
|
||||
for instance_item in filtered_instance_items:
|
||||
instance, thumbnail_path, version_id = instance_item
|
||||
instance_label = self._get_instance_label(instance)
|
||||
version_doc = version_docs_by_str_id.get(version_id)
|
||||
if not version_doc:
|
||||
self.log.warning((
|
||||
"Version entity for instance \"{}\" was not found."
|
||||
).format(instance_label))
|
||||
continue
|
||||
|
||||
filename, file_extension = os.path.splitext(thumbnail_path)
|
||||
# Create id for mongo entity now to fill anatomy template
|
||||
thumbnail_doc = new_thumbnail_doc()
|
||||
thumbnail_id = thumbnail_doc["_id"]
|
||||
|
||||
# Prepare anatomy template fill data
|
||||
template_data = copy.deepcopy(instance.data["anatomyData"])
|
||||
template_data.update({
|
||||
"_id": str(thumbnail_id),
|
||||
"ext": file_extension[1:],
|
||||
"name": "thumbnail",
|
||||
"thumbnail_root": thumbnail_root,
|
||||
"thumbnail_type": "thumbnail"
|
||||
})
|
||||
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
thumbnail_template = anatomy.templates["publish"]["thumbnail"]
|
||||
template_filled = anatomy_filled["publish"]["thumbnail"]
|
||||
|
||||
dst_full_path = os.path.normpath(str(template_filled))
|
||||
self.log.debug("Copying file .. {} -> {}".format(
|
||||
thumbnail_path, dst_full_path
|
||||
))
|
||||
dirname = os.path.dirname(dst_full_path)
|
||||
try:
|
||||
os.makedirs(dirname)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
tp, value, tb = sys.exc_info()
|
||||
six.reraise(tp, value, tb)
|
||||
|
||||
shutil.copy(thumbnail_path, dst_full_path)
|
||||
|
||||
# Clean template data from keys that are dynamic
|
||||
for key in ("_id", "thumbnail_root"):
|
||||
template_data.pop(key, None)
|
||||
|
||||
repre_context = template_filled.used_values
|
||||
for key in self.required_context_keys:
|
||||
value = template_data.get(key)
|
||||
if not value:
|
||||
continue
|
||||
repre_context[key] = template_data[key]
|
||||
|
||||
thumbnail_doc["data"] = {
|
||||
"template": thumbnail_template,
|
||||
"template_data": repre_context
|
||||
}
|
||||
op_session.create_entity(
|
||||
project_name, thumbnail_doc["type"], thumbnail_doc
|
||||
)
|
||||
# Create thumbnail entity
|
||||
self.log.debug(
|
||||
"Creating entity in database {}".format(str(thumbnail_doc))
|
||||
)
|
||||
|
||||
# Get full path to thumbnail file from representation
|
||||
src_full_path = os.path.normpath(thumb_repre["data"]["path"])
|
||||
if not os.path.exists(src_full_path):
|
||||
self.log.warning("Thumbnail file was not found. Path: {}".format(
|
||||
src_full_path
|
||||
# Set thumbnail id for version
|
||||
op_session.update_entity(
|
||||
project_name,
|
||||
version_doc["type"],
|
||||
version_doc["_id"],
|
||||
{"data.thumbnail_id": thumbnail_id}
|
||||
)
|
||||
if version_doc["type"] == "hero_version":
|
||||
version_name = "Hero"
|
||||
else:
|
||||
version_name = version_doc["name"]
|
||||
self.log.debug("Setting thumbnail for version \"{}\" <{}>".format(
|
||||
version_name, version_id
|
||||
))
|
||||
return
|
||||
|
||||
filename, file_extension = os.path.splitext(src_full_path)
|
||||
# Create id for mongo entity now to fill anatomy template
|
||||
thumbnail_doc = new_thumbnail_doc()
|
||||
thumbnail_id = thumbnail_doc["_id"]
|
||||
|
||||
# Prepare anatomy template fill data
|
||||
template_data = copy.deepcopy(thumb_repre_anatomy_data)
|
||||
template_data.update({
|
||||
"_id": str(thumbnail_id),
|
||||
"ext": file_extension[1:],
|
||||
"thumbnail_root": thumbnail_root,
|
||||
"thumbnail_type": "thumbnail"
|
||||
})
|
||||
|
||||
anatomy_filled = anatomy.format(template_data)
|
||||
template_filled = anatomy_filled["publish"]["thumbnail"]
|
||||
|
||||
dst_full_path = os.path.normpath(str(template_filled))
|
||||
self.log.debug(
|
||||
"Copying file .. {} -> {}".format(src_full_path, dst_full_path)
|
||||
)
|
||||
dirname = os.path.dirname(dst_full_path)
|
||||
try:
|
||||
os.makedirs(dirname)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
tp, value, tb = sys.exc_info()
|
||||
six.reraise(tp, value, tb)
|
||||
|
||||
shutil.copy(src_full_path, dst_full_path)
|
||||
|
||||
# Clean template data from keys that are dynamic
|
||||
for key in ("_id", "thumbnail_root"):
|
||||
template_data.pop(key, None)
|
||||
|
||||
repre_context = template_filled.used_values
|
||||
for key in self.required_context_keys:
|
||||
value = template_data.get(key)
|
||||
if not value:
|
||||
continue
|
||||
repre_context[key] = template_data[key]
|
||||
|
||||
op_session = OperationsSession()
|
||||
|
||||
thumbnail_doc["data"] = {
|
||||
"template": thumbnail_template,
|
||||
"template_data": repre_context
|
||||
}
|
||||
op_session.create_entity(
|
||||
project_name, thumbnail_doc["type"], thumbnail_doc
|
||||
)
|
||||
# Create thumbnail entity
|
||||
self.log.debug(
|
||||
"Creating entity in database {}".format(str(thumbnail_doc))
|
||||
)
|
||||
|
||||
# Set thumbnail id for version
|
||||
op_session.update_entity(
|
||||
project_name,
|
||||
version["type"],
|
||||
version["_id"],
|
||||
{"data.thumbnail_id": thumbnail_id}
|
||||
)
|
||||
self.log.debug("Setting thumbnail for version \"{}\" <{}>".format(
|
||||
version["name"], str(version["_id"])
|
||||
))
|
||||
|
||||
asset_entity = instance.data["assetEntity"]
|
||||
op_session.update_entity(
|
||||
project_name,
|
||||
asset_entity["type"],
|
||||
asset_entity["_id"],
|
||||
{"data.thumbnail_id": thumbnail_id}
|
||||
)
|
||||
self.log.debug("Setting thumbnail for asset \"{}\" <{}>".format(
|
||||
asset_entity["name"], str(version["_id"])
|
||||
))
|
||||
asset_entity = instance.data["assetEntity"]
|
||||
op_session.update_entity(
|
||||
project_name,
|
||||
asset_entity["type"],
|
||||
asset_entity["_id"],
|
||||
{"data.thumbnail_id": thumbnail_id}
|
||||
)
|
||||
self.log.debug("Setting thumbnail for asset \"{}\" <{}>".format(
|
||||
asset_entity["name"], version_id
|
||||
))
|
||||
|
||||
op_session.commit()
|
||||
|
||||
def _get_instance_label(self, instance):
|
||||
return (
|
||||
instance.data.get("label")
|
||||
or instance.data.get("name")
|
||||
or "N/A"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -138,8 +138,7 @@ def save_studio_settings(data):
|
|||
SaveWarningExc: If any module raises the exception.
|
||||
"""
|
||||
# Notify Pype modules
|
||||
from openpype.modules import ModulesManager
|
||||
from openpype_interfaces import ISettingsChangeListener
|
||||
from openpype.modules import ModulesManager, ISettingsChangeListener
|
||||
|
||||
old_data = get_system_settings()
|
||||
default_values = get_default_settings()[SYSTEM_SETTINGS_KEY]
|
||||
|
|
@ -186,8 +185,7 @@ def save_project_settings(project_name, overrides):
|
|||
SaveWarningExc: If any module raises the exception.
|
||||
"""
|
||||
# Notify Pype modules
|
||||
from openpype.modules import ModulesManager
|
||||
from openpype_interfaces import ISettingsChangeListener
|
||||
from openpype.modules import ModulesManager, ISettingsChangeListener
|
||||
|
||||
default_values = get_default_settings()[PROJECT_SETTINGS_KEY]
|
||||
if project_name:
|
||||
|
|
@ -248,8 +246,7 @@ def save_project_anatomy(project_name, anatomy_data):
|
|||
SaveWarningExc: If any module raises the exception.
|
||||
"""
|
||||
# Notify Pype modules
|
||||
from openpype.modules import ModulesManager
|
||||
from openpype_interfaces import ISettingsChangeListener
|
||||
from openpype.modules import ModulesManager, ISettingsChangeListener
|
||||
|
||||
default_values = get_default_settings()[PROJECT_ANATOMY_KEY]
|
||||
if project_name:
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
"bg": "#2C313A",
|
||||
"bg-inputs": "#21252B",
|
||||
"bg-buttons": "#434a56",
|
||||
"bg-button-hover": "rgba(168, 175, 189, 0.3)",
|
||||
"bg-button-hover": "rgb(81, 86, 97)",
|
||||
"bg-inputs-disabled": "#2C313A",
|
||||
"bg-buttons-disabled": "#434a56",
|
||||
|
||||
|
|
|
|||
|
|
@ -884,6 +884,26 @@ PublisherTabBtn[active="1"]:hover {
|
|||
background: {color:bg};
|
||||
}
|
||||
|
||||
PixmapButton{
|
||||
border: 0px solid transparent;
|
||||
border-radius: 0.2em;
|
||||
background: {color:bg-buttons};
|
||||
}
|
||||
PixmapButton:hover {
|
||||
background: {color:bg-button-hover};
|
||||
}
|
||||
PixmapButton:disabled {
|
||||
background: {color:bg-buttons-disabled};
|
||||
}
|
||||
|
||||
#ThumbnailPixmapHoverButton {
|
||||
font-size: 11pt;
|
||||
background: {color:bg-view};
|
||||
}
|
||||
#ThumbnailPixmapHoverButton:hover {
|
||||
background: {color:bg-button-hover};
|
||||
}
|
||||
|
||||
#CreatorDetailedDescription {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
|
|
@ -911,11 +931,11 @@ PublisherTabBtn[active="1"]:hover {
|
|||
#PublishLogConsole {
|
||||
font-family: "Noto Sans Mono";
|
||||
}
|
||||
VariantInputsWidget QLineEdit {
|
||||
#VariantInputsWidget QLineEdit {
|
||||
border-bottom-right-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
}
|
||||
VariantInputsWidget QToolButton {
|
||||
#VariantInputsWidget QToolButton {
|
||||
border-bottom-left-radius: 0px;
|
||||
border-top-left-radius: 0px;
|
||||
padding-top: 0.5em;
|
||||
|
|
|
|||
|
|
@ -20,9 +20,10 @@ INSTANCE_ID_ROLE = QtCore.Qt.UserRole + 1
|
|||
SORT_VALUE_ROLE = QtCore.Qt.UserRole + 2
|
||||
IS_GROUP_ROLE = QtCore.Qt.UserRole + 3
|
||||
CREATOR_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 4
|
||||
FAMILY_ROLE = QtCore.Qt.UserRole + 5
|
||||
GROUP_ROLE = QtCore.Qt.UserRole + 6
|
||||
CONVERTER_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 7
|
||||
CREATOR_THUMBNAIL_ENABLED_ROLE = QtCore.Qt.UserRole + 5
|
||||
FAMILY_ROLE = QtCore.Qt.UserRole + 6
|
||||
GROUP_ROLE = QtCore.Qt.UserRole + 7
|
||||
CONVERTER_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 8
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import logging
|
|||
import traceback
|
||||
import collections
|
||||
import uuid
|
||||
import tempfile
|
||||
import shutil
|
||||
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||
|
||||
import six
|
||||
|
|
@ -24,6 +26,7 @@ from openpype.pipeline import (
|
|||
KnownPublishError,
|
||||
registered_host,
|
||||
legacy_io,
|
||||
get_process_id,
|
||||
)
|
||||
from openpype.pipeline.create import (
|
||||
CreateContext,
|
||||
|
|
@ -825,6 +828,7 @@ class CreatorItem:
|
|||
default_variant,
|
||||
default_variants,
|
||||
create_allow_context_change,
|
||||
create_allow_thumbnail,
|
||||
pre_create_attributes_defs
|
||||
):
|
||||
self.identifier = identifier
|
||||
|
|
@ -838,6 +842,7 @@ class CreatorItem:
|
|||
self.default_variant = default_variant
|
||||
self.default_variants = default_variants
|
||||
self.create_allow_context_change = create_allow_context_change
|
||||
self.create_allow_thumbnail = create_allow_thumbnail
|
||||
self.instance_attributes_defs = instance_attributes_defs
|
||||
self.pre_create_attributes_defs = pre_create_attributes_defs
|
||||
|
||||
|
|
@ -864,6 +869,7 @@ class CreatorItem:
|
|||
default_variants = None
|
||||
pre_create_attr_defs = None
|
||||
create_allow_context_change = None
|
||||
create_allow_thumbnail = None
|
||||
if creator_type is CreatorTypes.artist:
|
||||
description = creator.get_description()
|
||||
detail_description = creator.get_detail_description()
|
||||
|
|
@ -871,6 +877,7 @@ class CreatorItem:
|
|||
default_variants = creator.get_default_variants()
|
||||
pre_create_attr_defs = creator.get_pre_create_attr_defs()
|
||||
create_allow_context_change = creator.create_allow_context_change
|
||||
create_allow_thumbnail = creator.create_allow_thumbnail
|
||||
|
||||
identifier = creator.identifier
|
||||
return cls(
|
||||
|
|
@ -886,6 +893,7 @@ class CreatorItem:
|
|||
default_variant,
|
||||
default_variants,
|
||||
create_allow_context_change,
|
||||
create_allow_thumbnail,
|
||||
pre_create_attr_defs
|
||||
)
|
||||
|
||||
|
|
@ -914,6 +922,7 @@ class CreatorItem:
|
|||
"default_variant": self.default_variant,
|
||||
"default_variants": self.default_variants,
|
||||
"create_allow_context_change": self.create_allow_context_change,
|
||||
"create_allow_thumbnail": self.create_allow_thumbnail,
|
||||
"instance_attributes_defs": instance_attributes_defs,
|
||||
"pre_create_attributes_defs": pre_create_attributes_defs,
|
||||
}
|
||||
|
|
@ -1115,11 +1124,13 @@ class AbstractPublisherController(object):
|
|||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def save_changes(self):
|
||||
"""Save changes in create context."""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def remove_instances(self, instance_ids):
|
||||
"""Remove list of instances from create context."""
|
||||
# TODO expect instance ids
|
||||
|
|
@ -1256,6 +1267,14 @@ class AbstractPublisherController(object):
|
|||
def trigger_convertor_items(self, convertor_identifiers):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_thumbnail_paths_for_instances(self, instance_ids):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_comment(self, comment):
|
||||
"""Set comment on pyblish context.
|
||||
|
|
@ -1283,6 +1302,22 @@ class AbstractPublisherController(object):
|
|||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_thumbnail_temp_dir_path(self):
|
||||
"""Return path to directory where thumbnails can be temporary stored.
|
||||
|
||||
Returns:
|
||||
str: Path to a directory.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def clear_thumbnail_temp_dir_path(self):
|
||||
"""Remove content of thumbnail temp directory."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BasePublisherController(AbstractPublisherController):
|
||||
"""Implement common logic for controllers.
|
||||
|
|
@ -1523,6 +1558,26 @@ class BasePublisherController(AbstractPublisherController):
|
|||
return creator_item.icon
|
||||
return None
|
||||
|
||||
def get_thumbnail_temp_dir_path(self):
|
||||
"""Return path to directory where thumbnails can be temporary stored.
|
||||
|
||||
Returns:
|
||||
str: Path to a directory.
|
||||
"""
|
||||
|
||||
return os.path.join(
|
||||
tempfile.gettempdir(),
|
||||
"publisher_thumbnails",
|
||||
get_process_id()
|
||||
)
|
||||
|
||||
def clear_thumbnail_temp_dir_path(self):
|
||||
"""Remove content of thumbnail temp directory."""
|
||||
|
||||
dirpath = self.get_thumbnail_temp_dir_path()
|
||||
if os.path.exists(dirpath):
|
||||
shutil.rmtree(dirpath)
|
||||
|
||||
|
||||
class PublisherController(BasePublisherController):
|
||||
"""Middleware between UI, CreateContext and publish Context.
|
||||
|
|
@ -1778,6 +1833,29 @@ class PublisherController(BasePublisherController):
|
|||
|
||||
self._on_create_instance_change()
|
||||
|
||||
def get_thumbnail_paths_for_instances(self, instance_ids):
|
||||
thumbnail_paths_by_instance_id = (
|
||||
self._create_context.thumbnail_paths_by_instance_id
|
||||
)
|
||||
return {
|
||||
instance_id: thumbnail_paths_by_instance_id.get(instance_id)
|
||||
for instance_id in instance_ids
|
||||
}
|
||||
|
||||
def set_thumbnail_paths_for_instances(self, thumbnail_path_mapping):
|
||||
thumbnail_paths_by_instance_id = (
|
||||
self._create_context.thumbnail_paths_by_instance_id
|
||||
)
|
||||
for instance_id, thumbnail_path in thumbnail_path_mapping.items():
|
||||
thumbnail_paths_by_instance_id[instance_id] = thumbnail_path
|
||||
|
||||
self._emit_event(
|
||||
"instance.thumbnail.changed",
|
||||
{
|
||||
"mapping": thumbnail_path_mapping
|
||||
}
|
||||
)
|
||||
|
||||
def emit_card_message(
|
||||
self, message, message_type=CardMessageTypes.standard
|
||||
):
|
||||
|
|
|
|||
|
|
@ -115,6 +115,11 @@ class QtRemotePublishController(BasePublisherController):
|
|||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._created_instances = {}
|
||||
self._thumbnail_paths_by_instance_id = None
|
||||
|
||||
def _reset_attributes(self):
|
||||
super()._reset_attributes()
|
||||
self._thumbnail_paths_by_instance_id = None
|
||||
|
||||
@abstractmethod
|
||||
def _get_serialized_instances(self):
|
||||
|
|
@ -180,6 +185,11 @@ class QtRemotePublishController(BasePublisherController):
|
|||
self.host_is_valid = event["value"]
|
||||
return
|
||||
|
||||
# Don't skip because UI want know about it too
|
||||
if event.topic == "instance.thumbnail.changed":
|
||||
for instance_id, path in event["mapping"].items():
|
||||
self.thumbnail_paths_by_instance_id[instance_id] = path
|
||||
|
||||
# Topics that can be just passed by because are not affecting
|
||||
# controller itself
|
||||
# - "show.card.message"
|
||||
|
|
@ -256,6 +266,42 @@ class QtRemotePublishController(BasePublisherController):
|
|||
def get_existing_subset_names(self, asset_name):
|
||||
pass
|
||||
|
||||
@property
|
||||
def thumbnail_paths_by_instance_id(self):
|
||||
if self._thumbnail_paths_by_instance_id is None:
|
||||
self._thumbnail_paths_by_instance_id = (
|
||||
self._collect_thumbnail_paths_by_instance_id()
|
||||
)
|
||||
return self._thumbnail_paths_by_instance_id
|
||||
|
||||
def get_thumbnail_path_for_instance(self, instance_id):
|
||||
return self.thumbnail_paths_by_instance_id.get(instance_id)
|
||||
|
||||
def set_thumbnail_path_for_instance(self, instance_id, thumbnail_path):
|
||||
self._set_thumbnail_path_on_context(self, instance_id, thumbnail_path)
|
||||
|
||||
@abstractmethod
|
||||
def _collect_thumbnail_paths_by_instance_id(self):
|
||||
"""Collect thumbnail paths by instance id in remote controller.
|
||||
|
||||
These should be collected from 'CreatedContext' there.
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: Mapping of thumbnail path by instance id.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _set_thumbnail_path_on_context(self, instance_id, thumbnail_path):
|
||||
"""Send change of thumbnail path in remote controller.
|
||||
|
||||
That should trigger event 'instance.thumbnail.changed' which is
|
||||
captured and handled in default implementation in this class.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_subset_name(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import sys
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.pipeline.create import (
|
||||
CreatorError,
|
||||
SUBSET_NAME_ALLOWED_SYMBOLS,
|
||||
PRE_CREATE_THUMBNAIL_KEY,
|
||||
TaskNotSetError,
|
||||
)
|
||||
|
||||
from .thumbnail_widget import ThumbnailWidget
|
||||
from .widgets import (
|
||||
IconValuePixmapLabel,
|
||||
CreateBtn,
|
||||
|
|
@ -20,17 +19,18 @@ from .precreate_widget import PreCreateWidget
|
|||
from ..constants import (
|
||||
VARIANT_TOOLTIP,
|
||||
CREATOR_IDENTIFIER_ROLE,
|
||||
FAMILY_ROLE
|
||||
FAMILY_ROLE,
|
||||
CREATOR_THUMBNAIL_ENABLED_ROLE,
|
||||
)
|
||||
|
||||
SEPARATORS = ("---separator---", "---")
|
||||
|
||||
|
||||
class VariantInputsWidget(QtWidgets.QWidget):
|
||||
class ResizeControlWidget(QtWidgets.QWidget):
|
||||
resized = QtCore.Signal()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(VariantInputsWidget, self).resizeEvent(event)
|
||||
super(ResizeControlWidget, self).resizeEvent(event)
|
||||
self.resized.emit()
|
||||
|
||||
|
||||
|
|
@ -153,13 +153,20 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
# --- Creator attr defs ---
|
||||
creators_attrs_widget = QtWidgets.QWidget(creators_splitter)
|
||||
|
||||
# Top part - variant / subset name + thumbnail
|
||||
creators_attrs_top = QtWidgets.QWidget(creators_attrs_widget)
|
||||
|
||||
# Basics - variant / subset name
|
||||
creator_basics_widget = ResizeControlWidget(creators_attrs_top)
|
||||
|
||||
variant_subset_label = QtWidgets.QLabel(
|
||||
"Create options", creators_attrs_widget
|
||||
"Create options", creator_basics_widget
|
||||
)
|
||||
|
||||
variant_subset_widget = QtWidgets.QWidget(creators_attrs_widget)
|
||||
variant_subset_widget = QtWidgets.QWidget(creator_basics_widget)
|
||||
# Variant and subset input
|
||||
variant_widget = VariantInputsWidget(creators_attrs_widget)
|
||||
variant_widget = ResizeControlWidget(variant_subset_widget)
|
||||
variant_widget.setObjectName("VariantInputsWidget")
|
||||
|
||||
variant_input = QtWidgets.QLineEdit(variant_widget)
|
||||
variant_input.setObjectName("VariantInput")
|
||||
|
|
@ -186,6 +193,18 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
variant_subset_layout.addRow("Variant", variant_widget)
|
||||
variant_subset_layout.addRow("Subset", subset_name_input)
|
||||
|
||||
creator_basics_layout = QtWidgets.QVBoxLayout(creator_basics_widget)
|
||||
creator_basics_layout.setContentsMargins(0, 0, 0, 0)
|
||||
creator_basics_layout.addWidget(variant_subset_label, 0)
|
||||
creator_basics_layout.addWidget(variant_subset_widget, 0)
|
||||
|
||||
thumbnail_widget = ThumbnailWidget(controller, creators_attrs_top)
|
||||
|
||||
creators_attrs_top_layout = QtWidgets.QHBoxLayout(creators_attrs_top)
|
||||
creators_attrs_top_layout.setContentsMargins(0, 0, 0, 0)
|
||||
creators_attrs_top_layout.addWidget(creator_basics_widget, 1)
|
||||
creators_attrs_top_layout.addWidget(thumbnail_widget, 0)
|
||||
|
||||
# Precreate attributes widget
|
||||
pre_create_widget = PreCreateWidget(creators_attrs_widget)
|
||||
|
||||
|
|
@ -201,8 +220,7 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
|
||||
creators_attrs_layout = QtWidgets.QVBoxLayout(creators_attrs_widget)
|
||||
creators_attrs_layout.setContentsMargins(0, 0, 0, 0)
|
||||
creators_attrs_layout.addWidget(variant_subset_label, 0)
|
||||
creators_attrs_layout.addWidget(variant_subset_widget, 0)
|
||||
creators_attrs_layout.addWidget(creators_attrs_top, 0)
|
||||
creators_attrs_layout.addWidget(pre_create_widget, 1)
|
||||
creators_attrs_layout.addWidget(create_btn_wrapper, 0)
|
||||
|
||||
|
|
@ -240,6 +258,7 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
|
||||
create_btn.clicked.connect(self._on_create)
|
||||
variant_widget.resized.connect(self._on_variant_widget_resize)
|
||||
creator_basics_widget.resized.connect(self._on_creator_basics_resize)
|
||||
variant_input.returnPressed.connect(self._on_create)
|
||||
variant_input.textChanged.connect(self._on_variant_change)
|
||||
creators_view.selectionModel().currentChanged.connect(
|
||||
|
|
@ -252,6 +271,8 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
self._on_current_session_context_request
|
||||
)
|
||||
tasks_widget.task_changed.connect(self._on_task_change)
|
||||
thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create)
|
||||
thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear)
|
||||
|
||||
controller.event_system.add_callback(
|
||||
"plugins.refresh.finished", self._on_plugins_refresh
|
||||
|
|
@ -278,11 +299,14 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
self._create_btn = create_btn
|
||||
|
||||
self._creator_short_desc_widget = creator_short_desc_widget
|
||||
self._creator_basics_widget = creator_basics_widget
|
||||
self._thumbnail_widget = thumbnail_widget
|
||||
self._pre_create_widget = pre_create_widget
|
||||
self._attr_separator_widget = attr_separator_widget
|
||||
|
||||
self._prereq_timer = prereq_timer
|
||||
self._first_show = True
|
||||
self._last_thumbnail_path = None
|
||||
|
||||
@property
|
||||
def current_asset_name(self):
|
||||
|
|
@ -434,6 +458,10 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
|
||||
item.setData(creator_item.label, QtCore.Qt.DisplayRole)
|
||||
item.setData(identifier, CREATOR_IDENTIFIER_ROLE)
|
||||
item.setData(
|
||||
creator_item.create_allow_thumbnail,
|
||||
CREATOR_THUMBNAIL_ENABLED_ROLE
|
||||
)
|
||||
item.setData(creator_item.family, FAMILY_ROLE)
|
||||
|
||||
# Remove families that are no more available
|
||||
|
|
@ -473,6 +501,13 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
if self._context_change_is_enabled():
|
||||
self._invalidate_prereq_deffered()
|
||||
|
||||
def _on_thumbnail_create(self, thumbnail_path):
|
||||
self._last_thumbnail_path = thumbnail_path
|
||||
self._thumbnail_widget.set_current_thumbnails([thumbnail_path])
|
||||
|
||||
def _on_thumbnail_clear(self):
|
||||
self._last_thumbnail_path = None
|
||||
|
||||
def _on_current_session_context_request(self):
|
||||
self._assets_widget.set_current_session_asset()
|
||||
task_name = self.current_task_name
|
||||
|
|
@ -527,6 +562,10 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
self._set_context_enabled(creator_item.create_allow_context_change)
|
||||
self._refresh_asset()
|
||||
|
||||
self._thumbnail_widget.setVisible(
|
||||
creator_item.create_allow_thumbnail
|
||||
)
|
||||
|
||||
default_variants = creator_item.default_variants
|
||||
if not default_variants:
|
||||
default_variants = ["Main"]
|
||||
|
|
@ -684,6 +723,11 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
self._first_show = False
|
||||
self._on_first_show()
|
||||
|
||||
def _on_creator_basics_resize(self):
|
||||
self._thumbnail_widget.set_height(
|
||||
self._creator_basics_widget.sizeHint().height()
|
||||
)
|
||||
|
||||
def _on_create(self):
|
||||
indexes = self._creators_view.selectedIndexes()
|
||||
if not indexes or len(indexes) > 1:
|
||||
|
|
@ -706,6 +750,11 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
task_name = self._get_task_name()
|
||||
|
||||
pre_create_data = self._pre_create_widget.current_value()
|
||||
if index.data(CREATOR_THUMBNAIL_ENABLED_ROLE):
|
||||
pre_create_data[PRE_CREATE_THUMBNAIL_KEY] = (
|
||||
self._last_thumbnail_path
|
||||
)
|
||||
|
||||
# Where to define these data?
|
||||
# - what data show be stored?
|
||||
instance_data = {
|
||||
|
|
@ -725,3 +774,5 @@ class CreateWidget(QtWidgets.QWidget):
|
|||
if success:
|
||||
self._set_creator(self._selected_creator)
|
||||
self._controller.emit_card_message("Creation finished...")
|
||||
self._last_thumbnail_path = None
|
||||
self._thumbnail_widget.set_current_thumbnails()
|
||||
|
|
|
|||
BIN
openpype/tools/publisher/widgets/images/clear_thumbnail.png
Normal file
BIN
openpype/tools/publisher/widgets/images/clear_thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
508
openpype/tools/publisher/widgets/thumbnail_widget.py
Normal file
508
openpype/tools/publisher/widgets/thumbnail_widget.py
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
import os
|
||||
import uuid
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.style import get_objected_colors
|
||||
from openpype.lib import (
|
||||
run_subprocess,
|
||||
is_oiio_supported,
|
||||
get_oiio_tools_path,
|
||||
get_ffmpeg_tool_path,
|
||||
)
|
||||
from openpype.lib.transcoding import (
|
||||
IMAGE_EXTENSIONS,
|
||||
VIDEO_EXTENSIONS,
|
||||
)
|
||||
|
||||
from openpype.tools.utils import (
|
||||
paint_image_with_color,
|
||||
PixmapButton,
|
||||
)
|
||||
from openpype.tools.publisher.control import CardMessageTypes
|
||||
|
||||
from .icons import get_image
|
||||
|
||||
|
||||
class ThumbnailPainterWidget(QtWidgets.QWidget):
|
||||
width_ratio = 3.0
|
||||
height_ratio = 2.0
|
||||
border_width = 1
|
||||
max_thumbnails = 3
|
||||
offset_sep = 4
|
||||
checker_boxes_count = 20
|
||||
|
||||
def __init__(self, parent):
|
||||
super(ThumbnailPainterWidget, self).__init__(parent)
|
||||
|
||||
border_color = get_objected_colors("bg-buttons").get_qcolor()
|
||||
thumbnail_bg_color = get_objected_colors("bg-view").get_qcolor()
|
||||
overlay_color = get_objected_colors("font").get_qcolor()
|
||||
|
||||
default_image = get_image("thumbnail")
|
||||
default_pix = paint_image_with_color(default_image, border_color)
|
||||
|
||||
self.border_color = border_color
|
||||
self.thumbnail_bg_color = thumbnail_bg_color
|
||||
self.overlay_color = overlay_color
|
||||
self._default_pix = default_pix
|
||||
|
||||
self._cached_pix = None
|
||||
self._current_pixes = None
|
||||
self._has_pixes = False
|
||||
|
||||
@property
|
||||
def has_pixes(self):
|
||||
return self._has_pixes
|
||||
|
||||
def clear_cache(self):
|
||||
self._cached_pix = None
|
||||
self.repaint()
|
||||
|
||||
def set_current_thumbnails(self, thumbnail_paths=None):
|
||||
pixes = []
|
||||
if thumbnail_paths:
|
||||
for thumbnail_path in thumbnail_paths:
|
||||
pixes.append(QtGui.QPixmap(thumbnail_path))
|
||||
|
||||
self._current_pixes = pixes or None
|
||||
self._has_pixes = self._current_pixes is not None
|
||||
self.clear_cache()
|
||||
|
||||
def paintEvent(self, event):
|
||||
if self._cached_pix is None:
|
||||
self._cache_pix()
|
||||
|
||||
painter = QtGui.QPainter()
|
||||
painter.begin(self)
|
||||
painter.drawPixmap(0, 0, self._cached_pix)
|
||||
painter.end()
|
||||
|
||||
def _paint_checker(self, width, height):
|
||||
checker_size = int(float(width) / self.checker_boxes_count)
|
||||
if checker_size < 1:
|
||||
checker_size = 1
|
||||
|
||||
checker_pix = QtGui.QPixmap(checker_size * 2, checker_size * 2)
|
||||
checker_pix.fill(QtCore.Qt.transparent)
|
||||
checker_painter = QtGui.QPainter()
|
||||
checker_painter.begin(checker_pix)
|
||||
checker_painter.setPen(QtCore.Qt.NoPen)
|
||||
checker_painter.setBrush(QtGui.QColor(89, 89, 89))
|
||||
checker_painter.drawRect(
|
||||
0, 0, checker_pix.width(), checker_pix.height()
|
||||
)
|
||||
checker_painter.setBrush(QtGui.QColor(188, 187, 187))
|
||||
checker_painter.drawRect(
|
||||
0, 0, checker_size, checker_size
|
||||
)
|
||||
checker_painter.drawRect(
|
||||
checker_size, checker_size, checker_size, checker_size
|
||||
)
|
||||
checker_painter.end()
|
||||
return checker_pix
|
||||
|
||||
def _paint_default_pix(self, pix_width, pix_height):
|
||||
full_border_width = 2 * self.border_width
|
||||
width = pix_width - full_border_width
|
||||
height = pix_height - full_border_width
|
||||
if width > 100:
|
||||
width = int(width * 0.6)
|
||||
height = int(height * 0.6)
|
||||
|
||||
scaled_pix = self._default_pix.scaled(
|
||||
width,
|
||||
height,
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation
|
||||
)
|
||||
pos_x = int(
|
||||
(pix_width - scaled_pix.width()) / 2
|
||||
)
|
||||
pos_y = int(
|
||||
(pix_height - scaled_pix.height()) / 2
|
||||
)
|
||||
new_pix = QtGui.QPixmap(pix_width, pix_height)
|
||||
new_pix.fill(QtCore.Qt.transparent)
|
||||
pix_painter = QtGui.QPainter()
|
||||
pix_painter.begin(new_pix)
|
||||
pix_painter.setRenderHints(
|
||||
pix_painter.Antialiasing
|
||||
| pix_painter.SmoothPixmapTransform
|
||||
| pix_painter.HighQualityAntialiasing
|
||||
)
|
||||
pix_painter.drawPixmap(pos_x, pos_y, scaled_pix)
|
||||
pix_painter.end()
|
||||
return new_pix
|
||||
|
||||
def _draw_thumbnails(self, thumbnails, pix_width, pix_height):
|
||||
full_border_width = 2 * self.border_width
|
||||
|
||||
checker_pix = self._paint_checker(pix_width, pix_height)
|
||||
|
||||
backgrounded_images = []
|
||||
for src_pix in thumbnails:
|
||||
scaled_pix = src_pix.scaled(
|
||||
pix_width - full_border_width,
|
||||
pix_height - full_border_width,
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation
|
||||
)
|
||||
pos_x = int(
|
||||
(pix_width - scaled_pix.width()) / 2
|
||||
)
|
||||
pos_y = int(
|
||||
(pix_height - scaled_pix.height()) / 2
|
||||
)
|
||||
|
||||
new_pix = QtGui.QPixmap(pix_width, pix_height)
|
||||
new_pix.fill(QtCore.Qt.transparent)
|
||||
pix_painter = QtGui.QPainter()
|
||||
pix_painter.begin(new_pix)
|
||||
pix_painter.setRenderHints(
|
||||
pix_painter.Antialiasing
|
||||
| pix_painter.SmoothPixmapTransform
|
||||
| pix_painter.HighQualityAntialiasing
|
||||
)
|
||||
|
||||
tiled_rect = QtCore.QRectF(
|
||||
pos_x, pos_y, scaled_pix.width(), scaled_pix.height()
|
||||
)
|
||||
pix_painter.drawTiledPixmap(
|
||||
tiled_rect,
|
||||
checker_pix,
|
||||
QtCore.QPointF(0.0, 0.0)
|
||||
)
|
||||
pix_painter.drawPixmap(pos_x, pos_y, scaled_pix)
|
||||
pix_painter.end()
|
||||
backgrounded_images.append(new_pix)
|
||||
return backgrounded_images
|
||||
|
||||
def _cache_pix(self):
|
||||
rect = self.rect()
|
||||
rect_width = rect.width()
|
||||
rect_height = rect.height()
|
||||
|
||||
pix_x_offset = 0
|
||||
pix_y_offset = 0
|
||||
expected_height = int(
|
||||
(rect_width / self.width_ratio) * self.height_ratio
|
||||
)
|
||||
if expected_height > rect_height:
|
||||
expected_height = rect_height
|
||||
expected_width = int(
|
||||
(rect_height / self.height_ratio) * self.width_ratio
|
||||
)
|
||||
pix_x_offset = (rect_width - expected_width) / 2
|
||||
else:
|
||||
expected_width = rect_width
|
||||
pix_y_offset = (rect_height - expected_height) / 2
|
||||
|
||||
if self._current_pixes is None:
|
||||
used_default_pix = True
|
||||
pixes_to_draw = None
|
||||
pixes_len = 1
|
||||
else:
|
||||
used_default_pix = False
|
||||
pixes_to_draw = self._current_pixes
|
||||
if len(pixes_to_draw) > self.max_thumbnails:
|
||||
pixes_to_draw = pixes_to_draw[:-self.max_thumbnails]
|
||||
pixes_len = len(pixes_to_draw)
|
||||
|
||||
width_offset, height_offset = self._get_pix_offset_size(
|
||||
expected_width, expected_height, pixes_len
|
||||
)
|
||||
pix_width = expected_width - width_offset
|
||||
pix_height = expected_height - height_offset
|
||||
|
||||
if used_default_pix:
|
||||
thumbnail_images = [self._paint_default_pix(pix_width, pix_height)]
|
||||
else:
|
||||
thumbnail_images = self._draw_thumbnails(
|
||||
pixes_to_draw, pix_width, pix_height
|
||||
)
|
||||
|
||||
if pixes_len == 1:
|
||||
width_offset_part = 0
|
||||
height_offset_part = 0
|
||||
else:
|
||||
width_offset_part = int(float(width_offset) / (pixes_len - 1))
|
||||
height_offset_part = int(float(height_offset) / (pixes_len - 1))
|
||||
full_width_offset = width_offset + pix_x_offset
|
||||
|
||||
final_pix = QtGui.QPixmap(rect_width, rect_height)
|
||||
final_pix.fill(QtCore.Qt.transparent)
|
||||
|
||||
bg_pen = QtGui.QPen()
|
||||
bg_pen.setWidth(self.border_width)
|
||||
bg_pen.setColor(self.border_color)
|
||||
|
||||
final_painter = QtGui.QPainter()
|
||||
final_painter.begin(final_pix)
|
||||
final_painter.setRenderHints(
|
||||
final_painter.Antialiasing
|
||||
| final_painter.SmoothPixmapTransform
|
||||
| final_painter.HighQualityAntialiasing
|
||||
)
|
||||
final_painter.setBrush(QtGui.QBrush(self.thumbnail_bg_color))
|
||||
final_painter.setPen(bg_pen)
|
||||
final_painter.drawRect(rect)
|
||||
|
||||
for idx, pix in enumerate(thumbnail_images):
|
||||
x_offset = full_width_offset - (width_offset_part * idx)
|
||||
y_offset = (height_offset_part * idx) + pix_y_offset
|
||||
final_painter.drawPixmap(x_offset, y_offset, pix)
|
||||
|
||||
# Draw drop enabled dashes
|
||||
if used_default_pix:
|
||||
pen = QtGui.QPen()
|
||||
pen.setWidth(1)
|
||||
pen.setBrush(QtCore.Qt.darkGray)
|
||||
pen.setStyle(QtCore.Qt.DashLine)
|
||||
final_painter.setPen(pen)
|
||||
final_painter.setBrush(QtCore.Qt.transparent)
|
||||
final_painter.drawRect(rect)
|
||||
|
||||
final_painter.end()
|
||||
|
||||
self._cached_pix = final_pix
|
||||
|
||||
def _get_pix_offset_size(self, width, height, image_count):
|
||||
if image_count == 1:
|
||||
return 0, 0
|
||||
|
||||
part_width = width / self.offset_sep
|
||||
part_height = height / self.offset_sep
|
||||
return part_width, part_height
|
||||
|
||||
|
||||
class ThumbnailWidget(QtWidgets.QWidget):
|
||||
"""Instance thumbnail widget."""
|
||||
|
||||
thumbnail_created = QtCore.Signal(str)
|
||||
thumbnail_cleared = QtCore.Signal()
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
# Missing implementation for thumbnail
|
||||
# - widget kept to make a visial offset of global attr widget offset
|
||||
super(ThumbnailWidget, self).__init__(parent)
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
thumbnail_painter = ThumbnailPainterWidget(self)
|
||||
|
||||
buttons_widget = QtWidgets.QWidget(self)
|
||||
buttons_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
|
||||
icon_color = get_objected_colors("bg-view-selection").get_qcolor()
|
||||
icon_color.setAlpha(255)
|
||||
clear_image = get_image("clear_thumbnail")
|
||||
clear_pix = paint_image_with_color(clear_image, icon_color)
|
||||
|
||||
clear_button = PixmapButton(clear_pix, buttons_widget)
|
||||
clear_button.setObjectName("ThumbnailPixmapHoverButton")
|
||||
|
||||
buttons_layout = QtWidgets.QHBoxLayout(buttons_widget)
|
||||
buttons_layout.setContentsMargins(3, 3, 3, 3)
|
||||
buttons_layout.addStretch(1)
|
||||
buttons_layout.addWidget(clear_button, 0)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(thumbnail_painter)
|
||||
|
||||
clear_button.clicked.connect(self._on_clear_clicked)
|
||||
|
||||
self._controller = controller
|
||||
self._output_dir = controller.get_thumbnail_temp_dir_path()
|
||||
|
||||
self._review_extensions = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS)
|
||||
|
||||
self._height = None
|
||||
self._width = None
|
||||
self._adapted_to_size = True
|
||||
self._last_width = None
|
||||
self._last_height = None
|
||||
|
||||
self._buttons_widget = buttons_widget
|
||||
self._thumbnail_painter = thumbnail_painter
|
||||
|
||||
@property
|
||||
def width_ratio(self):
|
||||
return self._thumbnail_painter.width_ratio
|
||||
|
||||
@property
|
||||
def height_ratio(self):
|
||||
return self._thumbnail_painter.height_ratio
|
||||
|
||||
def _get_filepath_from_event(self, event):
|
||||
mime_data = event.mimeData()
|
||||
if not mime_data.hasUrls():
|
||||
return None
|
||||
|
||||
filepaths = []
|
||||
for url in mime_data.urls():
|
||||
filepath = url.toLocalFile()
|
||||
if os.path.exists(filepath):
|
||||
filepaths.append(filepath)
|
||||
|
||||
if len(filepaths) == 1:
|
||||
filepath = filepaths[0]
|
||||
ext = os.path.splitext(filepath)[-1]
|
||||
if ext in self._review_extensions:
|
||||
return filepath
|
||||
return None
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
filepath = self._get_filepath_from_event(event)
|
||||
if filepath:
|
||||
event.setDropAction(QtCore.Qt.CopyAction)
|
||||
event.accept()
|
||||
|
||||
def dragLeaveEvent(self, event):
|
||||
event.accept()
|
||||
|
||||
def dropEvent(self, event):
|
||||
filepath = self._get_filepath_from_event(event)
|
||||
if not filepath:
|
||||
return
|
||||
|
||||
output = export_thumbnail(filepath, self._output_dir)
|
||||
if output:
|
||||
self.thumbnail_created.emit(output)
|
||||
else:
|
||||
self._controller.emit_card_message(
|
||||
"Couldn't convert the source for thumbnail",
|
||||
CardMessageTypes.error
|
||||
)
|
||||
|
||||
def set_adapted_to_hint(self, enabled):
|
||||
self._adapted_to_size = enabled
|
||||
if self._width is not None:
|
||||
self.setMinimumHeight(0)
|
||||
self._width = None
|
||||
|
||||
if self._height is not None:
|
||||
self.setMinimumWidth(0)
|
||||
self._height = None
|
||||
|
||||
def set_width(self, width):
|
||||
if self._width == width:
|
||||
return
|
||||
|
||||
self._adapted_to_size = False
|
||||
self._width = width
|
||||
self.setMinimumHeight(int(
|
||||
(width / self.width_ratio) * self.height_ratio
|
||||
))
|
||||
if self._height is not None:
|
||||
self.setMinimumWidth(0)
|
||||
self._height = None
|
||||
self._thumbnail_painter.clear_cache()
|
||||
|
||||
def set_height(self, height):
|
||||
if self._height == height:
|
||||
return
|
||||
|
||||
self._height = height
|
||||
self._adapted_to_size = False
|
||||
self.setMinimumWidth(int(
|
||||
(height / self.height_ratio) * self.width_ratio
|
||||
))
|
||||
if self._width is not None:
|
||||
self.setMinimumHeight(0)
|
||||
self._width = None
|
||||
|
||||
self._thumbnail_painter.clear_cache()
|
||||
|
||||
def set_current_thumbnails(self, thumbnail_paths=None):
|
||||
self._thumbnail_painter.set_current_thumbnails(thumbnail_paths)
|
||||
self._update_buttons_position()
|
||||
|
||||
def _on_clear_clicked(self):
|
||||
self.set_current_thumbnails()
|
||||
self.thumbnail_cleared.emit()
|
||||
|
||||
def _adapt_to_size(self):
|
||||
if not self._adapted_to_size:
|
||||
return
|
||||
|
||||
width = self.width()
|
||||
height = self.height()
|
||||
if width == self._last_width and height == self._last_height:
|
||||
return
|
||||
|
||||
self._last_width = width
|
||||
self._last_height = height
|
||||
self._thumbnail_painter.clear_cache()
|
||||
|
||||
def _update_buttons_position(self):
|
||||
self._buttons_widget.setVisible(self._thumbnail_painter.has_pixes)
|
||||
size = self.size()
|
||||
my_height = size.height()
|
||||
height = self._buttons_widget.sizeHint().height()
|
||||
self._buttons_widget.setGeometry(
|
||||
0, my_height - height,
|
||||
size.width(), height
|
||||
)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(ThumbnailWidget, self).resizeEvent(event)
|
||||
self._adapt_to_size()
|
||||
self._update_buttons_position()
|
||||
|
||||
def showEvent(self, event):
|
||||
super(ThumbnailWidget, self).showEvent(event)
|
||||
self._adapt_to_size()
|
||||
self._update_buttons_position()
|
||||
|
||||
|
||||
def _run_silent_subprocess(args):
|
||||
with open(os.devnull, "w") as devnull:
|
||||
run_subprocess(args, stdout=devnull, stderr=devnull)
|
||||
|
||||
|
||||
def _convert_thumbnail_oiio(src_path, dst_path):
|
||||
if not is_oiio_supported():
|
||||
return None
|
||||
|
||||
oiio_cmd = [
|
||||
get_oiio_tools_path(),
|
||||
"-i", src_path,
|
||||
"--subimage", "0",
|
||||
"-o", dst_path
|
||||
]
|
||||
try:
|
||||
_run_silent_subprocess(oiio_cmd)
|
||||
except Exception:
|
||||
return None
|
||||
return dst_path
|
||||
|
||||
|
||||
def _convert_thumbnail_ffmpeg(src_path, dst_path):
|
||||
ffmpeg_cmd = [
|
||||
get_ffmpeg_tool_path(),
|
||||
"-y",
|
||||
"-i", src_path,
|
||||
dst_path
|
||||
]
|
||||
try:
|
||||
_run_silent_subprocess(ffmpeg_cmd)
|
||||
except Exception:
|
||||
return None
|
||||
return dst_path
|
||||
|
||||
|
||||
def export_thumbnail(src_path, root_dir):
|
||||
if not os.path.exists(root_dir):
|
||||
os.makedirs(root_dir)
|
||||
|
||||
ext = os.path.splitext(src_path)[-1]
|
||||
if ext not in (".jpeg", ".jpg", ".png"):
|
||||
ext = ".jpeg"
|
||||
filename = str(uuid.uuid4()) + ext
|
||||
dst_path = os.path.join(root_dir, filename)
|
||||
|
||||
output_path = _convert_thumbnail_oiio(src_path, dst_path)
|
||||
if not output_path:
|
||||
output_path = _convert_thumbnail_ffmpeg(src_path, dst_path)
|
||||
return output_path
|
||||
|
|
@ -3,6 +3,8 @@ import os
|
|||
import re
|
||||
import copy
|
||||
import functools
|
||||
import uuid
|
||||
import shutil
|
||||
import collections
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
import qtawesome
|
||||
|
|
@ -22,6 +24,7 @@ from openpype.pipeline.create import (
|
|||
SUBSET_NAME_ALLOWED_SYMBOLS,
|
||||
TaskNotSetError,
|
||||
)
|
||||
from .thumbnail_widget import ThumbnailWidget
|
||||
from .assets_widget import AssetsDialog
|
||||
from .tasks_widget import TasksModel
|
||||
from .icons import (
|
||||
|
|
@ -124,6 +127,7 @@ class PublishIconBtn(IconButton):
|
|||
- error : other error happened
|
||||
- success : publishing finished
|
||||
"""
|
||||
|
||||
def __init__(self, pixmap_path, *args, **kwargs):
|
||||
super(PublishIconBtn, self).__init__(*args, **kwargs)
|
||||
|
||||
|
|
@ -1063,6 +1067,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
|||
|
||||
def _on_submit(self):
|
||||
"""Commit changes for selected instances."""
|
||||
|
||||
variant_value = None
|
||||
asset_name = None
|
||||
task_name = None
|
||||
|
|
@ -1131,6 +1136,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget):
|
|||
|
||||
def _on_cancel(self):
|
||||
"""Cancel changes and set back to their irigin value."""
|
||||
|
||||
self.variant_input.reset_to_origin()
|
||||
self.asset_value_widget.reset_to_origin()
|
||||
self.task_value_widget.reset_to_origin()
|
||||
|
|
@ -1256,6 +1262,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
|
||||
def set_instances_valid(self, valid):
|
||||
"""Change valid state of current instances."""
|
||||
|
||||
if (
|
||||
self._content_widget is not None
|
||||
and self._content_widget.isEnabled() != valid
|
||||
|
|
@ -1264,6 +1271,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
|
|||
|
||||
def set_current_instances(self, instances):
|
||||
"""Set current instances for which are attribute definitions shown."""
|
||||
|
||||
prev_content_widget = self._scroll_area.widget()
|
||||
if prev_content_widget:
|
||||
self._scroll_area.takeWidget()
|
||||
|
|
@ -1353,6 +1361,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
families. Similar definitions are merged into one (different label
|
||||
does not count).
|
||||
"""
|
||||
|
||||
def __init__(self, controller, parent):
|
||||
super(PublishPluginAttrsWidget, self).__init__(parent)
|
||||
|
||||
|
|
@ -1386,6 +1395,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
|
|||
|
||||
def set_current_instances(self, instances, context_selected):
|
||||
"""Set current instances for which are attribute definitions shown."""
|
||||
|
||||
prev_content_widget = self._scroll_area.widget()
|
||||
if prev_content_widget:
|
||||
self._scroll_area.takeWidget()
|
||||
|
|
@ -1471,7 +1481,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
|
|||
|
||||
# Global attributes
|
||||
global_attrs_widget = GlobalAttrsWidget(controller, top_widget)
|
||||
thumbnail_widget = ThumbnailWidget(top_widget)
|
||||
thumbnail_widget = ThumbnailWidget(controller, top_widget)
|
||||
|
||||
top_layout = QtWidgets.QHBoxLayout(top_widget)
|
||||
top_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
@ -1559,6 +1569,12 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
|
|||
self._on_instance_context_changed
|
||||
)
|
||||
convert_btn.clicked.connect(self._on_convert_click)
|
||||
thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create)
|
||||
thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear)
|
||||
|
||||
controller.event_system.add_callback(
|
||||
"instance.thumbnail.changed", self._on_thumbnail_changed
|
||||
)
|
||||
|
||||
self._controller = controller
|
||||
|
||||
|
|
@ -1568,7 +1584,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
|
|||
|
||||
self.creator_attrs_widget = creator_attrs_widget
|
||||
self.publish_attrs_widget = publish_attrs_widget
|
||||
self.thumbnail_widget = thumbnail_widget
|
||||
self._thumbnail_widget = thumbnail_widget
|
||||
|
||||
self.top_bottom = top_bottom
|
||||
self.bottom_separator = bottom_separator
|
||||
|
|
@ -1595,10 +1611,11 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
|
|||
"""Change currently selected items.
|
||||
|
||||
Args:
|
||||
instances(list<CreatedInstance>): List of currently selected
|
||||
instances(List[CreatedInstance]): List of currently selected
|
||||
instances.
|
||||
context_selected(bool): Is context selected.
|
||||
"""
|
||||
|
||||
all_valid = True
|
||||
for instance in instances:
|
||||
if not instance.has_valid_context:
|
||||
|
|
@ -1620,35 +1637,74 @@ class SubsetAttributesWidget(QtWidgets.QWidget):
|
|||
self.creator_attrs_widget.set_instances_valid(all_valid)
|
||||
self.publish_attrs_widget.set_instances_valid(all_valid)
|
||||
|
||||
self._update_thumbnails()
|
||||
|
||||
class ThumbnailWidget(QtWidgets.QWidget):
|
||||
"""Instance thumbnail widget.
|
||||
def _on_thumbnail_create(self, path):
|
||||
instance_ids = [
|
||||
instance.id
|
||||
for instance in self._current_instances
|
||||
]
|
||||
if self._context_selected:
|
||||
instance_ids.append(None)
|
||||
|
||||
Logic implementation of this widget is missing but widget is used
|
||||
to offset `GlobalAttrsWidget` inputs visually.
|
||||
"""
|
||||
def __init__(self, parent):
|
||||
super(ThumbnailWidget, self).__init__(parent)
|
||||
if not instance_ids:
|
||||
return
|
||||
|
||||
# Missing implementation for thumbnail
|
||||
# - widget kept to make a visial offset of global attr widget offset
|
||||
# default_pix = get_pixmap("thumbnail")
|
||||
default_pix = QtGui.QPixmap(10, 10)
|
||||
default_pix.fill(QtCore.Qt.transparent)
|
||||
mapping = {}
|
||||
if len(instance_ids) == 1:
|
||||
mapping[instance_ids[0]] = path
|
||||
|
||||
thumbnail_label = QtWidgets.QLabel(self)
|
||||
thumbnail_label.setPixmap(
|
||||
default_pix.scaled(
|
||||
200, 100,
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation
|
||||
)
|
||||
else:
|
||||
for instance_id in instance_ids:
|
||||
root = os.path.dirname(path)
|
||||
ext = os.path.splitext(path)[-1]
|
||||
dst_path = os.path.join(root, str(uuid.uuid4()) + ext)
|
||||
shutil.copy(path, dst_path)
|
||||
mapping[instance_id] = dst_path
|
||||
|
||||
self._controller.set_thumbnail_paths_for_instances(mapping)
|
||||
|
||||
def _on_thumbnail_clear(self):
|
||||
instance_ids = [
|
||||
instance.id
|
||||
for instance in self._current_instances
|
||||
]
|
||||
if self._context_selected:
|
||||
instance_ids.append(None)
|
||||
|
||||
if not instance_ids:
|
||||
return
|
||||
|
||||
mapping = {
|
||||
instance_id: None
|
||||
for instance_id in instance_ids
|
||||
}
|
||||
self._controller.set_thumbnail_paths_for_instances(mapping)
|
||||
|
||||
def _on_thumbnail_changed(self, event):
|
||||
self._update_thumbnails()
|
||||
|
||||
def _update_thumbnails(self):
|
||||
instance_ids = [
|
||||
instance.id
|
||||
for instance in self._current_instances
|
||||
]
|
||||
if self._context_selected:
|
||||
instance_ids.append(None)
|
||||
|
||||
if not instance_ids:
|
||||
self._thumbnail_widget.setVisible(False)
|
||||
self._thumbnail_widget.set_current_thumbnails(None)
|
||||
return
|
||||
|
||||
mapping = self._controller.get_thumbnail_paths_for_instances(
|
||||
instance_ids
|
||||
)
|
||||
thumbnail_paths = []
|
||||
for instance_id in instance_ids:
|
||||
path = mapping[instance_id]
|
||||
if path:
|
||||
thumbnail_paths.append(path)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(thumbnail_label, alignment=QtCore.Qt.AlignCenter)
|
||||
|
||||
self.thumbnail_label = thumbnail_label
|
||||
self.default_pix = default_pix
|
||||
self.current_pix = None
|
||||
self._thumbnail_widget.setVisible(True)
|
||||
self._thumbnail_widget.set_current_thumbnails(thumbnail_paths)
|
||||
|
|
|
|||
|
|
@ -391,6 +391,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
def closeEvent(self, event):
|
||||
self.save_changes()
|
||||
self._reset_on_show = True
|
||||
self._controller.clear_thumbnail_temp_dir_path()
|
||||
super(PublisherWindow, self).closeEvent(event)
|
||||
|
||||
def save_changes(self):
|
||||
|
|
|
|||
|
|
@ -892,6 +892,10 @@ class ProjectWidget(SettingsCategoryWidget):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super(ProjectWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
def set_edit_mode(self, enabled):
|
||||
super(ProjectWidget, self).set_edit_mode(enabled)
|
||||
self.project_list_widget.set_edit_mode(enabled)
|
||||
|
||||
def _check_last_saved_info(self):
|
||||
if self.is_modifying_defaults:
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -646,6 +646,9 @@ class UnsavedChangesDialog(QtWidgets.QDialog):
|
|||
|
||||
def __init__(self, parent=None):
|
||||
super(UnsavedChangesDialog, self).__init__(parent)
|
||||
|
||||
self.setWindowTitle("Unsaved changes")
|
||||
|
||||
message_label = QtWidgets.QLabel(self.message)
|
||||
|
||||
btns_widget = QtWidgets.QWidget(self)
|
||||
|
|
@ -1009,6 +1012,7 @@ class ProjectListWidget(QtWidgets.QWidget):
|
|||
|
||||
self._entity = None
|
||||
self.current_project = None
|
||||
self._edit_mode = True
|
||||
|
||||
super(ProjectListWidget, self).__init__(parent)
|
||||
self.setObjectName("ProjectListWidget")
|
||||
|
|
@ -1061,6 +1065,10 @@ class ProjectListWidget(QtWidgets.QWidget):
|
|||
self.project_model = project_model
|
||||
self.inactive_chk = inactive_chk
|
||||
|
||||
def set_edit_mode(self, enabled):
|
||||
if self._edit_mode is not enabled:
|
||||
self._edit_mode = enabled
|
||||
|
||||
def set_entity(self, entity):
|
||||
self._entity = entity
|
||||
|
||||
|
|
@ -1112,7 +1120,7 @@ class ProjectListWidget(QtWidgets.QWidget):
|
|||
|
||||
save_changes = False
|
||||
change_project = False
|
||||
if self.validate_context_change():
|
||||
if not self._edit_mode or self.validate_context_change():
|
||||
change_project = True
|
||||
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -401,7 +401,7 @@ class TrayManager:
|
|||
|
||||
def initialize_modules(self):
|
||||
"""Add modules to tray."""
|
||||
from openpype_interfaces import (
|
||||
from openpype.modules import (
|
||||
ITrayAction,
|
||||
ITrayService
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from .widgets import (
|
|||
ExpandBtn,
|
||||
PixmapLabel,
|
||||
IconButton,
|
||||
PixmapButton,
|
||||
SeparatorWidget,
|
||||
)
|
||||
from .views import DeselectableTreeView
|
||||
|
|
@ -38,6 +39,7 @@ __all__ = (
|
|||
"ExpandBtn",
|
||||
"PixmapLabel",
|
||||
"IconButton",
|
||||
"PixmapButton",
|
||||
"SeparatorWidget",
|
||||
|
||||
"DeselectableTreeView",
|
||||
|
|
|
|||
|
|
@ -79,6 +79,11 @@ def paint_image_with_color(image, color):
|
|||
pixmap.fill(QtCore.Qt.transparent)
|
||||
|
||||
painter = QtGui.QPainter(pixmap)
|
||||
painter.setRenderHints(
|
||||
painter.Antialiasing
|
||||
| painter.SmoothPixmapTransform
|
||||
| painter.HighQualityAntialiasing
|
||||
)
|
||||
painter.setClipRegion(alpha_region)
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
painter.setBrush(color)
|
||||
|
|
|
|||
|
|
@ -252,6 +252,90 @@ class PixmapLabel(QtWidgets.QLabel):
|
|||
super(PixmapLabel, self).resizeEvent(event)
|
||||
|
||||
|
||||
class PixmapButtonPainter(QtWidgets.QWidget):
|
||||
def __init__(self, pixmap, parent):
|
||||
super(PixmapButtonPainter, self).__init__(parent)
|
||||
|
||||
self._pixmap = pixmap
|
||||
self._cached_pixmap = None
|
||||
|
||||
def set_pixmap(self, pixmap):
|
||||
self._pixmap = pixmap
|
||||
self._cached_pixmap = None
|
||||
|
||||
self.repaint()
|
||||
|
||||
def _cache_pixmap(self):
|
||||
size = self.size()
|
||||
self._cached_pixmap = self._pixmap.scaled(
|
||||
size.width(),
|
||||
size.height(),
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.SmoothTransformation
|
||||
)
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QtGui.QPainter()
|
||||
painter.begin(self)
|
||||
if self._pixmap is None:
|
||||
painter.end()
|
||||
return
|
||||
|
||||
painter.setRenderHints(
|
||||
painter.Antialiasing
|
||||
| painter.SmoothPixmapTransform
|
||||
| painter.HighQualityAntialiasing
|
||||
)
|
||||
if self._cached_pixmap is None:
|
||||
self._cache_pixmap()
|
||||
|
||||
painter.drawPixmap(0, 0, self._cached_pixmap)
|
||||
|
||||
painter.end()
|
||||
|
||||
|
||||
class PixmapButton(ClickableFrame):
|
||||
def __init__(self, pixmap=None, parent=None):
|
||||
super(PixmapButton, self).__init__(parent)
|
||||
|
||||
button_painter = PixmapButtonPainter(pixmap, self)
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(2, 2, 2, 2)
|
||||
|
||||
self._button_painter = button_painter
|
||||
|
||||
def setContentsMargins(self, *args):
|
||||
layout = self.layout()
|
||||
layout.setContentsMargins(*args)
|
||||
self._update_painter_geo()
|
||||
|
||||
def set_pixmap(self, pixmap):
|
||||
self._button_painter.set_pixmap(pixmap)
|
||||
|
||||
def sizeHint(self):
|
||||
font_height = self.fontMetrics().height()
|
||||
return QtCore.QSize(font_height, font_height)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(PixmapButton, self).resizeEvent(event)
|
||||
self._update_painter_geo()
|
||||
|
||||
def showEvent(self, event):
|
||||
super(PixmapButton, self).showEvent(event)
|
||||
self._update_painter_geo()
|
||||
|
||||
def _update_painter_geo(self):
|
||||
size = self.size()
|
||||
layout = self.layout()
|
||||
left, top, right, bottom = layout.getContentsMargins()
|
||||
self._button_painter.setGeometry(
|
||||
left,
|
||||
top,
|
||||
size.width() - (left + right),
|
||||
size.height() - (top + bottom)
|
||||
)
|
||||
|
||||
|
||||
class OptionalMenu(QtWidgets.QMenu):
|
||||
"""A subclass of `QtWidgets.QMenu` to work with `OptionalAction`
|
||||
|
||||
|
|
@ -474,8 +558,10 @@ class SeparatorWidget(QtWidgets.QFrame):
|
|||
self.set_size(size)
|
||||
|
||||
def set_size(self, size):
|
||||
if size == self._size:
|
||||
return
|
||||
if size != self._size:
|
||||
self._set_size(size)
|
||||
|
||||
def _set_size(self, size):
|
||||
if self._orientation == QtCore.Qt.Vertical:
|
||||
self.setMinimumWidth(size)
|
||||
self.setMaximumWidth(size)
|
||||
|
|
@ -499,6 +585,4 @@ class SeparatorWidget(QtWidgets.QFrame):
|
|||
|
||||
self._orientation = orientation
|
||||
|
||||
size = self._size
|
||||
self._size = None
|
||||
self.set_size(size)
|
||||
self._set_size(self._size)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue