mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge pull request #2778 from pypeclub/feature/OP-2267_Standalone-publisher-using-new-publisher
This commit is contained in:
commit
09315decbf
43 changed files with 1836 additions and 326 deletions
|
|
@ -42,6 +42,12 @@ def standalonepublisher():
|
|||
PypeCommands().launch_standalone_publisher()
|
||||
|
||||
|
||||
@main.command()
|
||||
def traypublisher():
|
||||
"""Show new OpenPype Standalone publisher UI."""
|
||||
PypeCommands().launch_traypublisher()
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.option("-d", "--debug",
|
||||
is_flag=True, help=("Run pype tray in debug mode"))
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"asset": "sq01_sh0010",
|
||||
"task": "Compositing",
|
||||
"variant": "myVariant",
|
||||
"uuid": "a485f148-9121-46a5-8157-aa64df0fb449",
|
||||
"instance_id": "a485f148-9121-46a5-8157-aa64df0fb449",
|
||||
"creator_attributes": {
|
||||
"number_key": 10,
|
||||
"ha": 10
|
||||
|
|
@ -29,8 +29,8 @@
|
|||
"asset": "sq01_sh0010",
|
||||
"task": "Compositing",
|
||||
"variant": "myVariant2",
|
||||
"uuid": "a485f148-9121-46a5-8157-aa64df0fb444",
|
||||
"creator_attributes": {},
|
||||
"instance_id": "a485f148-9121-46a5-8157-aa64df0fb444",
|
||||
"publish_attributes": {
|
||||
"CollectFtrackApi": {
|
||||
"add_ftrack_family": true
|
||||
|
|
@ -47,8 +47,8 @@
|
|||
"asset": "sq01_sh0010",
|
||||
"task": "Compositing",
|
||||
"variant": "Main",
|
||||
"uuid": "3607bc95-75f6-4648-a58d-e699f413d09f",
|
||||
"creator_attributes": {},
|
||||
"instance_id": "3607bc95-75f6-4648-a58d-e699f413d09f",
|
||||
"publish_attributes": {
|
||||
"CollectFtrackApi": {
|
||||
"add_ftrack_family": true
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
"asset": "sq01_sh0020",
|
||||
"task": "Compositing",
|
||||
"variant": "Main2",
|
||||
"uuid": "4ccf56f6-9982-4837-967c-a49695dbe8eb",
|
||||
"instance_id": "4ccf56f6-9982-4837-967c-a49695dbe8eb",
|
||||
"creator_attributes": {},
|
||||
"publish_attributes": {
|
||||
"CollectFtrackApi": {
|
||||
|
|
@ -83,7 +83,7 @@
|
|||
"asset": "sq01_sh0020",
|
||||
"task": "Compositing",
|
||||
"variant": "Main2",
|
||||
"uuid": "4ccf56f6-9982-4837-967c-a49695dbe8ec",
|
||||
"instance_id": "4ccf56f6-9982-4837-967c-a49695dbe8ec",
|
||||
"creator_attributes": {},
|
||||
"publish_attributes": {
|
||||
"CollectFtrackApi": {
|
||||
|
|
@ -101,7 +101,7 @@
|
|||
"asset": "Alpaca_01",
|
||||
"task": "modeling",
|
||||
"variant": "Main",
|
||||
"uuid": "7c9ddfc7-9f9c-4c1c-b233-38c966735fb6",
|
||||
"instance_id": "7c9ddfc7-9f9c-4c1c-b233-38c966735fb6",
|
||||
"creator_attributes": {},
|
||||
"publish_attributes": {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ def update_instances(update_list):
|
|||
|
||||
instances = HostContext.get_instances()
|
||||
for instance_data in instances:
|
||||
instance_id = instance_data["uuid"]
|
||||
instance_id = instance_data["instance_id"]
|
||||
if instance_id in updated_instances:
|
||||
new_instance_data = updated_instances[instance_id]
|
||||
old_keys = set(instance_data.keys())
|
||||
|
|
@ -132,10 +132,10 @@ def remove_instances(instances):
|
|||
|
||||
current_instances = HostContext.get_instances()
|
||||
for instance in instances:
|
||||
instance_id = instance.data["uuid"]
|
||||
instance_id = instance.data["instance_id"]
|
||||
found_idx = None
|
||||
for idx, _instance in enumerate(current_instances):
|
||||
if instance_id == _instance["uuid"]:
|
||||
if instance_id == _instance["instance_id"]:
|
||||
found_idx = idx
|
||||
break
|
||||
|
||||
|
|
|
|||
20
openpype/hosts/traypublisher/api/__init__.py
Normal file
20
openpype/hosts/traypublisher/api/__init__.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from .pipeline import (
|
||||
install,
|
||||
ls,
|
||||
|
||||
set_project_name,
|
||||
get_context_title,
|
||||
get_context_data,
|
||||
update_context_data,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"install",
|
||||
"ls",
|
||||
|
||||
"set_project_name",
|
||||
"get_context_title",
|
||||
"get_context_data",
|
||||
"update_context_data",
|
||||
)
|
||||
180
openpype/hosts/traypublisher/api/pipeline.py
Normal file
180
openpype/hosts/traypublisher/api/pipeline.py
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import os
|
||||
import json
|
||||
import tempfile
|
||||
import atexit
|
||||
|
||||
from avalon import io
|
||||
import avalon.api
|
||||
import pyblish.api
|
||||
|
||||
from openpype.pipeline import BaseCreator
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__)
|
||||
))
|
||||
PUBLISH_PATH = os.path.join(ROOT_DIR, "plugins", "publish")
|
||||
CREATE_PATH = os.path.join(ROOT_DIR, "plugins", "create")
|
||||
|
||||
|
||||
class HostContext:
|
||||
_context_json_path = None
|
||||
|
||||
@staticmethod
|
||||
def _on_exit():
|
||||
if (
|
||||
HostContext._context_json_path
|
||||
and os.path.exists(HostContext._context_json_path)
|
||||
):
|
||||
os.remove(HostContext._context_json_path)
|
||||
|
||||
@classmethod
|
||||
def get_context_json_path(cls):
|
||||
if cls._context_json_path is None:
|
||||
output_file = tempfile.NamedTemporaryFile(
|
||||
mode="w", prefix="traypub_", suffix=".json"
|
||||
)
|
||||
output_file.close()
|
||||
cls._context_json_path = output_file.name
|
||||
atexit.register(HostContext._on_exit)
|
||||
print(cls._context_json_path)
|
||||
return cls._context_json_path
|
||||
|
||||
@classmethod
|
||||
def _get_data(cls, group=None):
|
||||
json_path = cls.get_context_json_path()
|
||||
data = {}
|
||||
if not os.path.exists(json_path):
|
||||
with open(json_path, "w") as json_stream:
|
||||
json.dump(data, json_stream)
|
||||
else:
|
||||
with open(json_path, "r") as json_stream:
|
||||
content = json_stream.read()
|
||||
if content:
|
||||
data = json.loads(content)
|
||||
if group is None:
|
||||
return data
|
||||
return data.get(group)
|
||||
|
||||
@classmethod
|
||||
def _save_data(cls, group, new_data):
|
||||
json_path = cls.get_context_json_path()
|
||||
data = cls._get_data()
|
||||
data[group] = new_data
|
||||
with open(json_path, "w") as json_stream:
|
||||
json.dump(data, json_stream)
|
||||
|
||||
@classmethod
|
||||
def add_instance(cls, instance):
|
||||
instances = cls.get_instances()
|
||||
instances.append(instance)
|
||||
cls.save_instances(instances)
|
||||
|
||||
@classmethod
|
||||
def get_instances(cls):
|
||||
return cls._get_data("instances") or []
|
||||
|
||||
@classmethod
|
||||
def save_instances(cls, instances):
|
||||
cls._save_data("instances", instances)
|
||||
|
||||
@classmethod
|
||||
def get_context_data(cls):
|
||||
return cls._get_data("context") or {}
|
||||
|
||||
@classmethod
|
||||
def save_context_data(cls, data):
|
||||
cls._save_data("context", data)
|
||||
|
||||
@classmethod
|
||||
def get_project_name(cls):
|
||||
return cls._get_data("project_name")
|
||||
|
||||
@classmethod
|
||||
def set_project_name(cls, project_name):
|
||||
cls._save_data("project_name", project_name)
|
||||
|
||||
@classmethod
|
||||
def get_data_to_store(cls):
|
||||
return {
|
||||
"project_name": cls.get_project_name(),
|
||||
"instances": cls.get_instances(),
|
||||
"context": cls.get_context_data(),
|
||||
}
|
||||
|
||||
|
||||
def list_instances():
|
||||
return HostContext.get_instances()
|
||||
|
||||
|
||||
def update_instances(update_list):
|
||||
updated_instances = {}
|
||||
for instance, _changes in update_list:
|
||||
updated_instances[instance.id] = instance.data_to_store()
|
||||
|
||||
instances = HostContext.get_instances()
|
||||
for instance_data in instances:
|
||||
instance_id = instance_data["instance_id"]
|
||||
if instance_id in updated_instances:
|
||||
new_instance_data = updated_instances[instance_id]
|
||||
old_keys = set(instance_data.keys())
|
||||
new_keys = set(new_instance_data.keys())
|
||||
instance_data.update(new_instance_data)
|
||||
for key in (old_keys - new_keys):
|
||||
instance_data.pop(key)
|
||||
|
||||
HostContext.save_instances(instances)
|
||||
|
||||
|
||||
def remove_instances(instances):
|
||||
if not isinstance(instances, (tuple, list)):
|
||||
instances = [instances]
|
||||
|
||||
current_instances = HostContext.get_instances()
|
||||
for instance in instances:
|
||||
instance_id = instance.data["instance_id"]
|
||||
found_idx = None
|
||||
for idx, _instance in enumerate(current_instances):
|
||||
if instance_id == _instance["instance_id"]:
|
||||
found_idx = idx
|
||||
break
|
||||
|
||||
if found_idx is not None:
|
||||
current_instances.pop(found_idx)
|
||||
HostContext.save_instances(current_instances)
|
||||
|
||||
|
||||
def get_context_data():
|
||||
return HostContext.get_context_data()
|
||||
|
||||
|
||||
def update_context_data(data, changes):
|
||||
HostContext.save_context_data(data)
|
||||
|
||||
|
||||
def get_context_title():
|
||||
return HostContext.get_project_name()
|
||||
|
||||
|
||||
def ls():
|
||||
"""Probably will never return loaded containers."""
|
||||
return []
|
||||
|
||||
|
||||
def install():
|
||||
"""This is called before a project is known.
|
||||
|
||||
Project is defined with 'set_project_name'.
|
||||
"""
|
||||
os.environ["AVALON_APP"] = "traypublisher"
|
||||
|
||||
pyblish.api.register_host("traypublisher")
|
||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
avalon.api.register_plugin_path(BaseCreator, CREATE_PATH)
|
||||
|
||||
|
||||
def set_project_name(project_name):
|
||||
# TODO Deregister project specific plugins and register new project plugins
|
||||
os.environ["AVALON_PROJECT"] = project_name
|
||||
avalon.api.Session["AVALON_PROJECT"] = project_name
|
||||
io.install()
|
||||
HostContext.set_project_name(project_name)
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
from openpype.hosts.traypublisher.api import pipeline
|
||||
from openpype.pipeline import (
|
||||
Creator,
|
||||
CreatedInstance,
|
||||
lib
|
||||
)
|
||||
|
||||
|
||||
class WorkfileCreator(Creator):
|
||||
identifier = "workfile"
|
||||
label = "Workfile"
|
||||
family = "workfile"
|
||||
description = "Publish backup of workfile"
|
||||
|
||||
create_allow_context_change = True
|
||||
|
||||
extensions = [
|
||||
# Maya
|
||||
".ma", ".mb",
|
||||
# Nuke
|
||||
".nk",
|
||||
# Hiero
|
||||
".hrox",
|
||||
# Houdini
|
||||
".hip", ".hiplc", ".hipnc",
|
||||
# Blender
|
||||
".blend",
|
||||
# Celaction
|
||||
".scn",
|
||||
# TVPaint
|
||||
".tvpp",
|
||||
# Fusion
|
||||
".comp",
|
||||
# Harmony
|
||||
".zip",
|
||||
# Premiere
|
||||
".prproj",
|
||||
# Resolve
|
||||
".drp",
|
||||
# Photoshop
|
||||
".psd", ".psb",
|
||||
# Aftereffects
|
||||
".aep"
|
||||
]
|
||||
|
||||
def get_icon(self):
|
||||
return "fa.file"
|
||||
|
||||
def collect_instances(self):
|
||||
for instance_data in pipeline.list_instances():
|
||||
creator_id = instance_data.get("creator_identifier")
|
||||
if creator_id == self.identifier:
|
||||
instance = CreatedInstance.from_existing(
|
||||
instance_data, self
|
||||
)
|
||||
self._add_instance_to_context(instance)
|
||||
|
||||
def update_instances(self, update_list):
|
||||
pipeline.update_instances(update_list)
|
||||
|
||||
def remove_instances(self, instances):
|
||||
pipeline.remove_instances(instances)
|
||||
for instance in instances:
|
||||
self._remove_instance_from_context(instance)
|
||||
|
||||
def create(self, subset_name, data, pre_create_data):
|
||||
# Pass precreate data to creator attributes
|
||||
data["creator_attributes"] = pre_create_data
|
||||
# Create new instance
|
||||
new_instance = CreatedInstance(self.family, subset_name, data, self)
|
||||
# Host implementation of storing metadata about instance
|
||||
pipeline.HostContext.add_instance(new_instance.data_to_store())
|
||||
# Add instance to current context
|
||||
self._add_instance_to_context(new_instance)
|
||||
|
||||
def get_default_variants(self):
|
||||
return [
|
||||
"Main"
|
||||
]
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
output = [
|
||||
lib.FileDef(
|
||||
"filepath",
|
||||
folders=False,
|
||||
extensions=self.extensions,
|
||||
label="Filepath"
|
||||
)
|
||||
]
|
||||
return output
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
# Use same attributes as for instance attrobites
|
||||
return self.get_instance_attr_defs()
|
||||
|
||||
def get_detail_description(self):
|
||||
return """# Publish workfile backup"""
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import pyblish.api
|
||||
|
||||
|
||||
class CollectSource(pyblish.api.ContextPlugin):
|
||||
"""Collecting instances from traypublisher host."""
|
||||
|
||||
label = "Collect source"
|
||||
order = pyblish.api.CollectorOrder - 0.49
|
||||
hosts = ["traypublisher"]
|
||||
|
||||
def process(self, context):
|
||||
# get json paths from os and load them
|
||||
source_name = "traypublisher"
|
||||
for instance in context:
|
||||
source = instance.data.get("source")
|
||||
if not source:
|
||||
instance.data["source"] = source_name
|
||||
self.log.info((
|
||||
"Source of instance \"{}\" is changed to \"{}\""
|
||||
).format(instance.data["name"], source_name))
|
||||
else:
|
||||
self.log.info((
|
||||
"Source of instance \"{}\" was already set to \"{}\""
|
||||
).format(instance.data["name"], source))
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectWorkfile(pyblish.api.InstancePlugin):
|
||||
"""Collect representation of workfile instances."""
|
||||
|
||||
label = "Collect Workfile"
|
||||
order = pyblish.api.CollectorOrder - 0.49
|
||||
families = ["workfile"]
|
||||
hosts = ["traypublisher"]
|
||||
|
||||
def process(self, instance):
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
repres = instance.data["representations"]
|
||||
|
||||
creator_attributes = instance.data["creator_attributes"]
|
||||
filepath = creator_attributes["filepath"]
|
||||
instance.data["sourceFilepath"] = filepath
|
||||
|
||||
staging_dir = os.path.dirname(filepath)
|
||||
filename = os.path.basename(filepath)
|
||||
ext = os.path.splitext(filename)[-1]
|
||||
|
||||
repres.append({
|
||||
"ext": ext,
|
||||
"name": ext,
|
||||
"stagingDir": staging_dir,
|
||||
"files": filename
|
||||
})
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
|
||||
class ValidateWorkfilePath(pyblish.api.InstancePlugin):
|
||||
"""Validate existence of workfile instance existence."""
|
||||
|
||||
label = "Collect Workfile"
|
||||
order = pyblish.api.ValidatorOrder - 0.49
|
||||
families = ["workfile"]
|
||||
hosts = ["traypublisher"]
|
||||
|
||||
def process(self, instance):
|
||||
filepath = instance.data["sourceFilepath"]
|
||||
if not filepath:
|
||||
raise PublishValidationError((
|
||||
"Filepath of 'workfile' instance \"{}\" is not set"
|
||||
).format(instance.data["name"]))
|
||||
|
||||
if not os.path.exists(filepath):
|
||||
raise PublishValidationError((
|
||||
"Filepath of 'workfile' instance \"{}\" does not exist: {}"
|
||||
).format(instance.data["name"], filepath))
|
||||
|
|
@ -29,6 +29,7 @@ from .execute import (
|
|||
get_linux_launcher_args,
|
||||
execute,
|
||||
run_subprocess,
|
||||
run_detached_process,
|
||||
run_openpype_process,
|
||||
clean_envs_for_openpype_process,
|
||||
path_to_subprocess_arg,
|
||||
|
|
@ -188,6 +189,7 @@ __all__ = [
|
|||
"get_linux_launcher_args",
|
||||
"execute",
|
||||
"run_subprocess",
|
||||
"run_detached_process",
|
||||
"run_openpype_process",
|
||||
"clean_envs_for_openpype_process",
|
||||
"path_to_subprocess_arg",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import platform
|
||||
import json
|
||||
import tempfile
|
||||
import distutils.spawn
|
||||
|
||||
from .log import PypeLogger as Logger
|
||||
|
|
@ -181,6 +185,80 @@ def run_openpype_process(*args, **kwargs):
|
|||
return run_subprocess(args, env=env, **kwargs)
|
||||
|
||||
|
||||
def run_detached_process(args, **kwargs):
|
||||
"""Execute process with passed arguments as separated process.
|
||||
|
||||
Values from 'os.environ' are used for environments if are not passed.
|
||||
They are cleaned using 'clean_envs_for_openpype_process' function.
|
||||
|
||||
Example:
|
||||
```
|
||||
run_detached_openpype_process("run", "<path to .py script>")
|
||||
```
|
||||
|
||||
Args:
|
||||
*args (tuple): OpenPype cli arguments.
|
||||
**kwargs (dict): Keyword arguments for for subprocess.Popen.
|
||||
|
||||
Returns:
|
||||
subprocess.Popen: Pointer to launched process but it is possible that
|
||||
launched process is already killed (on linux).
|
||||
"""
|
||||
env = kwargs.pop("env", None)
|
||||
# Keep env untouched if are passed and not empty
|
||||
if not env:
|
||||
env = os.environ
|
||||
|
||||
# Create copy of passed env
|
||||
kwargs["env"] = {k: v for k, v in env.items()}
|
||||
|
||||
low_platform = platform.system().lower()
|
||||
if low_platform == "darwin":
|
||||
new_args = ["open", "-na", args.pop(0), "--args"]
|
||||
new_args.extend(args)
|
||||
args = new_args
|
||||
|
||||
elif low_platform == "windows":
|
||||
flags = (
|
||||
subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
| subprocess.DETACHED_PROCESS
|
||||
)
|
||||
kwargs["creationflags"] = flags
|
||||
|
||||
if not sys.stdout:
|
||||
kwargs["stdout"] = subprocess.DEVNULL
|
||||
kwargs["stderr"] = subprocess.DEVNULL
|
||||
|
||||
elif low_platform == "linux" and get_linux_launcher_args() is not None:
|
||||
json_data = {
|
||||
"args": args,
|
||||
"env": kwargs.pop("env")
|
||||
}
|
||||
json_temp = tempfile.NamedTemporaryFile(
|
||||
mode="w", prefix="op_app_args", suffix=".json", delete=False
|
||||
)
|
||||
json_temp.close()
|
||||
json_temp_filpath = json_temp.name
|
||||
with open(json_temp_filpath, "w") as stream:
|
||||
json.dump(json_data, stream)
|
||||
|
||||
new_args = get_linux_launcher_args()
|
||||
new_args.append(json_temp_filpath)
|
||||
|
||||
# Create mid-process which will launch application
|
||||
process = subprocess.Popen(new_args, **kwargs)
|
||||
# Wait until the process finishes
|
||||
# - This is important! The process would stay in "open" state.
|
||||
process.wait()
|
||||
# Remove the temp file
|
||||
os.remove(json_temp_filpath)
|
||||
# Return process which is already terminated
|
||||
return process
|
||||
|
||||
process = subprocess.Popen(args, **kwargs)
|
||||
return process
|
||||
|
||||
|
||||
def path_to_subprocess_arg(path):
|
||||
"""Prepare path for subprocess arguments.
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ DEFAULT_OPENPYPE_MODULES = (
|
|||
"project_manager_action",
|
||||
"settings_action",
|
||||
"standalonepublish_action",
|
||||
"traypublish_action",
|
||||
"job_queue",
|
||||
"timers_manager",
|
||||
"sync_server",
|
||||
|
|
@ -846,6 +847,7 @@ class TrayModulesManager(ModulesManager):
|
|||
"avalon",
|
||||
"clockify",
|
||||
"standalonepublish_tool",
|
||||
"traypublish_tool",
|
||||
"log_viewer",
|
||||
"local_settings",
|
||||
"settings"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import os
|
||||
import logging
|
||||
import pyblish.api
|
||||
import avalon.api
|
||||
|
|
@ -43,37 +42,48 @@ class CollectFtrackApi(pyblish.api.ContextPlugin):
|
|||
).format(project_name))
|
||||
|
||||
project_entity = project_entities[0]
|
||||
|
||||
self.log.debug("Project found: {0}".format(project_entity))
|
||||
|
||||
# Find asset entity
|
||||
entity_query = (
|
||||
'TypedContext where project_id is "{0}"'
|
||||
' and name is "{1}"'
|
||||
).format(project_entity["id"], asset_name)
|
||||
self.log.debug("Asset entity query: < {0} >".format(entity_query))
|
||||
asset_entities = []
|
||||
for entity in session.query(entity_query).all():
|
||||
# Skip tasks
|
||||
if entity.entity_type.lower() != "task":
|
||||
asset_entities.append(entity)
|
||||
asset_entity = None
|
||||
if asset_name:
|
||||
# Find asset entity
|
||||
entity_query = (
|
||||
'TypedContext where project_id is "{0}"'
|
||||
' and name is "{1}"'
|
||||
).format(project_entity["id"], asset_name)
|
||||
self.log.debug("Asset entity query: < {0} >".format(entity_query))
|
||||
asset_entities = []
|
||||
for entity in session.query(entity_query).all():
|
||||
# Skip tasks
|
||||
if entity.entity_type.lower() != "task":
|
||||
asset_entities.append(entity)
|
||||
|
||||
if len(asset_entities) == 0:
|
||||
raise AssertionError((
|
||||
"Entity with name \"{0}\" not found"
|
||||
" in Ftrack project \"{1}\"."
|
||||
).format(asset_name, project_name))
|
||||
if len(asset_entities) == 0:
|
||||
raise AssertionError((
|
||||
"Entity with name \"{0}\" not found"
|
||||
" in Ftrack project \"{1}\"."
|
||||
).format(asset_name, project_name))
|
||||
|
||||
elif len(asset_entities) > 1:
|
||||
raise AssertionError((
|
||||
"Found more than one entity with name \"{0}\""
|
||||
" in Ftrack project \"{1}\"."
|
||||
).format(asset_name, project_name))
|
||||
elif len(asset_entities) > 1:
|
||||
raise AssertionError((
|
||||
"Found more than one entity with name \"{0}\""
|
||||
" in Ftrack project \"{1}\"."
|
||||
).format(asset_name, project_name))
|
||||
|
||||
asset_entity = asset_entities[0]
|
||||
|
||||
asset_entity = asset_entities[0]
|
||||
self.log.debug("Asset found: {0}".format(asset_entity))
|
||||
|
||||
task_entity = None
|
||||
# Find task entity if task is set
|
||||
if task_name:
|
||||
if not asset_entity:
|
||||
self.log.warning(
|
||||
"Asset entity is not set. Skipping query of task entity."
|
||||
)
|
||||
elif not task_name:
|
||||
self.log.warning("Task name is not set.")
|
||||
else:
|
||||
task_query = (
|
||||
'Task where name is "{0}" and parent_id is "{1}"'
|
||||
).format(task_name, asset_entity["id"])
|
||||
|
|
@ -88,10 +98,6 @@ class CollectFtrackApi(pyblish.api.ContextPlugin):
|
|||
else:
|
||||
self.log.debug("Task entity found: {0}".format(task_entity))
|
||||
|
||||
else:
|
||||
task_entity = None
|
||||
self.log.warning("Task name is not set.")
|
||||
|
||||
context.data["ftrackSession"] = session
|
||||
context.data["ftrackPythonModule"] = ftrack_api
|
||||
context.data["ftrackProject"] = project_entity
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ class ITrayAction(ITrayModule):
|
|||
|
||||
admin_action = False
|
||||
_admin_submenu = None
|
||||
_action_item = None
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
|
|
@ -149,6 +150,7 @@ class ITrayAction(ITrayModule):
|
|||
tray_menu.addAction(action)
|
||||
|
||||
action.triggered.connect(self.on_action_trigger)
|
||||
self._action_item = action
|
||||
|
||||
def tray_start(self):
|
||||
return
|
||||
|
|
|
|||
49
openpype/modules/traypublish_action.py
Normal file
49
openpype/modules/traypublish_action.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import os
|
||||
from openpype.lib import get_openpype_execute_args
|
||||
from openpype.lib.execute import run_detached_process
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import ITrayAction
|
||||
|
||||
|
||||
class TrayPublishAction(OpenPypeModule, ITrayAction):
|
||||
label = "New Publish (beta)"
|
||||
name = "traypublish_tool"
|
||||
|
||||
def initialize(self, modules_settings):
|
||||
import openpype
|
||||
self.enabled = True
|
||||
self.publish_paths = [
|
||||
os.path.join(
|
||||
openpype.PACKAGE_DIR,
|
||||
"hosts",
|
||||
"traypublisher",
|
||||
"plugins",
|
||||
"publish"
|
||||
)
|
||||
]
|
||||
self._experimental_tools = None
|
||||
|
||||
def tray_init(self):
|
||||
from openpype.tools.experimental_tools import ExperimentalTools
|
||||
|
||||
self._experimental_tools = ExperimentalTools()
|
||||
|
||||
def tray_menu(self, *args, **kwargs):
|
||||
super(TrayPublishAction, self).tray_menu(*args, **kwargs)
|
||||
traypublisher = self._experimental_tools.get("traypublisher")
|
||||
visible = False
|
||||
if traypublisher and traypublisher.enabled:
|
||||
visible = True
|
||||
self._action_item.setVisible(visible)
|
||||
|
||||
def on_action_trigger(self):
|
||||
self.run_traypublisher()
|
||||
|
||||
def connect_with_modules(self, enabled_modules):
|
||||
"""Collect publish paths from other modules."""
|
||||
publish_paths = self.manager.collect_plugin_paths()["publish"]
|
||||
self.publish_paths.extend(publish_paths)
|
||||
|
||||
def run_traypublisher(self):
|
||||
args = get_openpype_execute_args("traypublisher")
|
||||
run_detached_process(args)
|
||||
|
|
@ -14,7 +14,7 @@ Except creating and removing instances are all changes not automatically propaga
|
|||
|
||||
|
||||
## CreatedInstance
|
||||
Product of creation is "instance" which holds basic data defying it. Core data are `creator_identifier`, `family` and `subset`. Other data can be keys used to fill subset name or metadata modifying publishing process of the instance (more described later). All instances have `id` which holds constant `pyblish.avalon.instance` and `uuid` which is identifier of the instance.
|
||||
Product of creation is "instance" which holds basic data defying it. Core data are `creator_identifier`, `family` and `subset`. Other data can be keys used to fill subset name or metadata modifying publishing process of the instance (more described later). All instances have `id` which holds constant `pyblish.avalon.instance` and `instance_id` which is identifier of the instance.
|
||||
Family tells how should be instance processed and subset what name will published item have.
|
||||
- There are cases when subset is not fully filled during creation and may change during publishing. That is in most of cases caused because instance is related to other instance or instance data do not represent final product.
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ Family tells how should be instance processed and subset what name will publishe
|
|||
## Identifier that this data represents instance for publishing (automatically assigned)
|
||||
"id": "pyblish.avalon.instance",
|
||||
## Identifier of this specific instance (automatically assigned)
|
||||
"uuid": <uuid4>,
|
||||
"instance_id": <uuid4>,
|
||||
## Instance family (used from Creator)
|
||||
"family": <family>,
|
||||
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ class CreatedInstance:
|
|||
# their individual children but not on their own
|
||||
__immutable_keys = (
|
||||
"id",
|
||||
"uuid",
|
||||
"instance_id",
|
||||
"family",
|
||||
"creator_identifier",
|
||||
"creator_attributes",
|
||||
|
|
@ -434,8 +434,8 @@ class CreatedInstance:
|
|||
if data:
|
||||
self._data.update(data)
|
||||
|
||||
if not self._data.get("uuid"):
|
||||
self._data["uuid"] = str(uuid4())
|
||||
if not self._data.get("instance_id"):
|
||||
self._data["instance_id"] = str(uuid4())
|
||||
|
||||
self._asset_is_valid = self.has_set_asset
|
||||
self._task_is_valid = self.has_set_task
|
||||
|
|
@ -551,7 +551,7 @@ class CreatedInstance:
|
|||
@property
|
||||
def id(self):
|
||||
"""Instance identifier."""
|
||||
return self._data["uuid"]
|
||||
return self._data["instance_id"]
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
|
|
|
|||
|
|
@ -44,42 +44,18 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin):
|
|||
label = "Collect Anatomy Context Data"
|
||||
|
||||
def process(self, context):
|
||||
|
||||
task_name = api.Session["AVALON_TASK"]
|
||||
|
||||
project_entity = context.data["projectEntity"]
|
||||
asset_entity = context.data["assetEntity"]
|
||||
|
||||
asset_tasks = asset_entity["data"]["tasks"]
|
||||
task_type = asset_tasks.get(task_name, {}).get("type")
|
||||
|
||||
project_task_types = project_entity["config"]["tasks"]
|
||||
task_code = project_task_types.get(task_type, {}).get("short_name")
|
||||
|
||||
asset_parents = asset_entity["data"]["parents"]
|
||||
hierarchy = "/".join(asset_parents)
|
||||
|
||||
parent_name = project_entity["name"]
|
||||
if asset_parents:
|
||||
parent_name = asset_parents[-1]
|
||||
|
||||
context_data = {
|
||||
"project": {
|
||||
"name": project_entity["name"],
|
||||
"code": project_entity["data"].get("code")
|
||||
},
|
||||
"asset": asset_entity["name"],
|
||||
"parent": parent_name,
|
||||
"hierarchy": hierarchy,
|
||||
"task": {
|
||||
"name": task_name,
|
||||
"type": task_type,
|
||||
"short": task_code,
|
||||
},
|
||||
"username": context.data["user"],
|
||||
"app": context.data["hostName"]
|
||||
}
|
||||
|
||||
context.data["anatomyData"] = context_data
|
||||
|
||||
# add system general settings anatomy data
|
||||
system_general_data = get_system_general_anatomy_data()
|
||||
context_data.update(system_general_data)
|
||||
|
|
@ -87,7 +63,33 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin):
|
|||
datetime_data = context.data.get("datetimeData") or {}
|
||||
context_data.update(datetime_data)
|
||||
|
||||
context.data["anatomyData"] = context_data
|
||||
asset_entity = context.data.get("assetEntity")
|
||||
if asset_entity:
|
||||
task_name = api.Session["AVALON_TASK"]
|
||||
|
||||
asset_tasks = asset_entity["data"]["tasks"]
|
||||
task_type = asset_tasks.get(task_name, {}).get("type")
|
||||
|
||||
project_task_types = project_entity["config"]["tasks"]
|
||||
task_code = project_task_types.get(task_type, {}).get("short_name")
|
||||
|
||||
asset_parents = asset_entity["data"]["parents"]
|
||||
hierarchy = "/".join(asset_parents)
|
||||
|
||||
parent_name = project_entity["name"]
|
||||
if asset_parents:
|
||||
parent_name = asset_parents[-1]
|
||||
|
||||
context_data.update({
|
||||
"asset": asset_entity["name"],
|
||||
"parent": parent_name,
|
||||
"hierarchy": hierarchy,
|
||||
"task": {
|
||||
"name": task_name,
|
||||
"type": task_type,
|
||||
"short": task_code,
|
||||
}
|
||||
})
|
||||
|
||||
self.log.info("Global anatomy Data collected")
|
||||
self.log.debug(json.dumps(context_data, indent=4))
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
def fill_missing_asset_docs(self, context):
|
||||
self.log.debug("Qeurying asset documents for instances.")
|
||||
|
||||
context_asset_doc = context.data["assetEntity"]
|
||||
context_asset_doc = context.data.get("assetEntity")
|
||||
|
||||
instances_with_missing_asset_doc = collections.defaultdict(list)
|
||||
for instance in context:
|
||||
|
|
@ -69,7 +69,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
|
||||
# Check if asset name is the same as what is in context
|
||||
# - they may be different, e.g. in NukeStudio
|
||||
if context_asset_doc["name"] == _asset_name:
|
||||
if context_asset_doc and context_asset_doc["name"] == _asset_name:
|
||||
instance.data["assetEntity"] = context_asset_doc
|
||||
|
||||
else:
|
||||
|
|
@ -212,7 +212,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
self.log.debug("Storing anatomy data to instance data.")
|
||||
|
||||
project_doc = context.data["projectEntity"]
|
||||
context_asset_doc = context.data["assetEntity"]
|
||||
context_asset_doc = context.data.get("assetEntity")
|
||||
|
||||
project_task_types = project_doc["config"]["tasks"]
|
||||
|
||||
|
|
@ -240,7 +240,13 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
|
||||
# Hiearchy
|
||||
asset_doc = instance.data.get("assetEntity")
|
||||
if asset_doc and asset_doc["_id"] != context_asset_doc["_id"]:
|
||||
if (
|
||||
asset_doc
|
||||
and (
|
||||
not context_asset_doc
|
||||
or asset_doc["_id"] != context_asset_doc["_id"]
|
||||
)
|
||||
):
|
||||
parents = asset_doc["data"].get("parents") or list()
|
||||
parent_name = project_doc["name"]
|
||||
if parents:
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin):
|
|||
).format(project_name)
|
||||
self.log.debug("Collected Project \"{}\"".format(project_entity))
|
||||
|
||||
context.data["projectEntity"] = project_entity
|
||||
|
||||
if not asset_name:
|
||||
self.log.info("Context is not set. Can't collect global data.")
|
||||
return
|
||||
asset_entity = io.find_one({
|
||||
"type": "asset",
|
||||
"name": asset_name,
|
||||
|
|
@ -44,7 +49,6 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin):
|
|||
|
||||
self.log.debug("Collected Asset \"{}\"".format(asset_entity))
|
||||
|
||||
context.data["projectEntity"] = project_entity
|
||||
context.data["assetEntity"] = asset_entity
|
||||
|
||||
data = asset_entity['data']
|
||||
|
|
|
|||
|
|
@ -148,7 +148,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
|
||||
project_entity = instance.data["projectEntity"]
|
||||
|
||||
context_asset_name = context.data["assetEntity"]["name"]
|
||||
context_asset_name = None
|
||||
context_asset_doc = context.data.get("assetEntity")
|
||||
if context_asset_doc:
|
||||
context_asset_name = context_asset_doc["name"]
|
||||
|
||||
asset_name = instance.data["asset"]
|
||||
asset_entity = instance.data.get("assetEntity")
|
||||
|
|
|
|||
31
openpype/plugins/publish/validate_aseset_docs.py
Normal file
31
openpype/plugins/publish/validate_aseset_docs.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import pyblish.api
|
||||
from openpype.pipeline import PublishValidationError
|
||||
|
||||
|
||||
class ValidateContainers(pyblish.api.InstancePlugin):
|
||||
"""Validate existence of asset asset documents on instances.
|
||||
|
||||
Without asset document it is not possible to publish the instance.
|
||||
|
||||
If context has set asset document the validation is skipped.
|
||||
|
||||
Plugin was added because there are cases when context asset is not defined
|
||||
e.g. in tray publisher.
|
||||
"""
|
||||
|
||||
label = "Validate Asset docs"
|
||||
order = pyblish.api.ValidatorOrder
|
||||
|
||||
def process(self, instance):
|
||||
context_asset_doc = instance.context.data.get("assetEntity")
|
||||
if context_asset_doc:
|
||||
return
|
||||
|
||||
if instance.data.get("assetEntity"):
|
||||
self.log.info("Instance have set asset document in it's data.")
|
||||
|
||||
else:
|
||||
raise PublishValidationError((
|
||||
"Instance \"{}\" don't have set asset"
|
||||
" document which is needed for publishing."
|
||||
).format(instance.data["name"]))
|
||||
|
|
@ -80,6 +80,11 @@ class PypeCommands:
|
|||
from openpype.tools import standalonepublish
|
||||
standalonepublish.main()
|
||||
|
||||
@staticmethod
|
||||
def launch_traypublisher():
|
||||
from openpype.tools import traypublisher
|
||||
traypublisher.main()
|
||||
|
||||
@staticmethod
|
||||
def publish(paths, targets=None, gui=False):
|
||||
"""Start headless publishing.
|
||||
|
|
|
|||
|
|
@ -1261,6 +1261,12 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
|||
background: {color:restart-btn-bg};
|
||||
}
|
||||
|
||||
/* Tray publisher */
|
||||
#ChooseProjectLabel {
|
||||
font-size: 15pt;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
/* Globally used names */
|
||||
#Separator {
|
||||
background: {color:bg-menu-separator};
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ class ExperimentalToolsDialog(QtWidgets.QDialog):
|
|||
tool_btns_layout.addWidget(tool_btns_label, 0)
|
||||
|
||||
experimental_tools = ExperimentalTools(
|
||||
parent=parent, filter_hosts=True
|
||||
parent_widget=parent, refresh=False
|
||||
)
|
||||
|
||||
# Main layout
|
||||
|
|
@ -116,7 +116,8 @@ class ExperimentalToolsDialog(QtWidgets.QDialog):
|
|||
self._experimental_tools.refresh_availability()
|
||||
|
||||
buttons_to_remove = set(self._buttons_by_tool_identifier.keys())
|
||||
for idx, tool in enumerate(self._experimental_tools.tools):
|
||||
tools = self._experimental_tools.get_tools_for_host()
|
||||
for idx, tool in enumerate(tools):
|
||||
identifier = tool.identifier
|
||||
if identifier in buttons_to_remove:
|
||||
buttons_to_remove.remove(identifier)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,32 @@ from openpype.settings import get_local_settings
|
|||
LOCAL_EXPERIMENTAL_KEY = "experimental_tools"
|
||||
|
||||
|
||||
class ExperimentalTool:
|
||||
class ExperimentalTool(object):
|
||||
"""Definition of experimental tool.
|
||||
|
||||
Definition is used in local settings.
|
||||
|
||||
Args:
|
||||
identifier (str): String identifier of tool (unique).
|
||||
label (str): Label shown in UI.
|
||||
"""
|
||||
def __init__(self, identifier, label, tooltip):
|
||||
self.identifier = identifier
|
||||
self.label = label
|
||||
self.tooltip = tooltip
|
||||
self._enabled = True
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
"""Is tool enabled and button is clickable."""
|
||||
return self._enabled
|
||||
|
||||
def set_enabled(self, enabled=True):
|
||||
"""Change if tool is enabled."""
|
||||
self._enabled = enabled
|
||||
|
||||
|
||||
class ExperimentalHostTool(ExperimentalTool):
|
||||
"""Definition of experimental tool.
|
||||
|
||||
Definition is used in local settings and in experimental tools dialog.
|
||||
|
|
@ -19,12 +44,10 @@ class ExperimentalTool:
|
|||
Some tools may not be available in all hosts.
|
||||
"""
|
||||
def __init__(
|
||||
self, identifier, label, callback, tooltip, hosts_filter=None
|
||||
self, identifier, label, tooltip, callback, hosts_filter=None
|
||||
):
|
||||
self.identifier = identifier
|
||||
self.label = label
|
||||
super(ExperimentalHostTool, self).__init__(identifier, label, tooltip)
|
||||
self.callback = callback
|
||||
self.tooltip = tooltip
|
||||
self.hosts_filter = hosts_filter
|
||||
self._enabled = True
|
||||
|
||||
|
|
@ -33,18 +56,9 @@ class ExperimentalTool:
|
|||
return host_name in self.hosts_filter
|
||||
return True
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
"""Is tool enabled and button is clickable."""
|
||||
return self._enabled
|
||||
|
||||
def set_enabled(self, enabled=True):
|
||||
"""Change if tool is enabled."""
|
||||
self._enabled = enabled
|
||||
|
||||
def execute(self):
|
||||
def execute(self, *args, **kwargs):
|
||||
"""Trigger registered callback."""
|
||||
self.callback()
|
||||
self.callback(*args, **kwargs)
|
||||
|
||||
|
||||
class ExperimentalTools:
|
||||
|
|
@ -53,57 +67,36 @@ class ExperimentalTools:
|
|||
To add/remove experimental tool just add/remove tool to
|
||||
`experimental_tools` variable in __init__ function.
|
||||
|
||||
Args:
|
||||
parent (QtWidgets.QWidget): Parent widget for tools.
|
||||
host_name (str): Name of host in which context we're now. Environment
|
||||
value 'AVALON_APP' is used when not passed.
|
||||
filter_hosts (bool): Should filter tools. By default is set to 'True'
|
||||
when 'host_name' is passed. Is always set to 'False' if 'host_name'
|
||||
is not defined.
|
||||
--- Example tool (callback will just print on click) ---
|
||||
def example_callback(*args):
|
||||
print("Triggered tool")
|
||||
|
||||
experimental_tools = [
|
||||
ExperimentalHostTool(
|
||||
"example",
|
||||
"Example experimental tool",
|
||||
example_callback,
|
||||
"Example tool tooltip."
|
||||
)
|
||||
]
|
||||
---
|
||||
"""
|
||||
def __init__(self, parent=None, host_name=None, filter_hosts=None):
|
||||
def __init__(self, parent_widget=None, refresh=True):
|
||||
# Definition of experimental tools
|
||||
experimental_tools = [
|
||||
ExperimentalTool(
|
||||
ExperimentalHostTool(
|
||||
"publisher",
|
||||
"New publisher",
|
||||
self._show_publisher,
|
||||
"Combined creation and publishing into one tool."
|
||||
"Combined creation and publishing into one tool.",
|
||||
self._show_publisher
|
||||
),
|
||||
ExperimentalTool(
|
||||
"traypublisher",
|
||||
"New Standalone Publisher",
|
||||
"Standalone publisher using new publisher. Requires restart"
|
||||
)
|
||||
]
|
||||
|
||||
# --- Example tool (callback will just print on click) ---
|
||||
# def example_callback(*args):
|
||||
# print("Triggered tool")
|
||||
#
|
||||
# experimental_tools = [
|
||||
# ExperimentalTool(
|
||||
# "example",
|
||||
# "Example experimental tool",
|
||||
# example_callback,
|
||||
# "Example tool tooltip."
|
||||
# )
|
||||
# ]
|
||||
|
||||
# Try to get host name from env variable `AVALON_APP`
|
||||
if not host_name:
|
||||
host_name = os.environ.get("AVALON_APP")
|
||||
|
||||
# Decide if filtering by host name should happen
|
||||
if filter_hosts is None:
|
||||
filter_hosts = host_name is not None
|
||||
|
||||
if filter_hosts and not host_name:
|
||||
filter_hosts = False
|
||||
|
||||
# Filter tools by host name
|
||||
if filter_hosts:
|
||||
experimental_tools = [
|
||||
tool
|
||||
for tool in experimental_tools
|
||||
if tool.is_available_for_host(host_name)
|
||||
]
|
||||
|
||||
# Store tools by identifier
|
||||
tools_by_identifier = {}
|
||||
for tool in experimental_tools:
|
||||
|
|
@ -115,10 +108,13 @@ class ExperimentalTools:
|
|||
|
||||
self._tools_by_identifier = tools_by_identifier
|
||||
self._tools = experimental_tools
|
||||
self._parent_widget = parent
|
||||
|
||||
self._parent_widget = parent_widget
|
||||
self._publisher_tool = None
|
||||
|
||||
if refresh:
|
||||
self.refresh_availability()
|
||||
|
||||
@property
|
||||
def tools(self):
|
||||
"""Tools in list.
|
||||
|
|
@ -139,6 +135,22 @@ class ExperimentalTools:
|
|||
"""
|
||||
return self._tools_by_identifier
|
||||
|
||||
def get(self, tool_identifier):
|
||||
"""Get tool by identifier."""
|
||||
return self.tools_by_identifier.get(tool_identifier)
|
||||
|
||||
def get_tools_for_host(self, host_name=None):
|
||||
if not host_name:
|
||||
host_name = os.environ.get("AVALON_APP")
|
||||
tools = []
|
||||
for tool in self.tools:
|
||||
if (
|
||||
isinstance(tool, ExperimentalHostTool)
|
||||
and tool.is_available_for_host(host_name)
|
||||
):
|
||||
tools.append(tool)
|
||||
return tools
|
||||
|
||||
def refresh_availability(self):
|
||||
"""Reload local settings and check if any tool changed ability."""
|
||||
local_settings = get_local_settings()
|
||||
|
|
|
|||
|
|
@ -42,18 +42,23 @@ class MainThreadProcess(QtCore.QObject):
|
|||
This approach gives ability to update UI meanwhile plugin is in progress.
|
||||
"""
|
||||
|
||||
timer_interval = 3
|
||||
count_timeout = 2
|
||||
|
||||
def __init__(self):
|
||||
super(MainThreadProcess, self).__init__()
|
||||
self._items_to_process = collections.deque()
|
||||
|
||||
timer = QtCore.QTimer()
|
||||
timer.setInterval(self.timer_interval)
|
||||
timer.setInterval(0)
|
||||
|
||||
timer.timeout.connect(self._execute)
|
||||
|
||||
self._timer = timer
|
||||
self._switch_counter = self.count_timeout
|
||||
|
||||
def process(self, func, *args, **kwargs):
|
||||
item = MainThreadItem(func, *args, **kwargs)
|
||||
self.add_item(item)
|
||||
|
||||
def add_item(self, item):
|
||||
self._items_to_process.append(item)
|
||||
|
|
@ -62,6 +67,12 @@ class MainThreadProcess(QtCore.QObject):
|
|||
if not self._items_to_process:
|
||||
return
|
||||
|
||||
if self._switch_counter > 0:
|
||||
self._switch_counter -= 1
|
||||
return
|
||||
|
||||
self._switch_counter = self.count_timeout
|
||||
|
||||
item = self._items_to_process.popleft()
|
||||
item.process()
|
||||
|
||||
|
|
@ -173,11 +184,21 @@ class PublishReport:
|
|||
|
||||
self._stored_plugins.append(plugin)
|
||||
|
||||
plugin_data_item = self._create_plugin_data_item(plugin)
|
||||
|
||||
self._plugin_data_with_plugin.append({
|
||||
"plugin": plugin,
|
||||
"data": plugin_data_item
|
||||
})
|
||||
self._plugin_data.append(plugin_data_item)
|
||||
return plugin_data_item
|
||||
|
||||
def _create_plugin_data_item(self, plugin):
|
||||
label = None
|
||||
if hasattr(plugin, "label"):
|
||||
label = plugin.label
|
||||
|
||||
plugin_data_item = {
|
||||
return {
|
||||
"name": plugin.__name__,
|
||||
"label": label,
|
||||
"order": plugin.order,
|
||||
|
|
@ -186,12 +207,6 @@ class PublishReport:
|
|||
"skipped": False,
|
||||
"passed": False
|
||||
}
|
||||
self._plugin_data_with_plugin.append({
|
||||
"plugin": plugin,
|
||||
"data": plugin_data_item
|
||||
})
|
||||
self._plugin_data.append(plugin_data_item)
|
||||
return plugin_data_item
|
||||
|
||||
def set_plugin_skipped(self):
|
||||
"""Set that current plugin has been skipped."""
|
||||
|
|
@ -241,7 +256,7 @@ class PublishReport:
|
|||
if publish_plugins:
|
||||
for plugin in publish_plugins:
|
||||
if plugin not in self._stored_plugins:
|
||||
plugins_data.append(self._add_plugin_data_item(plugin))
|
||||
plugins_data.append(self._create_plugin_data_item(plugin))
|
||||
|
||||
crashed_file_paths = {}
|
||||
if self._publish_discover_result is not None:
|
||||
|
|
@ -971,6 +986,9 @@ class PublisherController:
|
|||
|
||||
self._publish_next_process()
|
||||
|
||||
def reset_project_data_cache(self):
|
||||
self._asset_docs_cache.reset()
|
||||
|
||||
|
||||
def collect_families_from_instances(instances, only_active=False):
|
||||
"""Collect all families for passed publish instances.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
from .report_items import (
|
||||
PublishReport
|
||||
)
|
||||
from .widgets import (
|
||||
PublishReportViewerWidget
|
||||
)
|
||||
|
|
@ -8,6 +11,8 @@ from .window import (
|
|||
|
||||
|
||||
__all__ = (
|
||||
"PublishReport",
|
||||
|
||||
"PublishReportViewerWidget",
|
||||
|
||||
"PublishReportViewerWindow",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ class InstancesModel(QtGui.QStandardItemModel):
|
|||
self.clear()
|
||||
self._items_by_id.clear()
|
||||
self._plugin_items_by_id.clear()
|
||||
if not report_item:
|
||||
return
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
|
||||
|
|
@ -119,6 +121,8 @@ class PluginsModel(QtGui.QStandardItemModel):
|
|||
self.clear()
|
||||
self._items_by_id.clear()
|
||||
self._plugin_items_by_id.clear()
|
||||
if not report_item:
|
||||
return
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
|
||||
|
|
|
|||
126
openpype/tools/publisher/publish_report_viewer/report_items.py
Normal file
126
openpype/tools/publisher/publish_report_viewer/report_items.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import uuid
|
||||
import collections
|
||||
import copy
|
||||
|
||||
|
||||
class PluginItem:
|
||||
def __init__(self, plugin_data):
|
||||
self._id = uuid.uuid4()
|
||||
|
||||
self.name = plugin_data["name"]
|
||||
self.label = plugin_data["label"]
|
||||
self.order = plugin_data["order"]
|
||||
self.skipped = plugin_data["skipped"]
|
||||
self.passed = plugin_data["passed"]
|
||||
|
||||
errored = False
|
||||
for instance_data in plugin_data["instances_data"]:
|
||||
for log_item in instance_data["logs"]:
|
||||
errored = log_item["type"] == "error"
|
||||
if errored:
|
||||
break
|
||||
if errored:
|
||||
break
|
||||
|
||||
self.errored = errored
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
|
||||
class InstanceItem:
|
||||
def __init__(self, instance_id, instance_data, logs_by_instance_id):
|
||||
self._id = instance_id
|
||||
self.label = instance_data.get("label") or instance_data.get("name")
|
||||
self.family = instance_data.get("family")
|
||||
self.removed = not instance_data.get("exists", True)
|
||||
|
||||
logs = logs_by_instance_id.get(instance_id) or []
|
||||
errored = False
|
||||
for log_item in logs:
|
||||
if log_item.errored:
|
||||
errored = True
|
||||
break
|
||||
|
||||
self.errored = errored
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
|
||||
class LogItem:
|
||||
def __init__(self, log_item_data, plugin_id, instance_id):
|
||||
self._instance_id = instance_id
|
||||
self._plugin_id = plugin_id
|
||||
self._errored = log_item_data["type"] == "error"
|
||||
self.data = log_item_data
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.data[key]
|
||||
|
||||
@property
|
||||
def errored(self):
|
||||
return self._errored
|
||||
|
||||
@property
|
||||
def instance_id(self):
|
||||
return self._instance_id
|
||||
|
||||
@property
|
||||
def plugin_id(self):
|
||||
return self._plugin_id
|
||||
|
||||
|
||||
class PublishReport:
|
||||
def __init__(self, report_data):
|
||||
data = copy.deepcopy(report_data)
|
||||
|
||||
context_data = data["context"]
|
||||
context_data["name"] = "context"
|
||||
context_data["label"] = context_data["label"] or "Context"
|
||||
|
||||
logs = []
|
||||
plugins_items_by_id = {}
|
||||
plugins_id_order = []
|
||||
for plugin_data in data["plugins_data"]:
|
||||
item = PluginItem(plugin_data)
|
||||
plugins_id_order.append(item.id)
|
||||
plugins_items_by_id[item.id] = item
|
||||
for instance_data_item in plugin_data["instances_data"]:
|
||||
instance_id = instance_data_item["id"]
|
||||
for log_item_data in instance_data_item["logs"]:
|
||||
log_item = LogItem(
|
||||
copy.deepcopy(log_item_data), item.id, instance_id
|
||||
)
|
||||
logs.append(log_item)
|
||||
|
||||
logs_by_instance_id = collections.defaultdict(list)
|
||||
for log_item in logs:
|
||||
logs_by_instance_id[log_item.instance_id].append(log_item)
|
||||
|
||||
instance_items_by_id = {}
|
||||
instance_items_by_family = {}
|
||||
context_item = InstanceItem(None, context_data, logs_by_instance_id)
|
||||
instance_items_by_id[context_item.id] = context_item
|
||||
instance_items_by_family[context_item.family] = [context_item]
|
||||
|
||||
for instance_id, instance_data in data["instances"].items():
|
||||
item = InstanceItem(
|
||||
instance_id, instance_data, logs_by_instance_id
|
||||
)
|
||||
instance_items_by_id[item.id] = item
|
||||
if item.family not in instance_items_by_family:
|
||||
instance_items_by_family[item.family] = []
|
||||
instance_items_by_family[item.family].append(item)
|
||||
|
||||
self.instance_items_by_id = instance_items_by_id
|
||||
self.instance_items_by_family = instance_items_by_family
|
||||
|
||||
self.plugins_id_order = plugins_id_order
|
||||
self.plugins_items_by_id = plugins_items_by_id
|
||||
|
||||
self.logs = logs
|
||||
|
||||
self.crashed_plugin_paths = report_data["crashed_file_paths"]
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
import copy
|
||||
import uuid
|
||||
|
||||
from Qt import QtWidgets, QtCore
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype.widgets.nice_checkbox import NiceCheckbox
|
||||
|
||||
# from openpype.tools.utils import DeselectableTreeView
|
||||
from .constants import (
|
||||
ITEM_ID_ROLE,
|
||||
ITEM_IS_GROUP_ROLE
|
||||
|
|
@ -16,98 +14,127 @@ from .model import (
|
|||
PluginsModel,
|
||||
PluginProxyModel
|
||||
)
|
||||
from .report_items import PublishReport
|
||||
|
||||
FILEPATH_ROLE = QtCore.Qt.UserRole + 1
|
||||
TRACEBACK_ROLE = QtCore.Qt.UserRole + 2
|
||||
IS_DETAIL_ITEM_ROLE = QtCore.Qt.UserRole + 3
|
||||
|
||||
|
||||
class PluginItem:
|
||||
def __init__(self, plugin_data):
|
||||
self._id = uuid.uuid4()
|
||||
class PluginLoadReportModel(QtGui.QStandardItemModel):
|
||||
def set_report(self, report):
|
||||
parent = self.invisibleRootItem()
|
||||
parent.removeRows(0, parent.rowCount())
|
||||
|
||||
self.name = plugin_data["name"]
|
||||
self.label = plugin_data["label"]
|
||||
self.order = plugin_data["order"]
|
||||
self.skipped = plugin_data["skipped"]
|
||||
self.passed = plugin_data["passed"]
|
||||
new_items = []
|
||||
new_items_by_filepath = {}
|
||||
for filepath in report.crashed_plugin_paths.keys():
|
||||
item = QtGui.QStandardItem(filepath)
|
||||
new_items.append(item)
|
||||
new_items_by_filepath[filepath] = item
|
||||
|
||||
logs = []
|
||||
errored = False
|
||||
for instance_data in plugin_data["instances_data"]:
|
||||
for log_item in instance_data["logs"]:
|
||||
if not errored:
|
||||
errored = log_item["type"] == "error"
|
||||
logs.append(copy.deepcopy(log_item))
|
||||
if not new_items:
|
||||
return
|
||||
|
||||
self.errored = errored
|
||||
self.logs = logs
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
parent.appendRows(new_items)
|
||||
for filepath, item in new_items_by_filepath.items():
|
||||
traceback_txt = report.crashed_plugin_paths[filepath]
|
||||
detail_item = QtGui.QStandardItem()
|
||||
detail_item.setData(filepath, FILEPATH_ROLE)
|
||||
detail_item.setData(traceback_txt, TRACEBACK_ROLE)
|
||||
detail_item.setData(True, IS_DETAIL_ITEM_ROLE)
|
||||
item.appendRow(detail_item)
|
||||
|
||||
|
||||
class InstanceItem:
|
||||
def __init__(self, instance_id, instance_data, report_data):
|
||||
self._id = instance_id
|
||||
self.label = instance_data.get("label") or instance_data.get("name")
|
||||
self.family = instance_data.get("family")
|
||||
self.removed = not instance_data.get("exists", True)
|
||||
class DetailWidget(QtWidgets.QTextEdit):
|
||||
def __init__(self, text, *args, **kwargs):
|
||||
super(DetailWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
logs = []
|
||||
for plugin_data in report_data["plugins_data"]:
|
||||
for instance_data_item in plugin_data["instances_data"]:
|
||||
if instance_data_item["id"] == self._id:
|
||||
logs.extend(copy.deepcopy(instance_data_item["logs"]))
|
||||
self.setReadOnly(True)
|
||||
self.setHtml(text)
|
||||
self.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
|
||||
self.setWordWrapMode(
|
||||
QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere
|
||||
)
|
||||
|
||||
errored = False
|
||||
for log in logs:
|
||||
if log["type"] == "error":
|
||||
errored = True
|
||||
break
|
||||
|
||||
self.errored = errored
|
||||
self.logs = logs
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
def sizeHint(self):
|
||||
content_margins = (
|
||||
self.contentsMargins().top()
|
||||
+ self.contentsMargins().bottom()
|
||||
)
|
||||
size = self.document().documentLayout().documentSize().toSize()
|
||||
size.setHeight(size.height() + content_margins)
|
||||
return size
|
||||
|
||||
|
||||
class PublishReport:
|
||||
def __init__(self, report_data):
|
||||
data = copy.deepcopy(report_data)
|
||||
class PluginLoadReportWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent):
|
||||
super(PluginLoadReportWidget, self).__init__(parent)
|
||||
|
||||
context_data = data["context"]
|
||||
context_data["name"] = "context"
|
||||
context_data["label"] = context_data["label"] or "Context"
|
||||
view = QtWidgets.QTreeView(self)
|
||||
view.setEditTriggers(view.NoEditTriggers)
|
||||
view.setTextElideMode(QtCore.Qt.ElideLeft)
|
||||
view.setHeaderHidden(True)
|
||||
view.setAlternatingRowColors(True)
|
||||
view.setVerticalScrollMode(view.ScrollPerPixel)
|
||||
|
||||
instance_items_by_id = {}
|
||||
instance_items_by_family = {}
|
||||
context_item = InstanceItem(None, context_data, data)
|
||||
instance_items_by_id[context_item.id] = context_item
|
||||
instance_items_by_family[context_item.family] = [context_item]
|
||||
model = PluginLoadReportModel()
|
||||
view.setModel(model)
|
||||
|
||||
for instance_id, instance_data in data["instances"].items():
|
||||
item = InstanceItem(instance_id, instance_data, data)
|
||||
instance_items_by_id[item.id] = item
|
||||
if item.family not in instance_items_by_family:
|
||||
instance_items_by_family[item.family] = []
|
||||
instance_items_by_family[item.family].append(item)
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(view, 1)
|
||||
|
||||
all_logs = []
|
||||
plugins_items_by_id = {}
|
||||
plugins_id_order = []
|
||||
for plugin_data in data["plugins_data"]:
|
||||
item = PluginItem(plugin_data)
|
||||
plugins_id_order.append(item.id)
|
||||
plugins_items_by_id[item.id] = item
|
||||
all_logs.extend(copy.deepcopy(item.logs))
|
||||
view.expanded.connect(self._on_expand)
|
||||
|
||||
self.instance_items_by_id = instance_items_by_id
|
||||
self.instance_items_by_family = instance_items_by_family
|
||||
self._view = view
|
||||
self._model = model
|
||||
self._widgets_by_filepath = {}
|
||||
|
||||
self.plugins_id_order = plugins_id_order
|
||||
self.plugins_items_by_id = plugins_items_by_id
|
||||
def _on_expand(self, index):
|
||||
for row in range(self._model.rowCount(index)):
|
||||
child_index = self._model.index(row, index.column(), index)
|
||||
self._create_widget(child_index)
|
||||
|
||||
self.logs = all_logs
|
||||
def showEvent(self, event):
|
||||
super(PluginLoadReportWidget, self).showEvent(event)
|
||||
self._update_widgets_size_hints()
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(PluginLoadReportWidget, self).resizeEvent(event)
|
||||
self._update_widgets_size_hints()
|
||||
|
||||
def _update_widgets_size_hints(self):
|
||||
for item in self._widgets_by_filepath.values():
|
||||
widget, index = item
|
||||
if not widget.isVisible():
|
||||
continue
|
||||
self._model.setData(
|
||||
index, widget.sizeHint(), QtCore.Qt.SizeHintRole
|
||||
)
|
||||
|
||||
def _create_widget(self, index):
|
||||
if not index.data(IS_DETAIL_ITEM_ROLE):
|
||||
return
|
||||
|
||||
filepath = index.data(FILEPATH_ROLE)
|
||||
if filepath in self._widgets_by_filepath:
|
||||
return
|
||||
|
||||
traceback_txt = index.data(TRACEBACK_ROLE)
|
||||
detail_text = (
|
||||
"<b>Filepath:</b><br/>"
|
||||
"{}<br/><br/>"
|
||||
"<b>Traceback:</b><br/>"
|
||||
"{}"
|
||||
).format(filepath, traceback_txt.replace("\n", "<br/>"))
|
||||
widget = DetailWidget(detail_text, self)
|
||||
self._view.setIndexWidget(index, widget)
|
||||
self._widgets_by_filepath[filepath] = (widget, index)
|
||||
|
||||
def set_report(self, report):
|
||||
self._widgets_by_filepath = {}
|
||||
self._model.set_report(report)
|
||||
|
||||
|
||||
class DetailsWidget(QtWidgets.QWidget):
|
||||
|
|
@ -123,11 +150,50 @@ class DetailsWidget(QtWidgets.QWidget):
|
|||
layout.addWidget(output_widget)
|
||||
|
||||
self._output_widget = output_widget
|
||||
self._report_item = None
|
||||
self._instance_filter = set()
|
||||
self._plugin_filter = set()
|
||||
|
||||
def clear(self):
|
||||
self._output_widget.setPlainText("")
|
||||
|
||||
def set_logs(self, logs):
|
||||
def set_report(self, report):
|
||||
self._report_item = report
|
||||
self._plugin_filter = set()
|
||||
self._instance_filter = set()
|
||||
self._update_logs()
|
||||
|
||||
def set_plugin_filter(self, plugin_filter):
|
||||
self._plugin_filter = plugin_filter
|
||||
self._update_logs()
|
||||
|
||||
def set_instance_filter(self, instance_filter):
|
||||
self._instance_filter = instance_filter
|
||||
self._update_logs()
|
||||
|
||||
def _update_logs(self):
|
||||
if not self._report_item:
|
||||
self._output_widget.setPlainText("")
|
||||
return
|
||||
|
||||
filtered_logs = []
|
||||
for log in self._report_item.logs:
|
||||
if (
|
||||
self._instance_filter
|
||||
and log.instance_id not in self._instance_filter
|
||||
):
|
||||
continue
|
||||
|
||||
if (
|
||||
self._plugin_filter
|
||||
and log.plugin_id not in self._plugin_filter
|
||||
):
|
||||
continue
|
||||
filtered_logs.append(log)
|
||||
|
||||
self._set_logs(filtered_logs)
|
||||
|
||||
def _set_logs(self, logs):
|
||||
lines = []
|
||||
for log in logs:
|
||||
if log["type"] == "record":
|
||||
|
|
@ -148,6 +214,60 @@ class DetailsWidget(QtWidgets.QWidget):
|
|||
self._output_widget.setPlainText(text)
|
||||
|
||||
|
||||
class DeselectableTreeView(QtWidgets.QTreeView):
|
||||
"""A tree view that deselects on clicking on an empty area in the view"""
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
index = self.indexAt(event.pos())
|
||||
clear_selection = False
|
||||
if not index.isValid():
|
||||
modifiers = QtWidgets.QApplication.keyboardModifiers()
|
||||
if modifiers == QtCore.Qt.ShiftModifier:
|
||||
return
|
||||
elif modifiers == QtCore.Qt.ControlModifier:
|
||||
return
|
||||
clear_selection = True
|
||||
else:
|
||||
indexes = self.selectedIndexes()
|
||||
if len(indexes) == 1 and index in indexes:
|
||||
clear_selection = True
|
||||
|
||||
if clear_selection:
|
||||
# clear the selection
|
||||
self.clearSelection()
|
||||
# clear the current index
|
||||
self.setCurrentIndex(QtCore.QModelIndex())
|
||||
event.accept()
|
||||
return
|
||||
|
||||
QtWidgets.QTreeView.mousePressEvent(self, event)
|
||||
|
||||
|
||||
class DetailsPopup(QtWidgets.QDialog):
|
||||
closed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent, center_widget):
|
||||
super(DetailsPopup, self).__init__(parent)
|
||||
self.setWindowTitle("Report Details")
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
|
||||
self._center_widget = center_widget
|
||||
self._first_show = True
|
||||
self._layout = layout
|
||||
|
||||
def showEvent(self, event):
|
||||
layout = self.layout()
|
||||
layout.insertWidget(0, self._center_widget)
|
||||
super(DetailsPopup, self).showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self.resize(700, 400)
|
||||
|
||||
def closeEvent(self, event):
|
||||
super(DetailsPopup, self).closeEvent(event)
|
||||
self.closed.emit()
|
||||
|
||||
|
||||
class PublishReportViewerWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(PublishReportViewerWidget, self).__init__(parent)
|
||||
|
|
@ -171,12 +291,13 @@ class PublishReportViewerWidget(QtWidgets.QWidget):
|
|||
removed_instances_layout.addWidget(removed_instances_check, 0)
|
||||
removed_instances_layout.addWidget(removed_instances_label, 1)
|
||||
|
||||
instances_view = QtWidgets.QTreeView(self)
|
||||
instances_view = DeselectableTreeView(self)
|
||||
instances_view.setObjectName("PublishDetailViews")
|
||||
instances_view.setModel(instances_proxy)
|
||||
instances_view.setIndentation(0)
|
||||
instances_view.setHeaderHidden(True)
|
||||
instances_view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
|
||||
instances_view.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection)
|
||||
instances_view.setExpandsOnDoubleClick(False)
|
||||
|
||||
instances_delegate = GroupItemDelegate(instances_view)
|
||||
|
|
@ -191,29 +312,49 @@ class PublishReportViewerWidget(QtWidgets.QWidget):
|
|||
skipped_plugins_layout.addWidget(skipped_plugins_check, 0)
|
||||
skipped_plugins_layout.addWidget(skipped_plugins_label, 1)
|
||||
|
||||
plugins_view = QtWidgets.QTreeView(self)
|
||||
plugins_view = DeselectableTreeView(self)
|
||||
plugins_view.setObjectName("PublishDetailViews")
|
||||
plugins_view.setModel(plugins_proxy)
|
||||
plugins_view.setIndentation(0)
|
||||
plugins_view.setHeaderHidden(True)
|
||||
plugins_view.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection)
|
||||
plugins_view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
|
||||
plugins_view.setExpandsOnDoubleClick(False)
|
||||
|
||||
plugins_delegate = GroupItemDelegate(plugins_view)
|
||||
plugins_view.setItemDelegate(plugins_delegate)
|
||||
|
||||
details_widget = DetailsWidget(self)
|
||||
details_widget = QtWidgets.QWidget(self)
|
||||
details_tab_widget = QtWidgets.QTabWidget(details_widget)
|
||||
details_popup_btn = QtWidgets.QPushButton("PopUp", details_widget)
|
||||
|
||||
layout = QtWidgets.QGridLayout(self)
|
||||
details_layout = QtWidgets.QVBoxLayout(details_widget)
|
||||
details_layout.setContentsMargins(0, 0, 0, 0)
|
||||
details_layout.addWidget(details_tab_widget, 1)
|
||||
details_layout.addWidget(details_popup_btn, 0)
|
||||
|
||||
details_popup = DetailsPopup(self, details_tab_widget)
|
||||
|
||||
logs_text_widget = DetailsWidget(details_tab_widget)
|
||||
plugin_load_report_widget = PluginLoadReportWidget(details_tab_widget)
|
||||
|
||||
details_tab_widget.addTab(logs_text_widget, "Logs")
|
||||
details_tab_widget.addTab(plugin_load_report_widget, "Crashed plugins")
|
||||
|
||||
middle_widget = QtWidgets.QWidget(self)
|
||||
middle_layout = QtWidgets.QGridLayout(middle_widget)
|
||||
middle_layout.setContentsMargins(0, 0, 0, 0)
|
||||
# Row 1
|
||||
layout.addLayout(removed_instances_layout, 0, 0)
|
||||
layout.addLayout(skipped_plugins_layout, 0, 1)
|
||||
middle_layout.addLayout(removed_instances_layout, 0, 0)
|
||||
middle_layout.addLayout(skipped_plugins_layout, 0, 1)
|
||||
# Row 2
|
||||
layout.addWidget(instances_view, 1, 0)
|
||||
layout.addWidget(plugins_view, 1, 1)
|
||||
layout.addWidget(details_widget, 1, 2)
|
||||
middle_layout.addWidget(instances_view, 1, 0)
|
||||
middle_layout.addWidget(plugins_view, 1, 1)
|
||||
|
||||
layout.setColumnStretch(2, 1)
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(middle_widget, 0)
|
||||
layout.addWidget(details_widget, 1)
|
||||
|
||||
instances_view.selectionModel().selectionChanged.connect(
|
||||
self._on_instance_change
|
||||
|
|
@ -230,10 +371,13 @@ class PublishReportViewerWidget(QtWidgets.QWidget):
|
|||
removed_instances_check.stateChanged.connect(
|
||||
self._on_removed_instances_check
|
||||
)
|
||||
details_popup_btn.clicked.connect(self._on_details_popup)
|
||||
details_popup.closed.connect(self._on_popup_close)
|
||||
|
||||
self._ignore_selection_changes = False
|
||||
self._report_item = None
|
||||
self._details_widget = details_widget
|
||||
self._logs_text_widget = logs_text_widget
|
||||
self._plugin_load_report_widget = plugin_load_report_widget
|
||||
|
||||
self._removed_instances_check = removed_instances_check
|
||||
self._instances_view = instances_view
|
||||
|
|
@ -248,6 +392,10 @@ class PublishReportViewerWidget(QtWidgets.QWidget):
|
|||
self._plugins_model = plugins_model
|
||||
self._plugins_proxy = plugins_proxy
|
||||
|
||||
self._details_widget = details_widget
|
||||
self._details_tab_widget = details_tab_widget
|
||||
self._details_popup = details_popup
|
||||
|
||||
def _on_instance_view_clicked(self, index):
|
||||
if not index.isValid() or not index.data(ITEM_IS_GROUP_ROLE):
|
||||
return
|
||||
|
|
@ -266,62 +414,46 @@ class PublishReportViewerWidget(QtWidgets.QWidget):
|
|||
else:
|
||||
self._plugins_view.expand(index)
|
||||
|
||||
def set_report(self, report_data):
|
||||
def set_report_data(self, report_data):
|
||||
report = PublishReport(report_data)
|
||||
self.set_report(report)
|
||||
|
||||
def set_report(self, report):
|
||||
self._ignore_selection_changes = True
|
||||
|
||||
report_item = PublishReport(report_data)
|
||||
self._report_item = report_item
|
||||
self._report_item = report
|
||||
|
||||
self._instances_model.set_report(report_item)
|
||||
self._plugins_model.set_report(report_item)
|
||||
self._details_widget.set_logs(report_item.logs)
|
||||
self._instances_model.set_report(report)
|
||||
self._plugins_model.set_report(report)
|
||||
self._logs_text_widget.set_report(report)
|
||||
self._plugin_load_report_widget.set_report(report)
|
||||
|
||||
self._ignore_selection_changes = False
|
||||
|
||||
self._instances_view.expandAll()
|
||||
self._plugins_view.expandAll()
|
||||
|
||||
def _on_instance_change(self, *_args):
|
||||
if self._ignore_selection_changes:
|
||||
return
|
||||
|
||||
valid_index = None
|
||||
instance_ids = set()
|
||||
for index in self._instances_view.selectedIndexes():
|
||||
if index.isValid():
|
||||
valid_index = index
|
||||
break
|
||||
instance_ids.add(index.data(ITEM_ID_ROLE))
|
||||
|
||||
if valid_index is None:
|
||||
return
|
||||
|
||||
if self._plugins_view.selectedIndexes():
|
||||
self._ignore_selection_changes = True
|
||||
self._plugins_view.selectionModel().clearSelection()
|
||||
self._ignore_selection_changes = False
|
||||
|
||||
plugin_id = valid_index.data(ITEM_ID_ROLE)
|
||||
instance_item = self._report_item.instance_items_by_id[plugin_id]
|
||||
self._details_widget.set_logs(instance_item.logs)
|
||||
self._logs_text_widget.set_instance_filter(instance_ids)
|
||||
|
||||
def _on_plugin_change(self, *_args):
|
||||
if self._ignore_selection_changes:
|
||||
return
|
||||
|
||||
valid_index = None
|
||||
plugin_ids = set()
|
||||
for index in self._plugins_view.selectedIndexes():
|
||||
if index.isValid():
|
||||
valid_index = index
|
||||
break
|
||||
plugin_ids.add(index.data(ITEM_ID_ROLE))
|
||||
|
||||
if valid_index is None:
|
||||
self._details_widget.set_logs(self._report_item.logs)
|
||||
return
|
||||
|
||||
if self._instances_view.selectedIndexes():
|
||||
self._ignore_selection_changes = True
|
||||
self._instances_view.selectionModel().clearSelection()
|
||||
self._ignore_selection_changes = False
|
||||
|
||||
plugin_id = valid_index.data(ITEM_ID_ROLE)
|
||||
plugin_item = self._report_item.plugins_items_by_id[plugin_id]
|
||||
self._details_widget.set_logs(plugin_item.logs)
|
||||
self._logs_text_widget.set_plugin_filter(plugin_ids)
|
||||
|
||||
def _on_skipped_plugin_check(self):
|
||||
self._plugins_proxy.set_ignore_skipped(
|
||||
|
|
@ -332,3 +464,16 @@ class PublishReportViewerWidget(QtWidgets.QWidget):
|
|||
self._instances_proxy.set_ignore_removed(
|
||||
self._removed_instances_check.isChecked()
|
||||
)
|
||||
|
||||
def _on_details_popup(self):
|
||||
self._details_widget.setVisible(False)
|
||||
self._details_popup.show()
|
||||
|
||||
def _on_popup_close(self):
|
||||
self._details_widget.setVisible(True)
|
||||
layout = self._details_widget.layout()
|
||||
layout.insertWidget(0, self._details_tab_widget)
|
||||
|
||||
def close_details_popup(self):
|
||||
if self._details_popup.isVisible():
|
||||
self._details_popup.close()
|
||||
|
|
|
|||
|
|
@ -1,29 +1,355 @@
|
|||
from Qt import QtWidgets
|
||||
import os
|
||||
import json
|
||||
import six
|
||||
import appdirs
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from openpype import style
|
||||
from openpype.lib import JSONSettingRegistry
|
||||
from openpype.resources import get_openpype_icon_filepath
|
||||
from openpype.tools import resources
|
||||
from openpype.tools.utils import (
|
||||
IconButton,
|
||||
paint_image_with_color
|
||||
)
|
||||
|
||||
from openpype.tools.utils.delegates import PrettyTimeDelegate
|
||||
|
||||
if __package__:
|
||||
from .widgets import PublishReportViewerWidget
|
||||
from .report_items import PublishReport
|
||||
else:
|
||||
from widgets import PublishReportViewerWidget
|
||||
from report_items import PublishReport
|
||||
|
||||
|
||||
FILEPATH_ROLE = QtCore.Qt.UserRole + 1
|
||||
MODIFIED_ROLE = QtCore.Qt.UserRole + 2
|
||||
|
||||
|
||||
class PublisherReportRegistry(JSONSettingRegistry):
|
||||
"""Class handling storing publish report tool.
|
||||
|
||||
Attributes:
|
||||
vendor (str): Name used for path construction.
|
||||
product (str): Additional name used for path construction.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.vendor = "pypeclub"
|
||||
self.product = "openpype"
|
||||
name = "publish_report_viewer"
|
||||
path = appdirs.user_data_dir(self.product, self.vendor)
|
||||
super(PublisherReportRegistry, self).__init__(name, path)
|
||||
|
||||
|
||||
class LoadedFilesMopdel(QtGui.QStandardItemModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoadedFilesMopdel, self).__init__(*args, **kwargs)
|
||||
self.setColumnCount(2)
|
||||
self._items_by_filepath = {}
|
||||
self._reports_by_filepath = {}
|
||||
|
||||
self._registry = PublisherReportRegistry()
|
||||
|
||||
self._loading_registry = False
|
||||
self._load_registry()
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
|
||||
if section == 0:
|
||||
return "Exports"
|
||||
if section == 1:
|
||||
return "Modified"
|
||||
return ""
|
||||
super(LoadedFilesMopdel, self).headerData(section, orientation, role)
|
||||
|
||||
def _load_registry(self):
|
||||
self._loading_registry = True
|
||||
try:
|
||||
filepaths = self._registry.get_item("filepaths")
|
||||
self.add_filepaths(filepaths)
|
||||
except ValueError:
|
||||
pass
|
||||
self._loading_registry = False
|
||||
|
||||
def _store_registry(self):
|
||||
if self._loading_registry:
|
||||
return
|
||||
filepaths = list(self._items_by_filepath.keys())
|
||||
self._registry.set_item("filepaths", filepaths)
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.DisplayRole
|
||||
|
||||
col = index.column()
|
||||
if col != 0:
|
||||
index = self.index(index.row(), 0, index.parent())
|
||||
|
||||
if role == QtCore.Qt.ToolTipRole:
|
||||
if col == 0:
|
||||
role = FILEPATH_ROLE
|
||||
elif col == 1:
|
||||
return "File modified"
|
||||
return None
|
||||
|
||||
elif role == QtCore.Qt.DisplayRole:
|
||||
if col == 1:
|
||||
role = MODIFIED_ROLE
|
||||
return super(LoadedFilesMopdel, self).data(index, role)
|
||||
|
||||
def add_filepaths(self, filepaths):
|
||||
if not filepaths:
|
||||
return
|
||||
|
||||
if isinstance(filepaths, six.string_types):
|
||||
filepaths = [filepaths]
|
||||
|
||||
filtered_paths = []
|
||||
for filepath in filepaths:
|
||||
normalized_path = os.path.normpath(filepath)
|
||||
if normalized_path in self._items_by_filepath:
|
||||
continue
|
||||
|
||||
if (
|
||||
os.path.exists(normalized_path)
|
||||
and normalized_path not in filtered_paths
|
||||
):
|
||||
filtered_paths.append(normalized_path)
|
||||
|
||||
if not filtered_paths:
|
||||
return
|
||||
|
||||
new_items = []
|
||||
for normalized_path in filtered_paths:
|
||||
try:
|
||||
with open(normalized_path, "r") as stream:
|
||||
data = json.load(stream)
|
||||
report = PublishReport(data)
|
||||
except Exception:
|
||||
# TODO handle errors
|
||||
continue
|
||||
|
||||
modified = os.path.getmtime(normalized_path)
|
||||
item = QtGui.QStandardItem(os.path.basename(normalized_path))
|
||||
item.setColumnCount(self.columnCount())
|
||||
item.setData(normalized_path, FILEPATH_ROLE)
|
||||
item.setData(modified, MODIFIED_ROLE)
|
||||
new_items.append(item)
|
||||
self._items_by_filepath[normalized_path] = item
|
||||
self._reports_by_filepath[normalized_path] = report
|
||||
|
||||
if not new_items:
|
||||
return
|
||||
|
||||
parent = self.invisibleRootItem()
|
||||
parent.appendRows(new_items)
|
||||
|
||||
self._store_registry()
|
||||
|
||||
def remove_filepaths(self, filepaths):
|
||||
if not filepaths:
|
||||
return
|
||||
|
||||
if isinstance(filepaths, six.string_types):
|
||||
filepaths = [filepaths]
|
||||
|
||||
filtered_paths = []
|
||||
for filepath in filepaths:
|
||||
normalized_path = os.path.normpath(filepath)
|
||||
if normalized_path in self._items_by_filepath:
|
||||
filtered_paths.append(normalized_path)
|
||||
|
||||
if not filtered_paths:
|
||||
return
|
||||
|
||||
parent = self.invisibleRootItem()
|
||||
for filepath in filtered_paths:
|
||||
self._reports_by_filepath.pop(normalized_path)
|
||||
item = self._items_by_filepath.pop(filepath)
|
||||
parent.removeRow(item.row())
|
||||
|
||||
self._store_registry()
|
||||
|
||||
def get_report_by_filepath(self, filepath):
|
||||
return self._reports_by_filepath.get(filepath)
|
||||
|
||||
|
||||
class LoadedFilesView(QtWidgets.QTreeView):
|
||||
selection_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoadedFilesView, self).__init__(*args, **kwargs)
|
||||
self.setEditTriggers(self.NoEditTriggers)
|
||||
self.setIndentation(0)
|
||||
self.setAlternatingRowColors(True)
|
||||
|
||||
model = LoadedFilesMopdel()
|
||||
self.setModel(model)
|
||||
|
||||
time_delegate = PrettyTimeDelegate()
|
||||
self.setItemDelegateForColumn(1, time_delegate)
|
||||
|
||||
remove_btn = IconButton(self)
|
||||
remove_icon_path = resources.get_icon_path("delete")
|
||||
loaded_remove_image = QtGui.QImage(remove_icon_path)
|
||||
pix = paint_image_with_color(loaded_remove_image, QtCore.Qt.white)
|
||||
icon = QtGui.QIcon(pix)
|
||||
remove_btn.setIcon(icon)
|
||||
|
||||
model.rowsInserted.connect(self._on_rows_inserted)
|
||||
remove_btn.clicked.connect(self._on_remove_clicked)
|
||||
self.selectionModel().selectionChanged.connect(
|
||||
self._on_selection_change
|
||||
)
|
||||
|
||||
self._model = model
|
||||
self._time_delegate = time_delegate
|
||||
self._remove_btn = remove_btn
|
||||
|
||||
def _update_remove_btn(self):
|
||||
viewport = self.viewport()
|
||||
height = viewport.height() + self.header().height()
|
||||
pos_x = viewport.width() - self._remove_btn.width() - 5
|
||||
pos_y = height - self._remove_btn.height() - 5
|
||||
self._remove_btn.move(max(0, pos_x), max(0, pos_y))
|
||||
|
||||
def _on_rows_inserted(self):
|
||||
header = self.header()
|
||||
header.resizeSections(header.ResizeToContents)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(LoadedFilesView, self).resizeEvent(event)
|
||||
self._update_remove_btn()
|
||||
|
||||
def showEvent(self, event):
|
||||
super(LoadedFilesView, self).showEvent(event)
|
||||
self._update_remove_btn()
|
||||
header = self.header()
|
||||
header.resizeSections(header.ResizeToContents)
|
||||
|
||||
def _on_selection_change(self):
|
||||
self.selection_changed.emit()
|
||||
|
||||
def add_filepaths(self, filepaths):
|
||||
self._model.add_filepaths(filepaths)
|
||||
self._fill_selection()
|
||||
|
||||
def remove_filepaths(self, filepaths):
|
||||
self._model.remove_filepaths(filepaths)
|
||||
self._fill_selection()
|
||||
|
||||
def _on_remove_clicked(self):
|
||||
index = self.currentIndex()
|
||||
filepath = index.data(FILEPATH_ROLE)
|
||||
self.remove_filepaths(filepath)
|
||||
|
||||
def _fill_selection(self):
|
||||
index = self.currentIndex()
|
||||
if index.isValid():
|
||||
return
|
||||
|
||||
index = self._model.index(0, 0)
|
||||
if index.isValid():
|
||||
self.setCurrentIndex(index)
|
||||
|
||||
def get_current_report(self):
|
||||
index = self.currentIndex()
|
||||
filepath = index.data(FILEPATH_ROLE)
|
||||
return self._model.get_report_by_filepath(filepath)
|
||||
|
||||
|
||||
class LoadedFilesWidget(QtWidgets.QWidget):
|
||||
report_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(LoadedFilesWidget, self).__init__(parent)
|
||||
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
view = LoadedFilesView(self)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(view, 1)
|
||||
|
||||
view.selection_changed.connect(self._on_report_change)
|
||||
|
||||
self._view = view
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
mime_data = event.mimeData()
|
||||
if mime_data.hasUrls():
|
||||
event.setDropAction(QtCore.Qt.CopyAction)
|
||||
event.accept()
|
||||
|
||||
def dragLeaveEvent(self, event):
|
||||
event.accept()
|
||||
|
||||
def dropEvent(self, event):
|
||||
mime_data = event.mimeData()
|
||||
if mime_data.hasUrls():
|
||||
filepaths = []
|
||||
for url in mime_data.urls():
|
||||
filepath = url.toLocalFile()
|
||||
ext = os.path.splitext(filepath)[-1]
|
||||
if os.path.exists(filepath) and ext == ".json":
|
||||
filepaths.append(filepath)
|
||||
self._add_filepaths(filepaths)
|
||||
event.accept()
|
||||
|
||||
def _on_report_change(self):
|
||||
self.report_changed.emit()
|
||||
|
||||
def _add_filepaths(self, filepaths):
|
||||
self._view.add_filepaths(filepaths)
|
||||
|
||||
def get_current_report(self):
|
||||
return self._view.get_current_report()
|
||||
|
||||
|
||||
class PublishReportViewerWindow(QtWidgets.QWidget):
|
||||
# TODO add buttons to be able load report file or paste content of report
|
||||
default_width = 1200
|
||||
default_height = 600
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(PublishReportViewerWindow, self).__init__(parent)
|
||||
self.setWindowTitle("Publish report viewer")
|
||||
icon = QtGui.QIcon(get_openpype_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
main_widget = PublishReportViewerWidget(self)
|
||||
body = QtWidgets.QSplitter(self)
|
||||
body.setContentsMargins(0, 0, 0, 0)
|
||||
body.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding,
|
||||
QtWidgets.QSizePolicy.Expanding
|
||||
)
|
||||
body.setOrientation(QtCore.Qt.Horizontal)
|
||||
|
||||
loaded_files_widget = LoadedFilesWidget(body)
|
||||
main_widget = PublishReportViewerWidget(body)
|
||||
|
||||
body.addWidget(loaded_files_widget)
|
||||
body.addWidget(main_widget)
|
||||
body.setStretchFactor(0, 70)
|
||||
body.setStretchFactor(1, 65)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.addWidget(main_widget)
|
||||
layout.addWidget(body, 1)
|
||||
|
||||
loaded_files_widget.report_changed.connect(self._on_report_change)
|
||||
|
||||
self._loaded_files_widget = loaded_files_widget
|
||||
self._main_widget = main_widget
|
||||
|
||||
self.resize(self.default_width, self.default_height)
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
def _on_report_change(self):
|
||||
report = self._loaded_files_widget.get_current_report()
|
||||
self.set_report(report)
|
||||
|
||||
def set_report(self, report_data):
|
||||
self._main_widget.set_report(report_data)
|
||||
|
|
|
|||
|
|
@ -174,6 +174,8 @@ class CreatorDescriptionWidget(QtWidgets.QWidget):
|
|||
|
||||
|
||||
class CreateDialog(QtWidgets.QDialog):
|
||||
default_size = (900, 500)
|
||||
|
||||
def __init__(
|
||||
self, controller, asset_name=None, task_name=None, parent=None
|
||||
):
|
||||
|
|
@ -262,11 +264,16 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
mid_layout.addLayout(form_layout, 0)
|
||||
mid_layout.addWidget(create_btn, 0)
|
||||
|
||||
splitter_widget = QtWidgets.QSplitter(self)
|
||||
splitter_widget.addWidget(context_widget)
|
||||
splitter_widget.addWidget(mid_widget)
|
||||
splitter_widget.addWidget(pre_create_widget)
|
||||
splitter_widget.setStretchFactor(0, 1)
|
||||
splitter_widget.setStretchFactor(1, 1)
|
||||
splitter_widget.setStretchFactor(2, 1)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setSpacing(10)
|
||||
layout.addWidget(context_widget, 1)
|
||||
layout.addWidget(mid_widget, 1)
|
||||
layout.addWidget(pre_create_widget, 1)
|
||||
layout.addWidget(splitter_widget, 1)
|
||||
|
||||
prereq_timer = QtCore.QTimer()
|
||||
prereq_timer.setInterval(50)
|
||||
|
|
@ -289,6 +296,8 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
|
||||
controller.add_plugins_refresh_callback(self._on_plugins_refresh)
|
||||
|
||||
self._splitter_widget = splitter_widget
|
||||
|
||||
self._pre_create_widget = pre_create_widget
|
||||
|
||||
self._context_widget = context_widget
|
||||
|
|
@ -308,6 +317,7 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
self.create_btn = create_btn
|
||||
|
||||
self._prereq_timer = prereq_timer
|
||||
self._first_show = True
|
||||
|
||||
def _context_change_is_enabled(self):
|
||||
return self._context_widget.isEnabled()
|
||||
|
|
@ -643,6 +653,16 @@ class CreateDialog(QtWidgets.QDialog):
|
|||
|
||||
def showEvent(self, event):
|
||||
super(CreateDialog, self).showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
width, height = self.default_size
|
||||
self.resize(width, height)
|
||||
|
||||
third_size = int(width / 3)
|
||||
self._splitter_widget.setSizes(
|
||||
[third_size, third_size, width - (2 * third_size)]
|
||||
)
|
||||
|
||||
if self._last_pos is not None:
|
||||
self.move(self._last_pos)
|
||||
|
||||
|
|
|
|||
|
|
@ -213,7 +213,6 @@ class PublishFrame(QtWidgets.QFrame):
|
|||
close_report_btn.setIcon(close_report_icon)
|
||||
|
||||
details_layout = QtWidgets.QVBoxLayout(details_widget)
|
||||
details_layout.setContentsMargins(0, 0, 0, 0)
|
||||
details_layout.addWidget(report_view)
|
||||
details_layout.addWidget(close_report_btn)
|
||||
|
||||
|
|
@ -495,10 +494,11 @@ class PublishFrame(QtWidgets.QFrame):
|
|||
def _on_show_details(self):
|
||||
self._change_bg_property(2)
|
||||
self._main_layout.setCurrentWidget(self._details_widget)
|
||||
logs = self.controller.get_publish_report()
|
||||
self._report_view.set_report(logs)
|
||||
report_data = self.controller.get_publish_report()
|
||||
self._report_view.set_report_data(report_data)
|
||||
|
||||
def _on_close_report_clicked(self):
|
||||
self._report_view.close_details_popup()
|
||||
if self.controller.get_publish_crash_error():
|
||||
self._change_bg_property()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ from openpype.tools.utils import BaseClickableFrame
|
|||
from .widgets import (
|
||||
IconValuePixmapLabel
|
||||
)
|
||||
from ..constants import (
|
||||
INSTANCE_ID_ROLE
|
||||
)
|
||||
|
||||
|
||||
class ValidationErrorInstanceList(QtWidgets.QListView):
|
||||
|
|
@ -22,19 +25,20 @@ class ValidationErrorInstanceList(QtWidgets.QListView):
|
|||
|
||||
self.setObjectName("ValidationErrorInstanceList")
|
||||
|
||||
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.setSelectionMode(QtWidgets.QListView.ExtendedSelection)
|
||||
|
||||
def minimumSizeHint(self):
|
||||
result = super(ValidationErrorInstanceList, self).minimumSizeHint()
|
||||
result.setHeight(self.sizeHint().height())
|
||||
return result
|
||||
return self.sizeHint()
|
||||
|
||||
def sizeHint(self):
|
||||
result = super(ValidationErrorInstanceList, self).sizeHint()
|
||||
row_count = self.model().rowCount()
|
||||
height = 0
|
||||
if row_count > 0:
|
||||
height = self.sizeHintForRow(0) * row_count
|
||||
return QtCore.QSize(self.width(), height)
|
||||
result.setHeight(height)
|
||||
return result
|
||||
|
||||
|
||||
class ValidationErrorTitleWidget(QtWidgets.QWidget):
|
||||
|
|
@ -47,6 +51,7 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
|
|||
if there is a list (Valdation error may happen on context).
|
||||
"""
|
||||
selected = QtCore.Signal(int)
|
||||
instance_changed = QtCore.Signal(int)
|
||||
|
||||
def __init__(self, index, error_info, parent):
|
||||
super(ValidationErrorTitleWidget, self).__init__(parent)
|
||||
|
|
@ -64,32 +69,38 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
|
|||
toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow)
|
||||
toggle_instance_btn.setMaximumWidth(14)
|
||||
|
||||
exception = error_info["exception"]
|
||||
label_widget = QtWidgets.QLabel(exception.title, title_frame)
|
||||
label_widget = QtWidgets.QLabel(error_info["title"], title_frame)
|
||||
|
||||
title_frame_layout = QtWidgets.QHBoxLayout(title_frame)
|
||||
title_frame_layout.addWidget(toggle_instance_btn)
|
||||
title_frame_layout.addWidget(label_widget)
|
||||
|
||||
instances_model = QtGui.QStandardItemModel()
|
||||
instances = error_info["instances"]
|
||||
error_info = error_info["error_info"]
|
||||
|
||||
help_text_by_instance_id = {}
|
||||
context_validation = False
|
||||
if (
|
||||
not instances
|
||||
or (len(instances) == 1 and instances[0] is None)
|
||||
not error_info
|
||||
or (len(error_info) == 1 and error_info[0][0] is None)
|
||||
):
|
||||
context_validation = True
|
||||
toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow)
|
||||
description = self._prepare_description(error_info[0][1])
|
||||
help_text_by_instance_id[None] = description
|
||||
else:
|
||||
items = []
|
||||
for instance in instances:
|
||||
for instance, exception in error_info:
|
||||
label = instance.data.get("label") or instance.data.get("name")
|
||||
item = QtGui.QStandardItem(label)
|
||||
item.setFlags(
|
||||
QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
|
||||
)
|
||||
item.setData(instance.id)
|
||||
item.setData(label, QtCore.Qt.ToolTipRole)
|
||||
item.setData(instance.id, INSTANCE_ID_ROLE)
|
||||
items.append(item)
|
||||
description = self._prepare_description(exception)
|
||||
help_text_by_instance_id[instance.id] = description
|
||||
|
||||
instances_model.invisibleRootItem().appendRows(items)
|
||||
|
||||
|
|
@ -114,17 +125,64 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
|
|||
if not context_validation:
|
||||
toggle_instance_btn.clicked.connect(self._on_toggle_btn_click)
|
||||
|
||||
instances_view.selectionModel().selectionChanged.connect(
|
||||
self._on_seleciton_change
|
||||
)
|
||||
|
||||
self._title_frame = title_frame
|
||||
|
||||
self._toggle_instance_btn = toggle_instance_btn
|
||||
|
||||
self._view_layout = view_layout
|
||||
|
||||
self._instances_model = instances_model
|
||||
self._instances_view = instances_view
|
||||
|
||||
self._context_validation = context_validation
|
||||
self._help_text_by_instance_id = help_text_by_instance_id
|
||||
|
||||
def sizeHint(self):
|
||||
result = super().sizeHint()
|
||||
expected_width = 0
|
||||
for idx in range(self._view_layout.count()):
|
||||
expected_width += self._view_layout.itemAt(idx).sizeHint().width()
|
||||
|
||||
if expected_width < 200:
|
||||
expected_width = 200
|
||||
|
||||
if result.width() < expected_width:
|
||||
result.setWidth(expected_width)
|
||||
return result
|
||||
|
||||
def minimumSizeHint(self):
|
||||
return self.sizeHint()
|
||||
|
||||
def _prepare_description(self, exception):
|
||||
dsc = exception.description
|
||||
detail = exception.detail
|
||||
if detail:
|
||||
dsc += "<br/><br/>{}".format(detail)
|
||||
|
||||
description = dsc
|
||||
if commonmark:
|
||||
description = commonmark.commonmark(dsc)
|
||||
return description
|
||||
|
||||
def _mouse_release_callback(self):
|
||||
"""Mark this widget as selected on click."""
|
||||
self.set_selected(True)
|
||||
|
||||
def current_desctiption_text(self):
|
||||
if self._context_validation:
|
||||
return self._help_text_by_instance_id[None]
|
||||
index = self._instances_view.currentIndex()
|
||||
# TODO make sure instance is selected
|
||||
if not index.isValid():
|
||||
index = self._instances_model.index(0, 0)
|
||||
|
||||
indence_id = index.data(INSTANCE_ID_ROLE)
|
||||
return self._help_text_by_instance_id[indence_id]
|
||||
|
||||
@property
|
||||
def is_selected(self):
|
||||
"""Is widget marked a selected"""
|
||||
|
|
@ -167,6 +225,9 @@ class ValidationErrorTitleWidget(QtWidgets.QWidget):
|
|||
else:
|
||||
self._toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow)
|
||||
|
||||
def _on_seleciton_change(self):
|
||||
self.instance_changed.emit(self._index)
|
||||
|
||||
|
||||
class ActionButton(BaseClickableFrame):
|
||||
"""Plugin's action callback button.
|
||||
|
|
@ -185,13 +246,15 @@ class ActionButton(BaseClickableFrame):
|
|||
action_label = action.label or action.__name__
|
||||
action_icon = getattr(action, "icon", None)
|
||||
label_widget = QtWidgets.QLabel(action_label, self)
|
||||
icon_label = None
|
||||
if action_icon:
|
||||
icon_label = IconValuePixmapLabel(action_icon, self)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(5, 0, 5, 0)
|
||||
layout.addWidget(label_widget, 1)
|
||||
layout.addWidget(icon_label, 0)
|
||||
if icon_label:
|
||||
layout.addWidget(icon_label, 0)
|
||||
|
||||
self.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Minimum,
|
||||
|
|
@ -231,6 +294,7 @@ class ValidateActionsWidget(QtWidgets.QFrame):
|
|||
item = self._content_layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget:
|
||||
widget.setVisible(False)
|
||||
widget.deleteLater()
|
||||
self._actions_mapping = {}
|
||||
|
||||
|
|
@ -363,24 +427,23 @@ class ValidationsWidget(QtWidgets.QWidget):
|
|||
errors_scroll.setWidgetResizable(True)
|
||||
|
||||
errors_widget = QtWidgets.QWidget(errors_scroll)
|
||||
errors_widget.setFixedWidth(200)
|
||||
errors_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
errors_layout = QtWidgets.QVBoxLayout(errors_widget)
|
||||
errors_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
errors_scroll.setWidget(errors_widget)
|
||||
|
||||
error_details_widget = QtWidgets.QWidget(self)
|
||||
error_details_input = QtWidgets.QTextEdit(error_details_widget)
|
||||
error_details_frame = QtWidgets.QFrame(self)
|
||||
error_details_input = QtWidgets.QTextEdit(error_details_frame)
|
||||
error_details_input.setObjectName("InfoText")
|
||||
error_details_input.setTextInteractionFlags(
|
||||
QtCore.Qt.TextBrowserInteraction
|
||||
)
|
||||
|
||||
actions_widget = ValidateActionsWidget(controller, self)
|
||||
actions_widget.setFixedWidth(140)
|
||||
actions_widget.setMinimumWidth(140)
|
||||
|
||||
error_details_layout = QtWidgets.QHBoxLayout(error_details_widget)
|
||||
error_details_layout = QtWidgets.QHBoxLayout(error_details_frame)
|
||||
error_details_layout.addWidget(error_details_input, 1)
|
||||
error_details_layout.addWidget(actions_widget, 0)
|
||||
|
||||
|
|
@ -389,7 +452,7 @@ class ValidationsWidget(QtWidgets.QWidget):
|
|||
content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
content_layout.addWidget(errors_scroll, 0)
|
||||
content_layout.addWidget(error_details_widget, 1)
|
||||
content_layout.addWidget(error_details_frame, 1)
|
||||
|
||||
top_label = QtWidgets.QLabel("Publish validation report", self)
|
||||
top_label.setObjectName("PublishInfoMainLabel")
|
||||
|
|
@ -403,7 +466,7 @@ class ValidationsWidget(QtWidgets.QWidget):
|
|||
self._top_label = top_label
|
||||
self._errors_widget = errors_widget
|
||||
self._errors_layout = errors_layout
|
||||
self._error_details_widget = error_details_widget
|
||||
self._error_details_frame = error_details_frame
|
||||
self._error_details_input = error_details_input
|
||||
self._actions_widget = actions_widget
|
||||
|
||||
|
|
@ -423,7 +486,7 @@ class ValidationsWidget(QtWidgets.QWidget):
|
|||
widget.deleteLater()
|
||||
|
||||
self._top_label.setVisible(False)
|
||||
self._error_details_widget.setVisible(False)
|
||||
self._error_details_frame.setVisible(False)
|
||||
self._errors_widget.setVisible(False)
|
||||
self._actions_widget.setVisible(False)
|
||||
|
||||
|
|
@ -434,34 +497,35 @@ class ValidationsWidget(QtWidgets.QWidget):
|
|||
return
|
||||
|
||||
self._top_label.setVisible(True)
|
||||
self._error_details_widget.setVisible(True)
|
||||
self._error_details_frame.setVisible(True)
|
||||
self._errors_widget.setVisible(True)
|
||||
|
||||
errors_by_title = []
|
||||
for plugin_info in errors:
|
||||
titles = []
|
||||
exception_by_title = {}
|
||||
instances_by_title = {}
|
||||
error_info_by_title = {}
|
||||
|
||||
for error_info in plugin_info["errors"]:
|
||||
exception = error_info["exception"]
|
||||
title = exception.title
|
||||
if title not in titles:
|
||||
titles.append(title)
|
||||
instances_by_title[title] = []
|
||||
exception_by_title[title] = exception
|
||||
instances_by_title[title].append(error_info["instance"])
|
||||
error_info_by_title[title] = []
|
||||
error_info_by_title[title].append(
|
||||
(error_info["instance"], exception)
|
||||
)
|
||||
|
||||
for title in titles:
|
||||
errors_by_title.append({
|
||||
"plugin": plugin_info["plugin"],
|
||||
"exception": exception_by_title[title],
|
||||
"instances": instances_by_title[title]
|
||||
"error_info": error_info_by_title[title],
|
||||
"title": title
|
||||
})
|
||||
|
||||
for idx, item in enumerate(errors_by_title):
|
||||
widget = ValidationErrorTitleWidget(idx, item, self)
|
||||
widget.selected.connect(self._on_select)
|
||||
widget.instance_changed.connect(self._on_instance_change)
|
||||
self._errors_layout.addWidget(widget)
|
||||
self._title_widgets[idx] = widget
|
||||
self._error_info[idx] = item
|
||||
|
|
@ -471,6 +535,8 @@ class ValidationsWidget(QtWidgets.QWidget):
|
|||
if self._title_widgets:
|
||||
self._title_widgets[0].set_selected(True)
|
||||
|
||||
self.updateGeometry()
|
||||
|
||||
def _on_select(self, index):
|
||||
if self._previous_select:
|
||||
if self._previous_select.index == index:
|
||||
|
|
@ -481,10 +547,19 @@ class ValidationsWidget(QtWidgets.QWidget):
|
|||
|
||||
error_item = self._error_info[index]
|
||||
|
||||
dsc = error_item["exception"].description
|
||||
self._actions_widget.set_plugin(error_item["plugin"])
|
||||
|
||||
self._update_description()
|
||||
|
||||
def _on_instance_change(self, index):
|
||||
if self._previous_select and self._previous_select.index != index:
|
||||
return
|
||||
self._update_description()
|
||||
|
||||
def _update_description(self):
|
||||
description = self._previous_select.current_desctiption_text()
|
||||
if commonmark:
|
||||
html = commonmark.commonmark(dsc)
|
||||
html = commonmark.commonmark(description)
|
||||
self._error_details_input.setHtml(html)
|
||||
else:
|
||||
self._error_details_input.setMarkdown(dsc)
|
||||
self._actions_widget.set_plugin(error_item["plugin"])
|
||||
self._error_details_input.setMarkdown(description)
|
||||
|
|
|
|||
|
|
@ -535,6 +535,7 @@ class TasksCombobox(QtWidgets.QComboBox):
|
|||
return
|
||||
|
||||
self._text = text
|
||||
self.repaint()
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""Paint custom text without using QLineEdit.
|
||||
|
|
@ -548,6 +549,7 @@ class TasksCombobox(QtWidgets.QComboBox):
|
|||
self.initStyleOption(opt)
|
||||
if self._text is not None:
|
||||
opt.currentText = self._text
|
||||
|
||||
style = self.style()
|
||||
style.drawComplexControl(
|
||||
QtWidgets.QStyle.CC_ComboBox, opt, painter, self
|
||||
|
|
@ -609,11 +611,15 @@ class TasksCombobox(QtWidgets.QComboBox):
|
|||
if self._selected_items:
|
||||
is_valid = True
|
||||
|
||||
valid_task_names = []
|
||||
for task_name in self._selected_items:
|
||||
is_valid = self._model.is_task_name_valid(asset_name, task_name)
|
||||
if not is_valid:
|
||||
break
|
||||
_is_valid = self._model.is_task_name_valid(asset_name, task_name)
|
||||
if _is_valid:
|
||||
valid_task_names.append(task_name)
|
||||
else:
|
||||
is_valid = _is_valid
|
||||
|
||||
self._selected_items = valid_task_names
|
||||
if len(self._selected_items) == 0:
|
||||
self.set_selected_item("")
|
||||
|
||||
|
|
@ -625,6 +631,7 @@ class TasksCombobox(QtWidgets.QComboBox):
|
|||
if multiselection_text is None:
|
||||
multiselection_text = "|".join(self._selected_items)
|
||||
self.set_selected_item(multiselection_text)
|
||||
|
||||
self._set_is_valid(is_valid)
|
||||
|
||||
def set_selected_items(self, asset_task_combinations=None):
|
||||
|
|
@ -708,8 +715,7 @@ class TasksCombobox(QtWidgets.QComboBox):
|
|||
idx = self.findText(item_name)
|
||||
# Set current index (must be set to -1 if is invalid)
|
||||
self.setCurrentIndex(idx)
|
||||
if idx < 0:
|
||||
self.set_text(item_name)
|
||||
self.set_text(item_name)
|
||||
|
||||
def reset_to_origin(self):
|
||||
"""Change to task names set with last `set_selected_items` call."""
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
|
||||
# Content
|
||||
# Subset widget
|
||||
subset_frame = QtWidgets.QWidget(self)
|
||||
subset_frame = QtWidgets.QFrame(self)
|
||||
|
||||
subset_views_widget = BorderedLabelWidget(
|
||||
"Subsets to publish", subset_frame
|
||||
|
|
@ -225,6 +225,9 @@ class PublisherWindow(QtWidgets.QDialog):
|
|||
controller.add_publish_validated_callback(self._on_publish_validated)
|
||||
controller.add_publish_stopped_callback(self._on_publish_stop)
|
||||
|
||||
# Store header for TrayPublisher
|
||||
self._header_layout = header_layout
|
||||
|
||||
self.content_stacked_layout = content_stacked_layout
|
||||
self.publish_frame = publish_frame
|
||||
self.subset_frame = subset_frame
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class LocalExperimentalToolsWidgets(QtWidgets.QWidget):
|
|||
|
||||
layout.addRow(empty_label)
|
||||
|
||||
experimental_defs = ExperimentalTools(filter_hosts=False)
|
||||
experimental_defs = ExperimentalTools(refresh=False)
|
||||
checkboxes_by_identifier = {}
|
||||
for tool in experimental_defs.tools:
|
||||
checkbox = QtWidgets.QCheckBox(self)
|
||||
|
|
|
|||
6
openpype/tools/traypublisher/__init__.py
Normal file
6
openpype/tools/traypublisher/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from .window import main
|
||||
|
||||
|
||||
__all__ = (
|
||||
"main",
|
||||
)
|
||||
158
openpype/tools/traypublisher/window.py
Normal file
158
openpype/tools/traypublisher/window.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
"""Tray publisher is extending publisher tool.
|
||||
|
||||
Adds ability to select project using overlay widget with list of projects.
|
||||
|
||||
Tray publisher can be considered as host implementeation with creators and
|
||||
publishing plugins.
|
||||
"""
|
||||
|
||||
from Qt import QtWidgets, QtCore
|
||||
|
||||
import avalon.api
|
||||
from avalon.api import AvalonMongoDB
|
||||
from openpype.hosts.traypublisher import (
|
||||
api as traypublisher
|
||||
)
|
||||
from openpype.tools.publisher import PublisherWindow
|
||||
from openpype.tools.utils.constants import PROJECT_NAME_ROLE
|
||||
from openpype.tools.utils.models import (
|
||||
ProjectModel,
|
||||
ProjectSortFilterProxy
|
||||
)
|
||||
|
||||
|
||||
class StandaloneOverlayWidget(QtWidgets.QFrame):
|
||||
project_selected = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, publisher_window):
|
||||
super(StandaloneOverlayWidget, self).__init__(publisher_window)
|
||||
self.setObjectName("OverlayFrame")
|
||||
|
||||
# Create db connection for projects model
|
||||
dbcon = AvalonMongoDB()
|
||||
dbcon.install()
|
||||
|
||||
header_label = QtWidgets.QLabel("Choose project", self)
|
||||
header_label.setObjectName("ChooseProjectLabel")
|
||||
# Create project models and view
|
||||
projects_model = ProjectModel(dbcon)
|
||||
projects_proxy = ProjectSortFilterProxy()
|
||||
projects_proxy.setSourceModel(projects_model)
|
||||
|
||||
projects_view = QtWidgets.QListView(self)
|
||||
projects_view.setModel(projects_proxy)
|
||||
projects_view.setEditTriggers(
|
||||
QtWidgets.QAbstractItemView.NoEditTriggers
|
||||
)
|
||||
|
||||
confirm_btn = QtWidgets.QPushButton("Choose", self)
|
||||
btns_layout = QtWidgets.QHBoxLayout()
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(confirm_btn, 0)
|
||||
|
||||
layout = QtWidgets.QGridLayout(self)
|
||||
layout.addWidget(header_label, 0, 1, alignment=QtCore.Qt.AlignCenter)
|
||||
layout.addWidget(projects_view, 1, 1)
|
||||
layout.addLayout(btns_layout, 2, 1)
|
||||
layout.setColumnStretch(0, 1)
|
||||
layout.setColumnStretch(1, 0)
|
||||
layout.setColumnStretch(2, 1)
|
||||
layout.setRowStretch(0, 0)
|
||||
layout.setRowStretch(1, 1)
|
||||
layout.setRowStretch(2, 0)
|
||||
|
||||
projects_view.doubleClicked.connect(self._on_double_click)
|
||||
confirm_btn.clicked.connect(self._on_confirm_click)
|
||||
|
||||
self._projects_view = projects_view
|
||||
self._projects_model = projects_model
|
||||
self._confirm_btn = confirm_btn
|
||||
|
||||
self._publisher_window = publisher_window
|
||||
|
||||
def showEvent(self, event):
|
||||
self._projects_model.refresh()
|
||||
super(StandaloneOverlayWidget, self).showEvent(event)
|
||||
|
||||
def _on_double_click(self):
|
||||
self.set_selected_project()
|
||||
|
||||
def _on_confirm_click(self):
|
||||
self.set_selected_project()
|
||||
|
||||
def set_selected_project(self):
|
||||
index = self._projects_view.currentIndex()
|
||||
|
||||
project_name = index.data(PROJECT_NAME_ROLE)
|
||||
if not project_name:
|
||||
return
|
||||
|
||||
traypublisher.set_project_name(project_name)
|
||||
self.setVisible(False)
|
||||
self.project_selected.emit(project_name)
|
||||
|
||||
|
||||
class TrayPublishWindow(PublisherWindow):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TrayPublishWindow, self).__init__(reset_on_show=False)
|
||||
|
||||
overlay_widget = StandaloneOverlayWidget(self)
|
||||
|
||||
btns_widget = QtWidgets.QWidget(self)
|
||||
|
||||
back_to_overlay_btn = QtWidgets.QPushButton(
|
||||
"Change project", btns_widget
|
||||
)
|
||||
save_btn = QtWidgets.QPushButton("Save", btns_widget)
|
||||
# TODO implement save mechanism of tray publisher
|
||||
save_btn.setVisible(False)
|
||||
|
||||
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
|
||||
btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
btns_layout.addWidget(save_btn, 0)
|
||||
btns_layout.addWidget(back_to_overlay_btn, 0)
|
||||
|
||||
self._header_layout.addWidget(btns_widget, 0)
|
||||
|
||||
overlay_widget.project_selected.connect(self._on_project_select)
|
||||
back_to_overlay_btn.clicked.connect(self._on_back_to_overlay)
|
||||
save_btn.clicked.connect(self._on_tray_publish_save)
|
||||
|
||||
self._back_to_overlay_btn = back_to_overlay_btn
|
||||
self._overlay_widget = overlay_widget
|
||||
|
||||
def _on_back_to_overlay(self):
|
||||
self._overlay_widget.setVisible(True)
|
||||
self._resize_overlay()
|
||||
|
||||
def _resize_overlay(self):
|
||||
self._overlay_widget.resize(
|
||||
self.width(),
|
||||
self.height()
|
||||
)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super(TrayPublishWindow, self).resizeEvent(event)
|
||||
self._resize_overlay()
|
||||
|
||||
def _on_project_select(self, project_name):
|
||||
# TODO register project specific plugin paths
|
||||
self.controller.save_changes()
|
||||
self.controller.reset_project_data_cache()
|
||||
|
||||
self.reset()
|
||||
if not self.controller.instances:
|
||||
self._on_create_clicked()
|
||||
|
||||
def _on_tray_publish_save(self):
|
||||
self.controller.save_changes()
|
||||
print("NOT YET IMPLEMENTED")
|
||||
|
||||
|
||||
def main():
|
||||
avalon.api.install(traypublisher)
|
||||
app = QtWidgets.QApplication([])
|
||||
window = TrayPublishWindow()
|
||||
window.show()
|
||||
app.exec_()
|
||||
|
|
@ -2,11 +2,12 @@ from .widgets import (
|
|||
PlaceholderLineEdit,
|
||||
BaseClickableFrame,
|
||||
ClickableFrame,
|
||||
ClickableLabel,
|
||||
ExpandBtn,
|
||||
PixmapLabel,
|
||||
IconButton,
|
||||
)
|
||||
|
||||
from .views import DeselectableTreeView
|
||||
from .error_dialog import ErrorMessageBox
|
||||
from .lib import (
|
||||
WrappedCallbackItem,
|
||||
|
|
@ -24,10 +25,13 @@ __all__ = (
|
|||
"PlaceholderLineEdit",
|
||||
"BaseClickableFrame",
|
||||
"ClickableFrame",
|
||||
"ClickableLabel",
|
||||
"ExpandBtn",
|
||||
"PixmapLabel",
|
||||
"IconButton",
|
||||
|
||||
"DeselectableTreeView",
|
||||
|
||||
"ErrorMessageBox",
|
||||
|
||||
"WrappedCallbackItem",
|
||||
|
|
|
|||
|
|
@ -63,6 +63,29 @@ class ClickableFrame(BaseClickableFrame):
|
|||
self.clicked.emit()
|
||||
|
||||
|
||||
class ClickableLabel(QtWidgets.QLabel):
|
||||
"""Label that catch left mouse click and can trigger 'clicked' signal."""
|
||||
clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(ClickableLabel, self).__init__(parent)
|
||||
|
||||
self._mouse_pressed = False
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self._mouse_pressed = True
|
||||
super(ClickableLabel, self).mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self._mouse_pressed:
|
||||
self._mouse_pressed = False
|
||||
if self.rect().contains(event.pos()):
|
||||
self.clicked.emit()
|
||||
|
||||
super(ClickableLabel, self).mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class ExpandBtnLabel(QtWidgets.QLabel):
|
||||
"""Label showing expand icon meant for ExpandBtn."""
|
||||
def __init__(self, parent):
|
||||
|
|
|
|||
|
|
@ -433,7 +433,7 @@ class MultiFilesWidget(QtWidgets.QFrame):
|
|||
filenames = index.data(FILENAMES_ROLE)
|
||||
for filename in filenames:
|
||||
filepaths.add(os.path.join(dirpath, filename))
|
||||
return filepaths
|
||||
return list(filepaths)
|
||||
|
||||
def set_filters(self, folders_allowed, exts_filter):
|
||||
self._files_proxy_model.set_allow_folders(folders_allowed)
|
||||
|
|
@ -552,7 +552,7 @@ class MultiFilesWidget(QtWidgets.QFrame):
|
|||
self._update_visibility()
|
||||
|
||||
def _update_visibility(self):
|
||||
files_exists = self._files_model.rowCount() > 0
|
||||
files_exists = self._files_proxy_model.rowCount() > 0
|
||||
self._files_view.setVisible(files_exists)
|
||||
self._empty_widget.setVisible(not files_exists)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue